一、IOCP简介
IOCP(I/O Completion Port,I/O完成端口)是Windows操作系统中伸缩性最好的一种I/O模型。
I/O 完成端口是应用程序使用线程池处理异步 I/O 请求的一种机制。处理多个并发异步I/O请求时,使用 I/O 完成端口比在 I/O 请求时创建线程更快更高效。
二、IOCP的优势
I/O 完成端口可以充分利用 Windows 内核来进行 I/O 调度,相较于传统的 Winsock 模型,IOCP 在机制上有明显的优势。
| 模型 | 机制 | 特性 |
|---|---|---|
| select模型 | 通过select函数来管理I/O,可以确定一个或多个套接字的状态 | 该模型的优势是程序能够在单个线程内同时处理多个套接字连接,避免了阻塞模式下的线程膨胀 |
| WSAAsyncSelect模型 | WSAAsyncSelect函数把socket设为非阻塞模式,并为socket绑定一个窗口句柄,依靠Windows的消息驱动机制,通过窗口进行消息接收、事件处理 | 该模型最突出的特点是与Windows的消息驱动机制融合在一起,使得开发带GUI界面的网络程序更简单 |
| WSAEventSelect模型 | 该模型与WSAAsyncSelect模型类似,允许应用程序在一个或多个socket上接收基于事件的网络通知,不过该模型是经由事件对象句柄通知的 | 该模型简单易用,也不需要窗口环境,缺点是最多等待64个事件对象的限制,当socket连接数量增加时,必须创建多个线程来处理I/O |
| 重叠I/O模型 | 该模型引入了重叠数据结构,允许应用程序使用重叠结构一次投递一个或多个异步I/O请求 | 该模型使用Winsock 2.0库的API,如:WSASend、WSARecv等,真正做到了“异步处理” |
| IOCP模型 | IOCP模型通过socket绑定完成端口,在socket上投递事件,工作线程在完成端口上轮询接收、处理事件 | IOCP充分利用内核对象的调度,只使用少量的几个线程来处理所有网络通信,消除了无谓的线程上下文切换,最大限度地提高了网络通信的性能 |
相较于传统的Winsock模型,IOCP的优势主要体现在两方面:独特的异步I/O方式和优秀的线程调度机制。
独特的异步I/O方式
IOCP模型在异步通信方式的基础上,设计了一套能够充分利用Windows内核的I/O通信机制,主要过程为:① socket关联iocp,② 在socket上投递I/O请求,③ 事件完成返回完成通知封包,④ 工作线程在iocp上处理事件。

IOCP的这种工作模式:程序只需要把事件投递出去,事件交给操作系统完成后,工作线程在完成端口上轮询处理。该模式充分利用了异步模式高速率输入输出的优势,能够有效提高程序的工作效率。
优秀的线程调度机制
完成端口可以抽象为一个公共消息队列,当用户请求到达时,完成端口把这些请求加入其抽象出的公共消息队列。这一过程与多个工作线程轮询消息队列并从中取出消息加以处理是并发操作。这种方式很好地实现了异步通信和负载均衡,因为它使几个线程“公平地”处理多客户端的I/O,并且线程空闲时会被挂起,不会占用CPU周期。
IOCP模型充分利用Windows系统内核,可以实现仅用少量的几个线程来处理和多个client之间的所有通信,消除了无谓的线程上下文切换,最大限度的提高了网络通信的性能。
三、IOCP的使用
初次学习使用IOCP的朋友在熟悉各个API时,建议参看MSDN的官方文档MSDN
IOCP的使用主要分为以下几步:
- 创建完成端口(iocp)对象
- 创建一个或多个工作线程,在完成端口上执行并处理投递到完成端口上的I/O请求
- Socket关联iocp对象,在Socket上投递网络事件
- 工作线程调用GetQueuedCompletionStatus函数获取完成通知封包,取得事件信息并进行处理
1 创建完成端口对象
使用IOCP模型,首先要调用 CreateIoCompletionPort 函数创建一个完成端口对象,Winsock将使用这个对象为任意数量的套接字句柄管理 I/O 请求。函数定义如下:
HANDLE WINAPI CreateIoCompletionPort(_In_ HANDLE FileHandle,_In_opt_ HANDLE ExistingCompletionPort,_In_ ULONG_PTR CompletionKey,_In_ DWORD NumberOfConcurrentThreads
);
此函数的两个不同功能:
- 创建一个完成端口对象
- 将一个或多个文件句柄(这里是套接字句柄)关联到 I/O 完成端口对象
最初创建完成端口对象时,唯一需要设置的参数是 NumberOfConcurrentThreads,该参数定义了 允许在完成端口上同时执行的线程的数量。理想情况下,我们希望每个处理器仅运行一个线程来为完成端口提供服务,以避免线程上下文切换。NumberOfConcurrentThreads 为0表示系统允许的线程数量和处理器数量一样多。因此,可以简单地使用以下代码创建完成端口对象,取得标识完成端口的句柄。
HANDLE m_hCompletion = ::CreateIoCompletionPort(INVALID_HANDLE_VALUE,0,0,0);
2 I/O工作线程和完成端口
I/O 工作线程在完成端口上执行并处理投递的I/O请求。关于工作线程的数量,要注意的是,创建完成端口时指定的线程数量和这里要创建的线程数量不是一回事。CreateIoCompletionPort 函数的 NumberOfConcurrentThreads 参数明确告诉系统允许在完成端口上同时运行的线程数量。如果创建的线程数量多于 NumberOfConcurrentThreads,也仅有NumberOfConcurrentThreads 个线程允许运行。
但也存在确实需要创建更多线程的特殊情况,这主要取决于程序的总体设计。如果某个线程调用了一个函数,如 Sleep 或 WaitForSingleObject,进入了暂停状态,多出来的线程中就会有一个开始运行,占据休眠线程的位置。
有了足够的工作线程来处理完成端口上的 I/O 请求后,就该为完成端口关联套接字句柄了,这就用到了 CreateCompletionPort 函数的前3个参数。
FileHandle:要关联的套接字句柄
ExistingCompletionPort:要关联的完成端口对象句柄
CompletionKey:指定一个句柄唯一(per-handle)数据,它将与FileHandle套接字句柄关联在一起
3 完成端口和重叠I/O
向完成端口关联套接字句柄之后,便可以通过在套接字上投递重叠发送和接收请求处理 I/O。在这些 I/O 操作完成时,I/O 系统会向完成端口对象发送一个完成通知封包。I/O 完成端口以先进先出的方式为这些封包排队。工作线程调用 GetQueuedCompletionStatus 函数可以取得这些队列中的封包。函数定义如下:
BOOL GetQueuedCompletionStatus([in] HANDLE CompletionPort,LPDWORD lpNumberOfBytesTransferred,[out] PULONG_PTR lpCompletionKey,[out] LPOVERLAPPED *lpOverlapped,[in] DWORD dwMilliseconds
);
参数说明
- CompletionPort:完成端口对象句柄
- lpNumberOfBytesTransferred:I/O操作期间传输的字节数
- lpCompletionKey:关联套接字时指定的句柄唯一数据
- lpOverlapped:投递 I/O 请求时使用的重叠对象地址,进一步得到 I/O 唯一(per-I/O)数据
lpCompletionKey 参数包含了我们称为 per-handle 的数据,该数据在套接字第一次关联到完成端口时传入,用于标识 I/O 事件是在哪个套接字句柄上发生的。可以给这个参数传递任何类型的数据。
lpOverlapped 参数指向一个 OVERLAPPED 结构,结构后面便是我们称为per-I/O的数据,这可以是工作线程处理完成封包时想要知道的任何信息。
per-handle数据和per-I/O数据结构类型示例
#define BUFFER_SIZE 1024
//per-handle 数据
typedef struct _PER_HANDLE_DATA
{SOCKET s; //对应的套接字句柄SOCKADDR_IN addr; //客户端地址信息
}PER_HANDLE_DATA,*PPER_HANDLE_DATA;
//per-I/O 数据
typedef struct _PER_IO_DATA
{OVERLAPPED ol; //重叠结构char buf[BUFFER_SIZE]; //数据缓冲区int nOperationType; //I/O操作类型
#define OP_READ 1
#define OP_WRITE 2
#define OP_ACCEPT 3
}PER_IO_DATA,*PPER_IO_DATA;
4 示例程序
主线程首先创建完成端口对象,创建工作线程处理完成端口对象中的事件;然后创建监听套接字,开始监听服务端口;循环处理到来的连接请求,该过程具体如下:
- 调用 accept 函数等待接受未决的连接请求
- 接受新连接后,创建 per-handle 数,并将其关联到完成端口对象
- 在新接受的套接字上投递一个接收请求,该I/O完成后,由工作线程负责处理
void main()
{int nPort = 4567;HANDLE hCompletion = ::CreateIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0, 0); //创建完成端口对象::CreateThread(NULL, 0, ServerThread, (LPVOID)hCompletion, 0, 0); //创建工作线程//创建监听套接字,绑定到本地地址,开始监听SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, 0);SOCKADDR_IN si;si.sin_family = AF_INET;si.sin_port = ::ntohs(nPort);si.sin_addr.S_un.S_addr = INADDR_ANY;::bind(sListen, (sockaddr*)&si, sizeof(si));::listen(sListen, 5);//循环处理到来的连接while (true) {//等待接受未决的连接请求SOCKADDR_IN saRemote;int nRemoteLen = sizeof(saRemote);SOCKET sNew = ::accept(sListen, (sockaddr*)&saRemote, &nRemoteLen);//接受到新连接之后,为它创建一个per-handle数据,并将它们关联到完成端口对象PPER_HANDLE_DATA pPerHandle = (PPER_HANDLE_DATA)::GlobalAlloc(GPTR, sizeof(PER_HANDLE_DATA));pPerHandle->s = sNew;memcpy(&pPerHandle->addr, &saRemote, nRemoteLen);::CreateIoCompletionPort((HANDLE)pPerHandle->s, hCompletion, (DWORD)pPerHandle, 0);//投递一个接收请求PPER_IO_DATA pPerIO = (PPER_IO_DATA)::GlobalAlloc(GPTR, sizeof(PER_IO_DATA));pPerIO->nOperationType = OP_READ;WSABUF buf;buf.buf = pPerIO->buf;buf.len = BUFFER_SIZE;DWORD dwRecv;DWORD dwFlags = 0;::WSARecv(pPerHandle->s, &buf, 1, &dwRecv, &dwFlags, &pPerIO->ol, NULL);}
}
I/O 工作线程循环调用 GetQueuedCompletionStatus 函数从 I/O 完成端口移除完成的 I/O 通知封包,解析并进行处理。
DWORD WINAPI ServerThread(LPVOID lpParam)
{ //得到完成端口对象句柄HANDLE hCompletion = (HANDLE)lpParam;DWORD dwTrans;PPER_HANDLE_DATA pPerHandle;PPER_IO_DATA pPerIO;while (true) {//在关联到此完成端口的所有套接字上等待I/O完成BOOL bOK = ::GetQueuedCompletionStatus(hCompletion, &dwTrans, (PULONG_PTR)&pPerHandle, (LPOVERLAPPED*)&pPerIO, WSA_INFINITE);if (!bOK) {//在此套接字上由错误发生::closesocket(pPerHandle->s);::GlobalFree(pPerHandle);::GlobalFree(pPerIO);continue;}if (dwTrans == 0 && (pPerIO->nOperationType == OP_READ || pPerIO->nOperationType == OP_WRITE)) {::closesocket(pPerHandle->s);::GlobalFree(pPerHandle);::GlobalFree(pPerIO);continue;}switch (pPerIO->nOperationType){ //通过per-IO数据中的nOperationType域查看有什么I/O请求完成了case OP_READ: //完成一个接收请求{pPerIO->buf[dwTrans] = '\0';cout << "接收到数据:" << pPerIO->buf << endl;cout << "共有" << dwTrans << "字符" << endl;//继续投递接收I/O请求WSABUF buf;buf.buf = pPerIO->buf;buf.len = BUFFER_SIZE;pPerIO->nOperationType = OP_READ;DWORD nFlags = 0;::WSARecv(pPerHandle->s, &buf, 1, &dwTrans, &nFlags, &pPerIO->ol, NULL);}break;case OP_WRITE: //本例中没有投递这些类型的I/O请求case OP_ACCEPT: break;}}return 0;
}
5 恰当地关闭IOCP
关闭 I/O 完成端口时,特别是有多个线程在socket上执行 I/O 时,要避免当重叠操作正在进行时释放它的 OVERLAPPED 结构。阻止该情况发生的最好方法是在每个 socket 上调用 closesocket 函数,确保所有未决的重叠 I/O 操作都会完成。
一旦所有socket关闭,就该终止完成端口上处理 I/O 事件的工作线程了。可以通过调用 PostQueuedCompletionStatus 函数发送特定的完成封包来实现。所有工作线程都终止之后,可以调用 CloseHandle 函数关闭完成端口。


















