四个棘手的IOCP编码问题和解决方法

使用IOCP时会出现一些问题,其中有一些不是很直观的。在使用IOCP的多线程编程中,一个线程函数的控制流程不是笔直的,因为在线程和通讯直接没有关系。在这一章节中,我们将描述四个不同的问题,可能在使用IOCP开发客户端/服务器应用程序时会出现,分别是:
The WSAENOBUFS error problem.(WSAENOBUFS错误问题) 
The package reordering problem.(包重构问题) 
The access violation problem.(访问非法问题)



3.6.1 WSAENOBUFS问题
这个问题通常很难靠直觉发现,因为当你第一次看见的时候你或许认为是一个内存泄露错误。假定已经开发完成了你的完成端口服务器并且运行的一切良好,但是当你对其进行压力测试的时候突然发现服务器被中止而不处理任何请求了,如果你运气好的话你会很快发现是因为WSAENOBUFS 错误而影响了这一切。 

每当我们重叠提交一个send或receive操作的时候,其中指定的发送或接收缓冲区就被锁定了。当内存缓冲区被锁定后,将不能从物理内存进行分页。操作系统有一个锁定最大数的限制,一旦超过这个锁定的限制,那么就会产生WSAENOBUFS 错误了。

如果一个服务器提交了非常多的重叠的receive在每一个连接上,那么限制会随着连接数的增长而变化。如果一个服务器能够预先估计可能会产生的最大并发连接数,服务器可以投递一个使用零缓冲区的receive在每一个连接上。因为当你提交操作没有缓冲区时,那么也不会存在内存被锁定了。使用这种办法后,当你的receive操作事件完成返回时,该socket底层缓冲区的数据会原封不动的还在其中而没有被读取到receive操作的缓冲区来。此时,服务器可以简单的调用非阻塞式的recv将存在socket缓冲区中的数据全部读出来,一直到recv返回 WSAEWOULDBLOCK 为止。 这种设计非常适合那些可以牺牲数据吞吐量而换取巨大 并发连接数的服务器。当然,你也需要意识到如何让客户端的行为尽量避免对服务器造成影响。在上一个例子中,当一个零缓冲区的receive操作被返回后使 用一个非阻塞的recv去读取socket缓冲区中的数据,如果服务器此时可预计到将会有爆发的数据流,那么可以考虑此时投递一个或者多个receive 来取代非阻塞的recv来进行数据接收。(这比你使用1个缺省的8K缓冲区来接收要好的多。)

源码中提供了一个简单实用的解决WSAENOBUF错误的办法。我们执行了一个零字节缓冲的异步WSARead(...)(参见 OnZeroByteRead(..))。当这个请求完成,我们知道在TCP/IP栈中有数据,然后我们通过执行几个有MAXIMUMPACKAGESIZE缓冲的异步WSARead(...)去读,解决了WSAENOBUFS问题。但是这种解决方法降低了服务器的吞吐量。

总结: 

解决方法一: 

投递使用空缓冲区的 receive操作,当操作返回后,使用非阻塞的recv来进行真实数据的读取。因此在完成端口的每一个连接中需要使用一个循环的操作来不断的来提交空缓冲区的receive操作。 

解决方法二: 

在投递几个普通含有缓冲区的receive操作后,进接着开始循环投递一个空缓冲区的receive操作。这样保证它们按照投递顺序依次返回,这样我们就总能对被锁定的内存进行解锁。 
3.6.2 包重构问题
... ... 尽管使用IO完成端口的待发操作将总是按照他们发送的顺序来完成,线程调度安排可能使绑定到完成端口的实际工作不按指定的顺序来处理。例如,如果你有两个I/O工作者线程,你可能接收到“字节块2,字节块1,字节块3”。这就意味着:当你通过向I/O完成端口提交请求数据发送数据时,数据实际上用重新排序过的顺序发送了。

这可以通过只使用一个工作者线程来解决,并只提交一个I/O请求,等待它完成。但是如果这么做,我们就失去了IOCP的长处。

解决这个问题的一个简单实用办法是给我们的缓冲类添加一个顺序数字,如果缓冲顺序数字是正确的,则处理缓冲中的数据。这意味着:有不正确的数字的缓冲将被存下来以后再用,并且因为执行原因,我们保存缓存到一个HASH MAP对象中(如m_SendBufferMap 和 m_ReadBufferMap)。

获取这种解决方法的更多信息,请查阅源码,仔细查看IOCPS类中如下的函数:

GetNextSendBuffer (..) and GetNextReadBuffer(..), to get the ordered send or receive buffer. 
IncreaseReadSequenceNumber(..) and IncreaseSendSequenceNumber(..), to increase the sequence numbers. 

3.6.3 异步等待读 和 字节块包处理问题

最通用的服务端协议是一个基于协议的包,首先X个字节代表包头,包头包含了详细的完整的包的长度。服务端可以读包头,计算出需要多少数据,继续读取直到读完一个完整的包。当服务端同时只处理一个异步请求时工作的很好。但是,如果我们想发挥IOCP服务端的全部潜能,我们应该启用几个等待的异步读事件,等待数据到达。这意味着几个异步读操作是不按顺序完成的,通过等待的读事件返回的字节块流将不会按顺序处理。而且,一个字节块流可以包含一个或几个包,也可能包含部分包,如下图所示:


这个图形显示了部分包(绿色)和完整包(黄色)是怎样在不同字节块流中异步到达的。
这意味着我们必须处理字节流来成功的读取一个完整的包。而且,我们必须处理部分包(图表中绿色的部分)。这就使得字节流的处理更加困难。这个问题的完整解决方法在IOCPS类的ProcessPackage(…)函数中。

3.6.4 访问非法问题
这是一个较小的问题,代码设计导致的问题更胜于IOCP的特定问题。假设一个客户端连接已经关闭并且一个I/O请求返回一个错误标志,然后我们知道客户端已经关闭。在参数CompletionKey中,我们传递了一个指向结构ClientContext的指针,该结构中包含了客户端的特定数据。如果我们释放这个ClientContext结构占用的内存,并且同一个客户端处理的一些其它I/O请求返回了错误代码,我们通过转换参数CompletionKey为一个指向ClientContext结构的指针并试图访问或删除它,会发生什么呢?一个非法访问出现了!

这个问题的解决方法是添加一个数字到结构中,包含等待的I/O请求的数量(m_nNumberOfPendingIO),然后当我们知道没有等待的I/O请求时删除这个结构。这个功能通过函数EnterIoLoop(…) 和ReleaseClientContext(…)来实现。

你可能感兴趣的:(四个棘手的IOCP编码问题和解决方法)