IOCP从本质上来说仍然属于重叠I/O,它是到目前为止Win32平台下效率最高的多线程网络编程模型,适用于具有高扩展性的高性能网络服务器的设计和实现。但是需要注意的是,该模型仅能用于Windows NT、Windows 2000之后的操作系统。
基本函数
IOCP模型的核心概念是完成端口,它是一种非常复杂的内核对象。在用户进程将一个或者(通常)多个套接口(完成端口并不仅仅适用于套接口编程,它事实上是Win32平台下的一种通用I/O机制,可与之绑定的设备有文件、套接口、邮槽、管道等)与之绑定(Associate)后,当这些套接口上有重叠I/O操作请求完成时,系统就会自动将完成信息报放入一个FIFO类型的I/O完成队列(见图2)中以达到通知应用程序的目的,而用户进程通常需要创建多个工作线程来处理这些通知信息。
IOCP涉及到三个基本函数:CreateIoCompletionPort、GetQueuedCompletionStatus和PostQueuedCompletionStat
us。第一个函数用于完成端口的创建和完成端口与套接口的绑定,第二个用于从完成队列中获取完成信息,最后一个用于模拟I/O请求的完成。
(1)CreateIoCompletionPort函数定义如下:
HANDLE CreateIoCompletionPort(
HANDLE FileHandle, // handle to file
HANDLE ExitingCompletionPort, // handle to I/O completion port
ULONG_PTR CompletionKey, // completion key
DWORD NumberOfConcurrentThreads // number of threads to execute concurrently
);
需要特别注意的是,该函数不仅用于完成端口的创建,还用于完成端口和套接口的绑定。因此它的使用可分为两个阶段:
阶段1:完成端口的创建
我们只需要关心最后一个参数NumberOfConcurrentThread
s,其余三个参数可以依次置为INVALID_HANDLE_VALUE、NULL和0。NumberOfConcurrentThread
s定义了在该完成端口上可以同时工作的最大线程数,通常情况下该数目应与系统的处理器个数相同,通过将NumberOfConcurrentThread
s赋值为0系统能自动完成这样的设置。可能的完成端口创建代码如下:
HANDLE hIOCP = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
如果函数操作成功返回创建的IOCP句柄,否则返回NULL,这时可以用GetLastError函数来查询错误信息。
阶段2:完成端口与套接口的绑定
这时四个参数的意义分别是:FileHandle,指向需要进行重叠I/O操作的设备句柄,在这儿就是套接口;ExistingCompletionPort,指向刚创建的完成端口的句柄;CompletionKey,完成键,又称单句柄(套接口)数据,用于指定I/O操作涉及的设备的特定信息,工作线程调用GetQueuedCompletionStatu
s函数可以获得该数据;NumberOfConcurrentThread
s,设置为0即可。示例代码如下:
typedef struct _CPKey{
SOCKET svrSock;
}PER_SOCKET_DATA, *LPPER_SOCKET_DATA;
LPPER_SOCKET_DATA lpPerSocketData;
SOCKET svrSock = accept(sock, NULL, NULL);
CreateIoCompletionPort((HANDLE)svrSock, hIOCP, (DWORD)lpPerSocketData, 0);
为两个完全不同的功能提供同一个函数接口,非常容易引起混淆,因此程序设计人员通常用两个自定义函数来对CreateIoCompletionPort进行包装:
HANDLE CreateNewIoCompletionPor
t(DWORD dwNumberOfConcurrentThre
ads){
return (CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0,
dwNumberOfConcurrentThre
ads));
}
BOOL AssociateWithIoCompletio
nPort(HANDLE hComPort, HANDLE hDevice, DWORD dwCompKey){
return (CreateIoCompletionPort(hDevice, hComPort, dwCompKey, 0) == hComPort);
}
当然,这种复杂的设计也并非毫无意义,事实上可以用一次CreateIoCompletionPort函数调用就实现完成端口的创建和与一个设备相绑定的功能。下面的代码将一个服务器套接口与新创建的完成端口hIOCP绑定,并且设置完成端口允许最多两个线程同时工作:
SOCKET svrSock = accept(sock, NULL, NULL);
HANDLE hIOCP = CreateIoCompletionPort((HANDEL)svrSock, NULL, (DWORD)lpPerSockData, 2);
(2)GetQueuedCompletionStatu
s函数定义如下:
BOOL GetQueuedCompletionStatu
s(
HANDLE CompletionPort,
// handle to completion port
LPDWORD lpNumberOfBytes,
// bytes transfered
PULONG_PTR lpCompletionKey,
// file completion key
LPOVERLAPPED *lpOverlapped,
// buffer
DWORD dwMilliseconds
// optional timeout value
);
该函数用于获取指定I/O完成端口的完成信息。如果没有完成信息,那么函数将阻塞直到I/O操作完成或者超时。五个参数中第一个和最后一个是输入参数,其余都是输出参数。CompletionPort,用于指定我们关心的完成端口的句柄;lpNumberOfBytes,用于返回I/O操作所传输的字节数;lpCompletionKey,返回I/O操作涉及的单文件句柄数据;lpOverlapped,返回进行重叠I/O操作时指定的重叠结构,使用18.2.4节介绍的技术可以在lpOverlapped上追加任意数量的数据--称为单I/O操作数据;dwMilliSeconds,指定函数等待完成信息出现的时间,以毫秒为单位,如果设置为INFINITE表示无限等待,设置为0表示采取询问方式函数立即返回。
GetQueuedCompletionStatus函数返回值的情况有些复杂,正确完整的示例代码如下:
BOOL ret = GetQueuedCompletionStatu
s(hIOCP, &dwNumBytes, &CompKey, &pOverlapped, 1000);
if(ret){
// 非0返回值,成功
// dequeues a completion packet for a successful I/O operation
}
else{
// 返回0,失败
DWORD dwError = GetLastError();
if(pOverlapped != NULL){
// dequeues a completion packet for a failed I/O operation
// dwErrro contains the reason for failure
}
else{
// does not dequeue a completion packet
// dwError contains the reason for failure
if(dwError == WAIT_TIMEOUT){
// 超时
}
else{
// Bad call to GetQueuedCompletionStatu
s
// dwErrro contains the reason for the bad call
}
}
}
(3)PostQueuedCompletionStat
us函数定义如下:
BOOL PostQueuedCompletionStat
us(
HANDLE CompletionPort,
// handle to an I/O completion port
DWORD dwNumberOfBytesTransfere
d,
// bytes transfered
ULONG_PTR dwCompletionKey,
// completion key
LPOVERLAPPED lpOverlapped
// overlapped buffer
);
该函数用于向完成端口CompletionPort投递I/O完成信息包。三个参数dwNumberOfBytesTransfere
d、dwCompletionKey和lpOverlapped会在调用函数GetQueuedCompletionStatu
s时返回。
PostQueuedCompletionStat
us函数提供了与工作线程通信的手段。假设要终止应用进程,但是希望各个工作线程(假设有四个)能先进行相应的资源释放,那么PostQueuedCompletionStat
us就是一个很好的途径。我们可以简单地以特定参数(一般将完成键设置为NULL)调用四次PostQueuedCompletionStat
us,当工作线程GetQueuedCompletionStatu
s返回该参数,也就获知了关闭通知,可以进行相应的线程退出处理了。
但是需要注意的是,如果并不是通知线程退出,这是线程会再次调用GetQueuedCompletionStatu
s函数,那么由于等待线程队列是LIFO方式的,很可能造成同一个线程接收了所有的PostQueuedCompletionStat
us发送的信息,而其他线程无法接收的局面。要解决这个问题,编程人员必须做好各个线程之间的同步工作。