IOCP--Windows服务器编程

                                                                             一

对于IOCP,搞Windows服务器编程的都不会陌生,它所表现出来的性能是其他各种WinSock模型难望其项背的。撰写本文的目的就是为让大家能够在深入理解IOCP的基础上,再来深入的挖掘Windows系统的性能。此处假设读者对IOCP模型已经深刻理解,并对Windows线程、线程池有一定的了解。如果对此还不熟悉,限于篇幅的原因,请您先学习理解这些内容后再来阅读本文。
       在IOCP模型编程中,我们经常需要考虑的就是创建多少个线程来作为完成执行线程,很多时候这是个非常需要技巧和经验的决策性问题。大多数情况下,我们采取的策略是看服务器上有多少CPU然后假定每个CPU最多执行两个线程,然后我们创建的线程数量就是CPU数*2。这看起来很合理,但是实际上在复杂的服务器应用环境中,这样做的效果并不尽如人意,很多时候我们希望得到一种更加动态灵活的方案。
       有些有经验的程序员就自己编写线程池库,来实现这种动态灵活的管理方式,从而还可以实现一定的扩展性,比如系统动态的添加了一些CPU的资源,或者系统负担比较重的时候,或者CPU因为频繁切换线程场景而导致效率低下时,线程池的动态性就发挥出来了。

       索性的是,在Windows2000以上的平台上,已经为我们提供了线程池的接口,虽然这些接口有时候看起来还有些简陋,比如有名的QueueUserWorkItem函数,这些接口简陋到你连当前线程池中有多少活动线程等信息都无法知道,你只能通过其它的工具来动态观察和猜测。但这样的简单性也为我们带来了调用方便的实惠。当然到了Windows2008以上的平台时,线程池的函数总算是被大大加强了,你可以控制更多东西了,关于Windows2008线程池的内容请看我的另一篇博客拙作《Windows2008线程池前瞻》。
       在结合IOCP和线程池这方面Windows系统也想到了程序员面临的这个困难,Windows系统干脆直接就在系统内部捆绑了IOCP和线程池,提供了一个带IOCP功能的线程池函数——BindIoCompletionCallback。
此函数的原形如下:
BOOL WINAPI BindIoCompletionCallback(
  __in          HANDLE FileHandle,
  __in          LPOVERLAPPED_COMPLETION_ROUTINE Function,
  ULONG Flags
);

需要的完成过程(实际也就是IOCP线程的过程)Function的原形需要你定义成如下的样子:
VOID CALLBACK FileIOCompletionRoutine(
  [in]                 DWORD dwErrorCode,
  [in]                 DWORD dwNumberOfBytesTransfered,
  [in]                 LPOVERLAPPED lpOverlapped
);

        熟悉IOCP的各位可能已经兴奋得血管暴胀了吧?
       从BindIoCompletionCallback函数的参数你应该已经能够猜到这个函数的用法了,第一个句柄就是你需要捆绑的文件句柄或者SOCKET套接字句柄,甚至是其他I/O设备的句柄。第二个函数的指针就是你的完成例程的指针,这个函数完全由你实现和控制,最后一个Flags参数当前所有的Windows系统中都必须赋予0值,这个参数实际上还没有被起用。
       这么简单?真是难以置信,代表IOCP的句柄上哪去了?其实哪个什么IOCP的句柄,以及创建多少个线程什么的都不需要你考虑了,你唯一需要操心的就是如何编写完成例程以及如何将一个I/O句柄和完成例程捆绑起来。以前需要n多行代码才能完成的事情,一个BindIoCompletionCallback函数就彻底搞定了,甚至我们不需要再考虑线程的动态性问题了。这一切现在都有Windows系统综合考虑了,而你就被解放出来了。

 

                                                                    二 关于Socket和IOCP的一些值得注意的地方

 

 

    IOCP是一整套高性能的IO操作异步模型,可以用在文件操作也可以用在网络SOCKET操作上面。当用在网络SOCKET上时,在服务器端主要配合AceeptEx WSASend WSASendto来使用,在客户机端主要配合ConnectEx WSARecv和WSARecvFrom来使用。这几天用IOCP模型模仿IPMSG软件时有一些感触,分享如下:(这里没有具体的使用常识,这部分请参考《Windows网络编程2nd》或者相关网路资料)

 

一、单句柄数据和单IO数据

      这部分的术语不是很明白如何而来,只是根据Windows网络编程一书的中文翻译而来。

      单句柄数据是跟随你丢给IOCP的相关句柄的,而IO数据则是根据你每次IO操作时丢给相关API函数的OVERLAPPED参数的指针。具体来说,如果你要把某个句柄上的操作用IOCP来完成,那么你会调用一次(注意,仅需一次,以前我会在每次IO操作时丢调用,这是错误的示范!)CreateIoCompletionPort时把他的指针赋值给CompletionKey这个参数,而这块堆上内存将会跟随你的句柄直到句柄被Close,而且中间不允许更换,所以说单句柄数据应该而且必须是与你的IO句柄相关的数据比如说socket跟状态等等。

      而单IO数据是在调用WSARecv等等的API函数时的OVERLAPPED参数指向的堆上内存,这部分的数据结构最简单的做法是把OVERLAPPED作为数据结构的第一个字段,然后后面跟上跟此次IO操作相关的一些数据,比如说指向缓冲区的指针和表明缓冲区长度的DWORD值等等。这部分的数据只跟每次调用API函数进行的IO操作相关。

二、AcceptEx函数

      我在这个函数上卡壳了很长时间,他第三个函数表示一个完成AcceptEx操作后用来接收数据的一个缓冲,第四个参数表示一个缓冲的大小,然后四个函数分别表示本地、远程地址结构的长度。如果你只想做Accept操作而不想在这里做接收数据的动作那么把第四个参数设为0即可。但是容易在这里犯错的是,如果你认为既然不要接收数据那么把第三个参数设定为NULL那么这次投递永远不可能完成,并且所有的返回值WSAGetLastError都会看上去非常正确,这很不幸。即使你不想接收任何数据你也不能把表示缓冲区的参数设为0,而要至少设置一个长度为两个地址结构长度加上32的长度才行,如果不到那个长度那么等着在delete的时候报运行时错误吧!后面两个表示地址结构长度的参数都必须设置成地址结构长度加上16字节。如果你打算从缓冲里取出那两个地址结构,那么切记在每个地址结构后面都有16字节的数据块,这两块数据到底是什么我也不知道,也没有任何资料给我解释包括MSDN,相当崩溃!

三、ConnectEx函数

      基本上这个函数至少从表面上没有AcceptEx函数那些龟毛和诡异的东西,但是你认为这跟WSARecv之类API一样直接简单你就又错了。你会发现按照普通的方法调用以后调用WSAGetLastError返回的是10022错误,而不是WSA_IO_PENDING,又崩溃了吧?还好,这次MSDN给了你一小行解释,说The parameter s is an unbound or a listening socket,还是诡异两个字connect操作干嘛要绑定?不知道,没人给解释,那绑定就对了,那么绑哪个?最好把你的地址结构像下面这样设置

SOCKADDR_IN temp;

temp.sin_family = AF_INET;

temp.sin_port = htons(0);

temp.sin_addr.s_addr = htonl(ADDR_ANY);

为什么端口这个地方用0,原因很简单,你去查查MSDN,这样表示他会在1000-4000这个范围(可能记错,想了解的话去查MSDN)找一个没被用到的port,这样的话最大程度保证你bind的成功,然后再把socket句柄丢给IOCP,然后调用ConnectEx这样就会看到熟悉的WSA_IO_PENDING了!

四、WSARecvFrom和WSASendTo

      这两个函数没什么诡异的地方,只有一个细节,由于这两个函数都是在UDP里用,所以有个地址结构参数,WSARecvFrom的地址结构API会自己抓取可以在堆栈上分配,而WSASendTo的地址结构API不会自己抓取所以需要你用new在堆上分配,在完成以后再delete掉。

      另外还有就是基于UDP的IOCP在WIN2K上可能有些问题,这个在google大神上很容易找到,比如说你打个WSARecvFrom就能在第一页看到,在WINXP上则没有什么问题。

 

      仔细玩了两天IOCP以后发现,细节很重要,无论是看书还是MSDN等等英文资料,不要错过任何一个单词,每错过一个单词就多一个可能让你在某个地方多调试一个小时甚至更多~

                                                             

     

 

 

                                                                          三IOCP使用时常见的几个错误

在使用IOCP时,最重要的几个API就是GetQueueCompeltionStatus、WSARecv、WSASend,数据的I/O及其完成状态通过这几个接口获取并进行后续处理。

GetQueueCompeltionStatus attempts to dequeue an I/O completion packet from the specified I/O completion port. If there is no completion packet queued, the function waits for a pending I/O operation associated with the completion port to complete.

						BOOL WINAPI GetQueuedCompletionStatus(
  __in   HANDLE CompletionPort,
  __out  LPDWORD lpNumberOfBytes,
  __out  PULONG_PTR lpCompletionKey,
  __out  LPOVERLAPPED *lpOverlapped,
  __in   DWORD dwMilliseconds
);
				

If the function dequeues a completion packet for a successful I/O operation from the completion port, the return value is nonzero. The function stores information in the variables pointed to by the lpNumberOfBytes, lpCompletionKey, and lpOverlapped parameters.

除了关心这个API的in & out(这是MSDN开头的几行就可以告诉我们的)之外,我们更加关心不同的return & out意味着什么,因为由于各种已知或未知的原因,我们的程序并不总是有正确的return & out。

If *lpOverlapped is NULL and the function does not dequeue a completion packet from the completion port, the return value is zero. The function does not store information in the variables pointed to by the lpNumberOfBytes and lpCompletionKey parameters. To get extended error information, call GetLastError. If the function did not dequeue a completion packet because the wait timed out, GetLastError returns WAIT_TIMEOUT.

假设我们指定dwMilliseconds为INFINITE。

这里常见的几个错误有:

WSA_OPERATION_ABORTED (995): Overlapped operation aborted.

由于线程退出或应用程序请求,已放弃I/O 操作。

MSDN: An overlapped operation was canceled due to the closure of the socket, or the execution of the SIO_FLUSH command in WSAIoctl. Note that this error is returned by the operating system, so the error number may change in future releases of Windows.

成因分析:这个错误一般是由于peer socket被closesocket或者WSACleanup关闭后,针对这些socket的pending overlapped I/O operation被中止。

解决方案:针对socket,一般应该先调用shutdown禁止I/O操作后再调用closesocket关闭。

严重程度轻微易处理

WSAENOTSOCK (10038): Socket operation on nonsocket.

MSDN: An operation was attempted on something that is not a socket. Either the socket handle parameter did not reference a valid socket, or for select, a member of an fd_set was not valid.

成因分析:在一个非套接字上尝试了一个操作。

使用closesocket关闭socket之后,针对该invalid socket的任何操作都会获得该错误。

解决方案:如果是多线程存在对同一socket的操作,要保证对socket的I/O操作逻辑上的顺序,做好socket的graceful disconnect。

严重程度轻微易处理

WSAECONNRESET (10054): Connection reset by peer.

远程主机强迫关闭了一个现有的连接。

MSDN: An existing connection was forcibly closed by the remote host. This normally results if the peer application on the remote host is suddenly stopped, the host is rebooted, the host or remote network interface is disabled, or the remote host uses a hard close (see setsockopt for more information on the SO_LINGER option on the remote socket). This error may also result if a connection was broken due to keep-alive activity detecting a failure while one or more operations are in progress. Operations that were in progress fail with WSAENETRESET. Subsequent operations fail with WSAECONNRESET.

成因分析:在使用WSAAccpet、WSARecv、WSASend等接口时,如果peer application突然中止(原因如上所述),往其对应的socket上投递的operations将会失败。

解决方案:如果是对方主机或程序意外中止,那就只有各安天命了。但如果这程序是你写的,而你只是hard close,那就由不得别人了。至少,你要知道这样的错误已经出现了,就不要再费劲的继续投递或等待了。

严重程度轻微易处理

WSAECONNREFUSED (10061): Connection refused.

由于目标机器积极拒绝,无法连接。

MSDN: No connection could be made because the target computer actively refused it. This usually results from trying to connect to a service that is inactive on the foreign host—that is, one with no server application running.

成因分析:在使用connect或WSAConnect时,服务器没有运行或者服务器的监听队列已满;在使用WSAAccept时,客户端的连接请求被condition function拒绝。

解决方案:Call connect or WSAConnect again for the same socket. 等待服务器开启、监听空闲或查看被拒绝的原因。是不是长的丑或者钱没给够,要不就是服务器拒绝接受天价薪酬自主创业去了?

严重程度轻微易处理

WSAENOBUFS (10055): No buffer space available.

由于系统缓冲区空间不足或列队已满,不能执行套接字上的操作。

MSDN: An operation on a socket could not be performed because the system lacked sufficient buffer space or because a queue was full.

成因分析:这个错误是我查看错误日志后,最在意的一个错误。因为服务器对于消息收发有明确限制,如果缓冲区不足应该早就处理了,不可能待到send/recv失败啊。而且这个错误在之前的版本中几乎没有出现过。这也是这篇文章的主要内容。像connect和accept因为缓冲区空间不足都可以理解,而且危险不高,但如果send/recv造成拥堵并恶性循环下去,麻烦就大了,至少说明之前的验证逻辑有疏漏。

WSASend失败的原因是:The Windows Sockets provider reports a buffer deadlock. 这里提到的是buffer deadlock,显然是由于多线程I/O投递不当引起的。

解决方案:在消息收发前,对最大挂起的消息总的数量和容量进行检验和控制。

严重程度严重

本文主要参考MSDN。

 

 

                                                                          四关于TCP丢包,断开的疑问

 

丢包:以前在局域网内做过这样的试验, A机向B机不断地发 4096Byte的TCP包, 每个包都有序号, 结果有部份包B机收不到
断开:直接拔网线(存在假连接),可能要十几分钟后才检测到

从TCP的机制来看,
TCP的下层会丢包,但经过TCP处理后,提交到应用层的包是正确无误的包
如果包无应答,会重发,理论上不可能出现丢包。

至于上面的丢包实验,那时没细究原因,有可能是没有检测 Send 成功(程序处理不过来的原故),但是否存在那种被路由器过滤掉而造成丢包,或其它原因造成TCP丢包的可能性?有待进一步验证&找资料

而断开,只能通过心跳包来解决了。

                                

你可能感兴趣的:(IOCP--Windows服务器编程)