socket详解与实现
字数 1608 2025-08-24 23:51:23
Socket详解与实现 - 教学文档
1. Socket基础概念
Socket(套接字)是应用程序通过网络发出请求或应答网络请求的接口,本质上是对TCP/IP协议的API封装。在数据传输中,Socket是病毒木马常用的技术手段之一。
1.1 Socket缓冲区
每个Socket被创建后都会分配两个缓冲区:
- 输入缓冲区:用于接收数据
- 输出缓冲区:用于发送数据
关键特性:
- I/O缓冲区在每个TCP套接字中单独存在
- 创建套接字时自动生成
- 关闭套接字会继续传送输出缓冲区中遗留的数据
- 关闭套接字将丢失输入缓冲区中的数据
1.2 数据发送/接收机制
write()/send()函数:
- 不立即传输数据,而是先写入输出缓冲区
- 由TCP协议负责将数据从缓冲区发送到目标机器
- 函数成功返回仅表示数据已写入缓冲区,不代表已到达目标
read()/recv()函数:
- 从输入缓冲区读取数据,而非直接从网络读取
- 如果缓冲区无数据,函数会被阻塞直到数据到达
2. 阻塞模式详解
2.1 write()/send()阻塞情况
- 缓冲区空间不足:暂停执行直到空间足够
- 缓冲区被锁定(正在发送数据):等待解锁
- 数据大于缓冲区:分批写入
- 所有数据写入缓冲区后函数才能返回
2.2 read()/recv()阻塞情况
- 缓冲区无数据:阻塞直到数据到来
- 读取长度小于缓冲区数据:不能一次性读取所有数据
- 读取到数据后函数才会返回
3. TCP粘包问题
3.1 问题描述
由于Socket缓冲区的存在,多次发送的数据可能被一次性接收,导致数据边界不清晰。例如:
- 发送三次"abc",可能接收为"abcabcabc"
- 发送"1"和"3"可能被接收为"13"
3.2 解决方案
需要在应用层实现:
- 固定长度数据包
- 特殊字符分隔
- 在数据头添加长度信息
4. TCP连接建立过程(三次握手)
4.1 TCP数据包关键字段
- 序号(Seq):32位,标识数据包序号
- 确认号(Ack):32位,Ack = 收到的Seq + 1
- 标志位:
- URG:紧急指针有效
- ACK:确认序号有效
- PSH:尽快交给应用层
- RST:重置连接
- SYN:建立新连接
- FIN:断开连接
4.2 三次握手流程
-
客户端→服务器:
- 设置SYN=1,Seq=随机数X
- 客户端进入SYN-SEND状态
-
服务器→客户端:
- 设置SYN=1,ACK=1,Seq=随机数Y,Ack=X+1
- 服务器进入SYN-RECV状态
-
客户端→服务器:
- 设置ACK=1,Ack=Y+1
- 双方进入ESTABLISHED状态
5. Socket编程实现
5.1 Winsock基础
Windows下的网络编程接口,主要版本:
- Winsock1:头文件WINSOCK.H,库WSOCK32.LIB
- Winsock2:头文件WINSOCK2.H,库WS2_32.LIB
- 扩展功能:MSWSOCK.H,库MSWSOCK.LIB
5.2 关键API函数
-
socket():创建套接字
SOCKET WSAAPI socket(int af, int type, int protocol); -
bind():绑定地址和套接字
int bind(SOCKET s, const sockaddr *addr, int namelen); -
listen():监听连接
int WSAAPI listen(SOCKET s, int backlog); -
accept():接受连接
-
connect():客户端连接服务器
-
send()/recv():数据收发
5.3 服务端实现
BOOL SocketListen(LPSTR ipaddr, int port) {
// 初始化Winsock
WSADATA wsadata = {0};
WORD w_version_req = MAKEWORD(2, 2);
if(WSAStartup(w_version_req, &wsadata) == SOCKET_ERROR || &wsadata == nullptr) {
printf("[!] Failed to initialize Winsock\n");
return FALSE;
}
// 创建流式socket
g_ServerSocket = socket(AF_INET, SOCK_STREAM, NULL);
if(g_ServerSocket == INVALID_SOCKET) {
printf("[!] Create socket Failed\n");
return FALSE;
}
// 设置服务端地址和端口
sockaddr_in ServerAddr;
ServerAddr.sin_family = AF_INET;
ServerAddr.sin_port = ::htons(port);
ServerAddr.sin_addr.S_un.S_addr = ::inet_addr(ipaddr);
// 绑定端口ip
if(NULL != ::bind(g_ServerSocket, (LPSOCKADDR)&ServerAddr, sizeof(ServerAddr))) {
printf("[!] Bind port failed\n");
return FALSE;
}
// 设置监听客户端数量
if(NULL != ::listen(g_ServerSocket, 5)) {
printf("[!] Listen port failed\n");
return FALSE;
}
return TRUE;
}
void AcceptMessage() {
sockaddr_in addr = {0};
int dwLength = sizeof(addr);
g_clientsocket = ::accept(g_ServerSocket, (sockaddr*)(&addr), &dwLength);
char szBuffer[MAX_PATH] = {0};
while(TRUE) {
int Ret = ::recv(g_clientsocket, szBuffer, MAX_PATH, 0);
if(Ret <= 0) continue;
printf("[*] recv:%s\n", szBuffer);
}
}
void SendMessage() {
char cmd[100] = {0};
cin.getline(cmd, 100);
::send(g_clientsocket, cmd, (::strlen(cmd)+1), 0);
printf("[*] send:%s\n", cmd);
}
5.4 客户端实现
// 初始化Winsock环境
WSADATA wsadata = {0};
WORD w_version_req = MAKEWORD(2, 2);
WSAStartup(w_version_req, &wsadata);
// 创建流式socket
SOCKET g_SeverSocket = socket(AF_INET, SOCK_STREAM, NULL);
// 连接服务端
connect(g_SeverSocket, (LPSOCKADDR)&ServerAddr, sizeof(ServerAddr));
// 创建线程接收数据
::CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)ThreadProc, NULL, NULL, NULL);
void SendMsg(char* pszSend) {
::send(g_ClientSocket, pszSend, (::strlen(pszSend)+1), 0);
printf("[*] Sent:%s", pszSend);
}
void GetMsg() {
char szBuffer[MAX_PATH] = {0};
while(TRUE) {
int Ret = ::recv(g_ClientSocket, szBuffer, MAX_PATH, 0);
if(Ret <= 0) continue;
system(szBuffer);
SendMsg((LPSTR)"The command executed successfully");
}
}
6. 进程间通信实现命令结果返回
6.1 匿名管道实现
HANDLE hRead;
HANDLE hWrite;
SECURITY_ATTRIBUTES sa;
sa.bInheritHandle = TRUE;
sa.lpSecurityDescriptor = NULL;
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
if(!CreatePipe(&hRead, &hWrite, &sa, 0)) {
printf("CreatePipe Failed\n\n");
return FALSE;
}
STARTUPINFO si;
ZeroMemory(&si, sizeof(STARTUPINFO));
si.cb = sizeof(STARTUPINFO);
si.dwFlags = STARTF_USESTDHANDLES;
si.hStdInput = hRead;
si.hStdOutput = hWrite;
si.hStdError = GetStdHandle(STD_ERROR_HANDLE);
if(!::CreateProcessA(NULL, lpscmd, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi)) {
printf("Create Process failed, error is : %d", GetLastError());
return FALSE;
}
CloseHandle(hWrite);
::WaitForSingleObject(pi.hThread, -1);
::WaitForSingleObject(pi.hProcess, -1);
::RtlZeroMemory(lpsRetBuffer, RetBufferSize);
if(!::ReadFile(hRead, lpsRetBuffer, 4096, &RetBufferSize, NULL)) {
printf("Readfile failed, error is : %d", GetLastError());
return FALSE;
}
CloseHandle(hRead);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return TRUE;
6.2 实现效果
使用匿名管道后,服务端可以接收到客户端命令执行的结果,解决了system()函数直接调用无法获取返回结果的问题。
7. 总结
- Socket是网络编程的基础,理解缓冲区和阻塞机制至关重要
- TCP三次握手确保可靠连接建立
- 粘包问题需要在应用层解决
- Windows下使用Winsock API进行Socket编程
- 匿名管道是实现进程间通信获取命令结果的有效方法
通过以上实现,可以构建一个完整的客户端-服务器通信系统,包括命令执行和结果返回功能。