Windows网络编程
14、IOCP完成端口
1、课程目标
- 理解IOCP的设计思想和工作原理
- 掌握IOCP相关API的使用
- 实现高性能IOCP服务器
- 掌握IOCP在C2开发中的应用
2、名词解释
| 名词 | 全称 | 解释 |
|---|---|---|
| IOCP | I/O Completion Port | I/O完成端口 |
| Completion Port | - | 内核对象,管理异步I/O完成通知 |
| Worker Thread | 工作线程 | 处理I/O完成通知的线程 |
| Completion Key | 完成键 | 关联套接字的标识数据 |
| Per-Handle Data | 句柄数据 | 与套接字关联的上下文 |
| Per-I/O Data | I/O数据 | 与每次I/O操作关联的上下文 |
| GetQueuedCompletionStatus | - | 获取完成通知的函数 |
| PostQueuedCompletionStatus | - | 投递自定义完成通知 |
3、使用工具
- Visual Studio 2022
- Process Explorer(查看I/O完成端口)
- Windows Performance Toolkit
- Intel VTune(性能分析)
4、技术原理
4.1、IOCP工作原理
应用程序
|
+------------------+------------------+
| | |
AcceptEx 工作线程池 PostRecv/Send
| | |
| [Thread 1] |
| [Thread 2] |
| [Thread 3] |
| [Thread 4] |
| | |
+------------------+------------------+
|
I/O完成端口 (IOCP)
|
+------------------+------------------+
| | |
Socket 1 Socket 2 Socket 3
| | |
+------------------+------------------+
|
内核
管理异步I/O,通知完成
4.2、IOCP核心函数
// 创建完成端口
HANDLE CreateIoCompletionPort(
HANDLE FileHandle, // 关联的句柄(INVALID_HANDLE_VALUE创建新端口)
HANDLE ExistingCompletionPort, // 已有的完成端口(NULL创建新端口)
ULONG_PTR CompletionKey, // 完成键
DWORD NumberOfConcurrentThreads // 并发线程数(0=CPU核心数)
);
// 获取完成状态
BOOL GetQueuedCompletionStatus(
HANDLE CompletionPort, // 完成端口
LPDWORD lpNumberOfBytesTransferred, // 传输字节数
PULONG_PTR lpCompletionKey, // 完成键
LPOVERLAPPED* lpOverlapped, // 重叠结构
DWORD dwMilliseconds // 超时
);
// 获取多个完成状态(Vista+)
BOOL GetQueuedCompletionStatusEx(
HANDLE CompletionPort,
LPOVERLAPPED_ENTRY lpCompletionPortEntries, // 输出数组
ULONG ulCount, // 数组大小
PULONG ulNumEntriesRemoved, // 实际获取数量
DWORD dwMilliseconds,
BOOL fAlertable
);
// 投递自定义完成通知
BOOL PostQueuedCompletionStatus(
HANDLE CompletionPort,
DWORD dwNumberOfBytesTransferred,
ULONG_PTR dwCompletionKey,
LPOVERLAPPED lpOverlapped
);
5、代码实现
5.1、IOCP服务器基础结构
#include <winsock2.h>
#include <mswsock.h>
#include <windows.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")
// 操作类型
typedef enum _IO_OPERATION {
IO_ACCEPT,
IO_RECV,
IO_SEND,
IO_DISCONNECT
} IO_OPERATION;
// Per-Handle数据(每个套接字一个)
typedef struct _PER_HANDLE_DATA {
SOCKET socket;
char ip[32];
int port;
DWORD connectTime;
volatile LONG refCount; // 引用计数
} PER_HANDLE_DATA, *PPER_HANDLE_DATA;
// Per-I/O数据(每次I/O操作一个)
typedef struct _PER_IO_DATA {
WSAOVERLAPPED overlapped; // 必须是第一个成员
SOCKET socket;
WSABUF wsabuf;
char buffer[4096];
IO_OPERATION operation;
PPER_HANDLE_DATA handleData;
} PER_IO_DATA, *PPER_IO_DATA;
// IOCP服务器
typedef struct _IOCP_SERVER {
HANDLE completionPort;
SOCKET listenSocket;
HANDLE workerThreads[16];
int threadCount;
volatile BOOL running;
// AcceptEx相关
LPFN_ACCEPTEX AcceptEx;
LPFN_GETACCEPTEXSOCKADDRS GetAcceptExSockaddrs;
} IOCP_SERVER, *PIOCP_SERVER;
// 全局服务器实例
IOCP_SERVER g_Server = {0};
5.2、加载扩展函数
// 加载AcceptEx等扩展函数
BOOL LoadExtensionFunctions(SOCKET socket) {
GUID guidAcceptEx = WSAID_ACCEPTEX;
GUID guidGetAcceptExSockaddrs = WSAID_GETACCEPTEXSOCKADDRS;
DWORD bytes;
// 加载AcceptEx
if (WSAIoctl(socket, SIO_GET_EXTENSION_FUNCTION_POINTER,
&guidAcceptEx, sizeof(guidAcceptEx),
&g_Server.AcceptEx, sizeof(g_Server.AcceptEx),
&bytes, NULL, NULL) == SOCKET_ERROR) {
return FALSE;
}
// 加载GetAcceptExSockaddrs
if (WSAIoctl(socket, SIO_GET_EXTENSION_FUNCTION_POINTER,
&guidGetAcceptExSockaddrs, sizeof(guidGetAcceptExSockaddrs),
&g_Server.GetAcceptExSockaddrs,
sizeof(g_Server.GetAcceptExSockaddrs),
&bytes, NULL, NULL) == SOCKET_ERROR) {
return FALSE;
}
return TRUE;
}
5.3、创建Per-Handle和Per-I/O数据
// 创建Per-Handle数据
PPER_HANDLE_DATA CreatePerHandleData(SOCKET socket,
const char* ip, int port) {
PPER_HANDLE_DATA data = (PPER_HANDLE_DATA)HeapAlloc(
GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(PER_HANDLE_DATA)
);
if (data) {
data->socket = socket;
strncpy(data->ip, ip, sizeof(data->ip));
data->port = port;
data->connectTime = GetTickCount();
data->refCount = 1;
}
return data;
}
// 释放Per-Handle数据
void ReleasePerHandleData(PPER_HANDLE_DATA data) {
if (InterlockedDecrement(&data->refCount) == 0) {
HeapFree(GetProcessHeap(), 0, data);
}
}
// 创建Per-I/O数据
PPER_IO_DATA CreatePerIoData(IO_OPERATION op, PPER_HANDLE_DATA handleData) {
PPER_IO_DATA data = (PPER_IO_DATA)HeapAlloc(
GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(PER_IO_DATA)
);
if (data) {
data->operation = op;
data->handleData = handleData;
data->wsabuf.buf = data->buffer;
data->wsabuf.len = sizeof(data->buffer);
if (handleData) {
data->socket = handleData->socket;
InterlockedIncrement(&handleData->refCount);
}
}
return data;
}
// 释放Per-I/O数据
void FreePerIoData(PPER_IO_DATA data) {
if (data->handleData) {
ReleasePerHandleData(data->handleData);
}
HeapFree(GetProcessHeap(), 0, data);
}
5.4、投递AcceptEx
// 投递AcceptEx请求
BOOL PostAcceptEx(void) {
// 创建接受套接字
SOCKET acceptSocket = WSASocket(AF_INET, SOCK_STREAM, 0,
NULL, 0, WSA_FLAG_OVERLAPPED);
if (acceptSocket == INVALID_SOCKET) {
return FALSE;
}
// 创建Per-I/O数据
PPER_IO_DATA ioData = CreatePerIoData(IO_ACCEPT, NULL);
if (!ioData) {
closesocket(acceptSocket);
return FALSE;
}
ioData->socket = acceptSocket;
// 调用AcceptEx
DWORD bytesReceived;
BOOL result = g_Server.AcceptEx(
g_Server.listenSocket,
acceptSocket,
ioData->buffer, // 接收第一批数据
0, // 0表示不接收数据,只接受连接
sizeof(SOCKADDR_IN) + 16,
sizeof(SOCKADDR_IN) + 16,
&bytesReceived,
&ioData->overlapped
);
if (!result && WSAGetLastError() != ERROR_IO_PENDING) {
closesocket(acceptSocket);
FreePerIoData(ioData);
return FALSE;
}
return TRUE;
}
5.5、投递Recv和Send
// 投递接收请求
BOOL PostRecv(PPER_HANDLE_DATA handleData) {
PPER_IO_DATA ioData = CreatePerIoData(IO_RECV, handleData);
if (!ioData) {
return FALSE;
}
DWORD flags = 0;
DWORD bytesRecv;
int result = WSARecv(
handleData->socket,
&ioData->wsabuf,
1,
&bytesRecv,
&flags,
&ioData->overlapped,
NULL
);
if (result == SOCKET_ERROR && WSAGetLastError() != WSA_IO_PENDING) {
FreePerIoData(ioData);
return FALSE;
}
return TRUE;
}
// 投递发送请求
BOOL PostSend(PPER_HANDLE_DATA handleData, const char* data, int len) {
PPER_IO_DATA ioData = CreatePerIoData(IO_SEND, handleData);
if (!ioData) {
return FALSE;
}
memcpy(ioData->buffer, data, len);
ioData->wsabuf.len = len;
DWORD bytesSent;
int result = WSASend(
handleData->socket,
&ioData->wsabuf,
1,
&bytesSent,
0,
&ioData->overlapped,
NULL
);
if (result == SOCKET_ERROR && WSAGetLastError() != WSA_IO_PENDING) {
FreePerIoData(ioData);
return FALSE;
}
return TRUE;
}
5.6、工作线程
// 工作线程函数
DWORD WINAPI WorkerThread(LPVOID param) {
PIOCP_SERVER server = (PIOCP_SERVER)param;
DWORD bytesTransferred;
ULONG_PTR completionKey;
LPOVERLAPPED overlapped;
while (server->running) {
BOOL result = GetQueuedCompletionStatus(
server->completionPort,
&bytesTransferred,
&completionKey,
&overlapped,
INFINITE
);
// 检查退出信号
if (completionKey == 0 && overlapped == NULL) {
break;
}
PPER_IO_DATA ioData = CONTAINING_RECORD(
overlapped, PER_IO_DATA, overlapped
);
if (!result) {
// I/O失败
DWORD err = GetLastError();
printf("[-] I/O失败: %d\n", err);
if (ioData->handleData) {
closesocket(ioData->handleData->socket);
}
FreePerIoData(ioData);
continue;
}
switch (ioData->operation) {
case IO_ACCEPT:
HandleAcceptComplete(ioData, bytesTransferred);
break;
case IO_RECV:
HandleRecvComplete(ioData, bytesTransferred);
break;
case IO_SEND:
HandleSendComplete(ioData, bytesTransferred);
break;
}
}
return 0;
}
// 处理AcceptEx完成
void HandleAcceptComplete(PPER_IO_DATA ioData, DWORD bytesTransferred) {
SOCKET acceptSocket = ioData->socket;
// 更新套接字属性
setsockopt(acceptSocket, SOL_SOCKET, SO_UPDATE_ACCEPT_CONTEXT,
(char*)&g_Server.listenSocket, sizeof(SOCKET));
// 获取地址信息
SOCKADDR_IN *localAddr, *remoteAddr;
int localLen, remoteLen;
g_Server.GetAcceptExSockaddrs(
ioData->buffer,
0,
sizeof(SOCKADDR_IN) + 16,
sizeof(SOCKADDR_IN) + 16,
(SOCKADDR**)&localAddr, &localLen,
(SOCKADDR**)&remoteAddr, &remoteLen
);
char* ip = inet_ntoa(remoteAddr->sin_addr);
int port = ntohs(remoteAddr->sin_port);
printf("[+] 新连接: %s:%d\n", ip, port);
// 创建Per-Handle数据
PPER_HANDLE_DATA handleData = CreatePerHandleData(acceptSocket, ip, port);
// 关联到完成端口
CreateIoCompletionPort(
(HANDLE)acceptSocket,
g_Server.completionPort,
(ULONG_PTR)handleData,
0
);
// 投递接收请求
PostRecv(handleData);
// 释放Accept的I/O数据
FreePerIoData(ioData);
// 投递新的AcceptEx
PostAcceptEx();
}
// 处理接收完成
void HandleRecvComplete(PPER_IO_DATA ioData, DWORD bytesTransferred) {
PPER_HANDLE_DATA handleData = ioData->handleData;
if (bytesTransferred == 0) {
// 连接断开
printf("[-] 断开: %s:%d\n", handleData->ip, handleData->port);
closesocket(handleData->socket);
FreePerIoData(ioData);
return;
}
ioData->buffer[bytesTransferred] = '\0';
printf("[%s:%d] %s\n", handleData->ip, handleData->port, ioData->buffer);
// 回显
char response[4096];
int len = sprintf(response, "Echo: %s", ioData->buffer);
PostSend(handleData, response, len);
FreePerIoData(ioData);
}
// 处理发送完成
void HandleSendComplete(PPER_IO_DATA ioData, DWORD bytesTransferred) {
PPER_HANDLE_DATA handleData = ioData->handleData;
// 发送完成,继续接收
PostRecv(handleData);
FreePerIoData(ioData);
}
5.7、服务器初始化和运行
// 初始化IOCP服务器
BOOL InitIOCPServer(int port, int threadCount) {
WSADATA wsa;
WSAStartup(MAKEWORD(2, 2), &wsa);
// 创建完成端口
g_Server.completionPort = CreateIoCompletionPort(
INVALID_HANDLE_VALUE, NULL, 0, 0
);
if (!g_Server.completionPort) {
return FALSE;
}
// 创建监听套接字
g_Server.listenSocket = WSASocket(
AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED
);
if (g_Server.listenSocket == INVALID_SOCKET) {
return FALSE;
}
// 加载扩展函数
if (!LoadExtensionFunctions(g_Server.listenSocket)) {
return FALSE;
}
// 绑定和监听
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(port);
bind(g_Server.listenSocket, (struct sockaddr*)&addr, sizeof(addr));
listen(g_Server.listenSocket, SOMAXCONN);
// 关联监听套接字到完成端口
CreateIoCompletionPort(
(HANDLE)g_Server.listenSocket,
g_Server.completionPort,
0,
0
);
// 创建工作线程
if (threadCount <= 0) {
SYSTEM_INFO sysInfo;
GetSystemInfo(&sysInfo);
threadCount = sysInfo.dwNumberOfProcessors * 2;
}
g_Server.threadCount = threadCount;
g_Server.running = TRUE;
for (int i = 0; i < threadCount; i++) {
g_Server.workerThreads[i] = CreateThread(
NULL, 0, WorkerThread, &g_Server, 0, NULL
);
}
// 预投递AcceptEx
for (int i = 0; i < 10; i++) {
PostAcceptEx();
}
printf("[+] IOCP服务器启动,端口: %d, 线程: %d\n", port, threadCount);
return TRUE;
}
// 停止服务器
void StopIOCPServer(void) {
g_Server.running = FALSE;
// 发送退出信号给所有工作线程
for (int i = 0; i < g_Server.threadCount; i++) {
PostQueuedCompletionStatus(
g_Server.completionPort, 0, 0, NULL
);
}
// 等待工作线程结束
WaitForMultipleObjects(
g_Server.threadCount,
g_Server.workerThreads,
TRUE,
5000
);
closesocket(g_Server.listenSocket);
CloseHandle(g_Server.completionPort);
WSACleanup();
}
5.8、C2服务器 - IOCP实现
// C2 IOCP服务器
typedef struct _C2_IOCP_SERVER {
IOCP_SERVER base;
// Agent管理
PPER_HANDLE_DATA agents[1024];
int agentCount;
CRITICAL_SECTION agentLock;
} C2_IOCP_SERVER;
C2_IOCP_SERVER g_C2Server = {0};
// 处理Agent消息
void HandleAgentMessage(PPER_HANDLE_DATA agent,
void* data, DWORD length) {
MSG_HEADER* header = (MSG_HEADER*)data;
void* payload = (char*)data + sizeof(MSG_HEADER);
DWORD payloadLen = length - sizeof(MSG_HEADER);
switch (header->Type) {
case MSG_CHECKIN:
HandleCheckin(agent, payload, payloadLen);
break;
case MSG_HEARTBEAT:
printf("[Heartbeat] %s:%d\n", agent->ip, agent->port);
break;
case MSG_RESULT:
HandleCommandResult(agent, payload, payloadLen);
break;
case MSG_FILE_DATA:
HandleFileData(agent, payload, payloadLen);
break;
}
}
// 向Agent发送命令
BOOL SendCommandToAgent(PPER_HANDLE_DATA agent,
DWORD cmdType,
const void* data,
DWORD dataLen) {
DWORD totalLen = sizeof(MSG_HEADER) + dataLen;
char* buffer = (char*)malloc(totalLen);
MSG_HEADER* header = (MSG_HEADER*)buffer;
header->Magic = 0xDEADBEEF;
header->Type = cmdType;
header->Length = totalLen;
header->Sequence = GetTickCount();
if (data && dataLen > 0) {
memcpy(buffer + sizeof(MSG_HEADER), data, dataLen);
}
BOOL result = PostSend(agent, buffer, totalLen);
free(buffer);
return result;
}
// 广播命令
void BroadcastCommand(DWORD cmdType, const void* data, DWORD dataLen) {
EnterCriticalSection(&g_C2Server.agentLock);
for (int i = 0; i < g_C2Server.agentCount; i++) {
SendCommandToAgent(g_C2Server.agents[i], cmdType, data, dataLen);
}
LeaveCriticalSection(&g_C2Server.agentLock);
}
int main() {
InitializeCriticalSection(&g_C2Server.agentLock);
if (!InitIOCPServer(4444, 0)) {
printf("服务器启动失败\n");
return 1;
}
printf("按Enter键停止服务器...\n");
getchar();
StopIOCPServer();
DeleteCriticalSection(&g_C2Server.agentLock);
return 0;
}
6、课后作业
-
实现连接池和内存池
- 预分配Per-I/O数据
- 实现高效的对象复用
- 减少内存分配开销
-
实现零拷贝传输
- 使用TransmitFile
- 使用TransmitPackets
- 实现高效文件下发
-
性能压力测试
- 测试最大连接数
- 测试吞吐量
- 对比其他I/O模型