I/O完成端口--IOCP

 
2009-04-14 18:30
   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发送的信息,而其他线程无法接收的局面。要解决这个问题,编程人员必须做好各个线程之间的同步工作。
最近有项目要做一个高性能网络服务器,决定下功夫搞定完成端口IOCP),最终花了一个星期终于把它弄清楚了,并用C++写了一个版本,效率很不错。 但,从项目的总体需求来考虑,最终决定上.net平台,因此又花了一天一夜弄出了一个C#版,在这与大家分享。 一些心得体会: 1、在C#中,不用去面对完成端口的操作系统内核对象,Microsoft已经为我们提供了SocketAsyncEventArgs类,它封装了IOCP的使用。请参考:http://msdn.microsoft.com/zh-cn/library/system.net.sockets.socketasynceventargs.aspx?cs-save-lang=1&cs-lang=cpp#code-snippet-1。 2、我的SocketAsyncEventArgsPool类使用List对象来存储对客户端来通信的SocketAsyncEventArgs对象,它相当于直接使用内核对象时的IoContext。我这样设计比用堆栈来实现的好处理是,我可以在SocketAsyncEventArgsPool池中找到任何一个与服务器连接的客户,主动向它发信息。而用堆栈来实现的话,要主动给客户发信息,则还要设计一个结构来存储已连接上服务器的客户。 3、对每一个客户端不管还发送还是接收,我使用同一个SocketAsyncEventArgs对象,对每一个客户端来说,通信是同步进行的,也就是说服务器高度保证同一个客户连接上要么在投递发送请求,并等待;或者是在投递接收请求,等待中。本例只做echo服务器,还未考虑由服务器主动向客户发送信息。 4、SocketAsyncEventArgs的UserToken被直接设定为被接受的客户端Socket。 5、没有使用BufferManager 类,因为我在初始化时给每一个SocketAsyncEventArgsPool中的对象分配一个缓冲区,发送时使用Arrary.Copy来进行字符拷贝,不去改变缓冲区的位置,只改变使用的长度,因此在下次投递接收请求时恢复缓冲区长度就可以了!如果要主动给客户发信息的话,可以new一个SocketAsyncEventArgs对象,或者在初始化中建立几个来专门用于主动发送信息,因为这种需求一般是进行信息群发,建立一个对象可以用于很多次信息发送,总体来看,这种花销不大,还减去了字符拷贝和消耗。 6、测试结果:(在我的笔记本上时行的,我的本本是T420 I7 8G内存) 100客户 100,000(十万次)不间断的发送接收数据(发送和接收之间没有Sleep,就一个一循环,不断的发送与接收) 耗时3004.6325 秒完成 总共 10,000,000 一千万次访问 平均每分完成 199,691.6 次发送与接收 平均每秒完成 3,328.2 次发送与接收 整个运行过程中,内存消耗在开始两三分种后就保持稳定不再增涨。 看了一下对每个客户端的延迟最多不超过2秒。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值