IOCP(I/O Completion Port)的核心范式:
CreateIoCompletionPort()// 创建完成端口CreateIoCompletionPort(sock,iocp)// 把socket绑定到端口bind/listen/accept// 服务端WSASend/WSARecv(OVERLAPPED)// 投递异步IOGetQueuedCompletionStatus()// 工作线程取完成事件关键结构:每个连接一个PerSocketContext,每次IO一个PerIoContext(内嵌OVERLAPPED)。
1.公共头文件 common.h
#pragmaonce#include<WinSock2.h>#include<MSWSock.h>// AcceptEx, ConnectEx#include<Windows.h>#include<cstdio>#pragmacomment(lib,"ws2_32.lib")#defineMAX_BUFFER4096#defineWORKER_THREADS4// 每次异步IO的上下文(必须包含 OVERLAPPED,且是第一个成员)structPerIoContext{OVERLAPPED overlapped;WSABUF wsaBuf;charbuffer[MAX_BUFFER];intopType;// OP_RECV / OP_SEND / OP_ACCEPTSOCKET sockAccept;// AcceptEx 用};enum{OP_NONE=0,OP_ACCEPT,OP_RECV,OP_SEND,OP_CONNECT};// 每个连接的上下文structPerSocketContext{SOCKET sock;SOCKADDR_IN addr;};// 全局函数指针LPFN_ACCEPTEX lpfnAcceptEx=nullptr;LPFN_CONNECTEX lpfnConnectEx=nullptr;LPFN_GETACCEPTEXSOCKADDRS lpfnGetAcceptExSockaddrs=nullptr;voidLoadExtensionFuncs(SOCKET sock){GUID guidAcceptEx=WSAID_ACCEPTEX;GUID guidConnectEx=WSAID_CONNECTEX;GUID guidGetAddr=WSAID_GETACCEPTEXSOCKADDRS;DWORD bytes=0;WSAIoctl(sock,SIO_GET_EXTENSION_FUNCTION_POINTER,&guidAcceptEx,sizeof(guidAcceptEx),&lpfnAcceptEx,sizeof(lpfnAcceptEx),&bytes,NULL,NULL);WSAIoctl(sock,SIO_GET_EXTENSION_FUNCTION_POINTER,&guidConnectEx,sizeof(guidConnectEx),&lpfnConnectEx,sizeof(lpfnConnectEx),&bytes,NULL,NULL);WSAIoctl(sock,SIO_GET_EXTENSION_FUNCTION_POINTER,&guidGetAddr,sizeof(guidGetAddr),&lpfnGetAcceptExSockaddrs,sizeof(lpfnGetAcceptExSockaddrs),&bytes,NULL,NULL);}voidPostRecv(SOCKET sock,PerIoContext*io){ZeroMemory(&io->overlapped,sizeof(OVERLAPPED));io->opType=OP_RECV;io->wsaBuf.buf=io->buffer;io->wsaBuf.len=MAX_BUFFER;DWORD flags=0,bytes=0;// 返回 SOCKET_ERROR 且 WSAGetLastError()==WSA_IO_PENDING 是正常的WSARecv(sock,&io->wsaBuf,1,&bytes,&flags,&io->overlapped,NULL);}2. 服务端 server.cpp
#include"common.h"// 投递一个 AcceptExvoidPostAccept(SOCKET listenSock,HANDLE iocp){PerIoContext*io=newPerIoContext();ZeroMemory(io,sizeof(PerIoContext));io->opType=OP_ACCEPT;io->sockAccept=WSASocket(AF_INET,SOCK_STREAM,IPPROTO_TCP,NULL,0,WSA_FLAG_OVERLAPPED);// 把 accept socket 也关联到 IOCP(提前绑定)CreateIoCompletionPort((HANDLE)io->sockAccept,iocp,0,0);DWORD bytes=0;lpfnAcceptEx(listenSock,io->sockAccept,io->buffer,0,sizeof(SOCKADDR_IN)+16,sizeof(SOCKADDR_IN)+16,&bytes,&io->overlapped);}DWORD WINAPIWorkerThread(LPVOID lpParam){HANDLE iocp=(HANDLE)lpParam;DWORD bytes=0;ULONG_PTR key=0;OVERLAPPED*lpOverlapped=nullptr;while(true){BOOL ok=GetQueuedCompletionStatus(iocp,&bytes,&key,&lpOverlapped,INFINITE);if(!ok&&lpOverlapped==nullptr)continue;// 端口关闭PerIoContext*io=CONTAINING_RECORD(lpOverlapped,PerIoContext,overlapped);if(!ok||(bytes==0&&io->opType==OP_RECV)){// 连接断开或出错if(io->opType==OP_ACCEPT){closesocket(io->sockAccept);}else{closesocket((SOCKET)key);}delete io;continue;}switch(io->opType){caseOP_ACCEPT:{// 获取客户端地址SOCKADDR_IN*local=nullptr,*remote=nullptr;intlocalLen=sizeof(SOCKADDR_IN),remoteLen=sizeof(SOCKADDR_IN);lpfnGetAcceptExSockaddrs(io->buffer,0,sizeof(SOCKADDR_IN)+16,sizeof(SOCKADDR_IN)+16,(SOCKADDR**)&local,&localLen,(SOCKADDR**)&remote,&remoteLen);printf("[Server] Client connected: %s:%d\n",inet_ntoa(remote->sin_addr),ntohs(remote->sin_port));// 更新 accept socket 的 CompletionKey 为 socket 本身CreateIoCompletionPort((HANDLE)io->sockAccept,iocp,(ULONG_PTR)io->sockAccept,0);// 新连接立即投递第一个 RecvPerIoContext*recvIo=newPerIoContext();PostRecv(io->sockAccept,recvIo);// 继续投递下一个 AcceptPostAccept((SOCKET)key,iocp);delete io;break;}caseOP_RECV:{printf("[Server] Recv %u bytes: %.100s\n",bytes,io->buffer);// 回显给客户端(简单echo)PerIoContext*sendIo=newPerIoContext();memcpy(sendIo->buffer,io->buffer,bytes);ZeroMemory(&sendIo->overlapped,sizeof(OVERLAPPED));sendIo->opType=OP_SEND;sendIo->wsaBuf.buf=sendIo->buffer;sendIo->wsaBuf.len=bytes;DWORD sent=0;WSASend((SOCKET)key,&sendIo->wsaBuf,1,&sent,0,&sendIo->overlapped,NULL);// 继续投递下一次 Recv —— IOCP常见写法ZeroMemory(&io->overlapped,sizeof(OVERLAPPED));io->opType=OP_RECV;io->wsaBuf.len=MAX_BUFFER;DWORD flags=0,rb=0;WSARecv((SOCKET)key,&io->wsaBuf,1,&rb,&flags,&io->overlapped,NULL);break;}caseOP_SEND:delete io;break;}}return0;}intmain(){WSADATA wsa;WSAStartup(MAKEWORD(2,2),&wsa);SOCKET listenSock=WSASocket(AF_INET,SOCK_STREAM,IPPROTO_TCP,NULL,0,WSA_FLAG_OVERLAPPED);LoadExtensionFuncs(listenSock);// 1. 创建完成端口HANDLE iocp=CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,0,0);// 2. 把 listen socket 绑定到端口,CompletionKey = listenSockCreateIoCompletionPort((HANDLE)listenSock,iocp,(ULONG_PTR)listenSock,0);// 3. 启动工作线程(数量 ≈ CPU核数)SYSTEM_INFO si;GetSystemInfo(&si);for(DWORD i=0;i<si.dwNumberOfProcessors*2;i++)CreateThread(NULL,0,WorkerThread,iocp,0,NULL);// 4. bind & listenSOCKADDR_IN addr{};addr.sin_family=AF_INET;addr.sin_addr.s_addr=INADDR_ANY;addr.sin_port=htons(8888);bind(listenSock,(SOCKADDR*)&addr,sizeof(addr));listen(listenSock,SOMAXCONN);printf("[Server] Listening on 8888...\n");// 5. 预投递多个 AcceptExfor(inti=0;i<10;i++)PostAccept(listenSock,iocp);Sleep(INFINITE);return0;}3.客户端 client.cpp
#include"common.h"intmain(){WSADATA wsa;WSAStartup(MAKEWORD(2,2),&wsa);SOCKET sock=WSASocket(AF_INET,SOCK_STREAM,IPPROTO_TCP,NULL,0,WSA_FLAG_OVERLAPPED);LoadExtensionFuncs(sock);// 1. 创建 IOCP 并绑定 socketHANDLE iocp=CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,0,0);CreateIoCompletionPort((HANDLE)sock,iocp,(ULONG_PTR)sock,0);// 2. 必须先 bind 才能用 ConnectExSOCKADDR_IN localAddr{};localAddr.sin_family=AF_INET;localAddr.sin_addr.s_addr=INADDR_ANY;localAddr.sin_port=0;bind(sock,(SOCKADDR*)&localAddr,sizeof(localAddr));// 3. 投递 ConnectEx(异步连接)SOCKADDR_IN serverAddr{};serverAddr.sin_family=AF_INET;serverAddr.sin_addr.s_addr=inet_addr("127.0.0.1");serverAddr.sin_port=htons(8888);PerIoContext*connIo=newPerIoContext();ZeroMemory(connIo,sizeof(PerIoContext));connIo->opType=OP_CONNECT;lpfnConnectEx(sock,(SOCKADDR*)&serverAddr,sizeof(serverAddr),NULL,0,NULL,&connIo->overlapped);// 4. 工作线程:等待连接完成 → 发送 → 接收CreateThread(NULL,0,[](LPVOID p)->DWORD{HANDLE iocp=(HANDLE)p;DWORD bytes;ULONG_PTR key;OVERLAPPED*ov;bool connected=false;while(true){GetQueuedCompletionStatus(iocp,&bytes,&key,&ov,INFINITE);PerIoContext*io=CONTAINING_RECORD(ov,PerIoContext,overlapped);if(io->opType==OP_CONNECT){printf("[Client] Connected!\n");connected=true;// 连接成功后投递发送PerIoContext*sendIo=newPerIoContext();strcpy_s(sendIo->buffer,"Hello IOCP Server!");sendIo->opType=OP_SEND;sendIo->wsaBuf.buf=sendIo->buffer;sendIo->wsaBuf.len=(ULONG)strlen(sendIo->buffer);DWORD sent=0;WSASend((SOCKET)key,&sendIo->wsaBuf,1,&sent,0,&sendIo->overlapped,NULL);// 同时投递接收PerIoContext*recvIo=newPerIoContext();PostRecv((SOCKET)key,recvIo);}elseif(io->opType==OP_SEND){printf("[Client] Sent %u bytes\n",bytes);delete io;}elseif(io->opType==OP_RECV){if(bytes>0){printf("[Client] Recv: %.*s\n",bytes,io->buffer);// 继续接收ZeroMemory(&io->overlapped,sizeof(OVERLAPPED));io->wsaBuf.len=MAX_BUFFER;DWORD flags=0,rb=0;WSARecv((SOCKET)key,&io->wsaBuf,1,&rb,&flags,&io->overlapped,NULL);}else{printf("[Client] Server closed.\n");delete io;break;}}}return0;},iocp,0,NULL);Sleep(INFINITE);return0;}4.关键范式要点总结
| 要点 | 说明 |
|---|---|
| PerIoContext 内嵌 OVERLAPPED | 且OVERLAPPED必须是第一个成员,这样GetQueuedCompletionStatus返回的指针直接强转 |
| 投递后立即返回 | WSARecv/WSASend/AcceptEx返回SOCKET_ERROR+WSA_IO_PENDING是正常路径 |
| 零拷贝技巧 | 复用同一个 PerIoContext 连续投递 Recv(示例中 OP_RECV 分支的做法),减少内存分配 |
| AcceptEx 预投递 | 监听 socket 应一次性投递多个 AcceptEx(通常几十个),accept socket 提前绑定 IOCP |
| 断线判断 | GetQueuedCompletionStatus返回 FALSE,或 RECV 完成字节数为 0,即为对端关闭 |
| 线程数 | 工作线程数 ≈ CPU 核数(CreateIoCompletionPort最后一个参数为 0 时自动调度) |
| GetQueuedCompletionStatus 的 key | 常用做法:CompletionKey 直接存 SOCKET 句柄,省掉查表 |
- 这是
IOCP最典型的"Proactor+ 多线程工作池"模型:主线程只负责投递操作,所有完成的IO事件由工作线程池统一取出处理。 - 字节流协议下,接接收完成时,需要自行进行黏包处理。