Winsock完成端口

WINDOWS下进行网络服务端程序开发,毫无疑问,Winsock完成端口模型是最高效的。Winsock的完成端口模型借助Widnows的重叠IO和完成端口来实现,完成端口模型懂了之后是比较简单的,但是要想掌握Winsock完成端口模型,需要对WINDOWS下的线程、线程同步,WinsockAPI以及WINDOWSIO机制有一定的了解。如果不了解,推荐几本书:《InsideWindows2000,《WINDOWS核心编程》,《WIN32多线程程序设计》、《WINDOWS网络编程技术》。在去年,我在C语言下用完成端口模型写了一个WEBSERVER,前些天,我决定用C++重写这个WEBSERVER,给这个WEBSERVER增加了一些功能,并改进完成端口操作方法,比如采用AcceptEx来代替accept和使用LOOKASIDELIST来管理内存,使得WEBSERVER的性能有了比较大的提高。

一:完成端口模型
至于完成端口和Winsock完成端口模型的详细介绍,请参见我上面介绍的那几本书,这里只是我个人对完成端口模型理解的一点心得。
首先我们要抽象出一个完成端口大概的处理流程:
1
:创建一个完成端口。
2
:创建一个线程A
3
A线程循环调用GetQueuedCompletionStatus()函数来得到IO操作结果,这个函数是个阻塞函数。
4
:主线程循环里调用accept等待客户端连接上来。
5
:主线程里accept返回新连接建立以后,把这个新的套接字句柄用CreateIoCompletionPort关联到完成端口,然后发出一个异步的WSASend或者WSARecv调用,因为是异步函数,WSASend/WSARecv会马上返回,实际的发送或者接收数据的操作由WINDOWS系统去做。
6
:主线程继续下一次循环,阻塞在accept这里等待客户端连接。
7
WINDOWS系统完成WSASend或者WSArecv的操作,把结果发到完成端口。
8
A线程里的GetQueuedCompletionStatus()马上返回,并从完成端口取得刚完成的WSASend/WSARecv的结果。
9
:在A线程里对这些数据进行处理(如果处理过程很耗时,需要新开线程处理),然后接着发出WSASend/WSARecv,并继续下一次循环阻塞在GetQueuedCompletionStatus()这里。
具体的流程请看附图,其中红线表示是WINDOWS系统进行的处理,不需要我们程序干预。

归根到底概括完成端口模型一句话:
我们不停地发出异步的WSASend/WSARecvIO操作,具体的IO处理过程由WINDOWS系统完成,WINDOWS系统完成实际的IO处理后,把结果送到完成端口上(如果有多个IO都完成了,那么就在完成端口那里排成一个队列)。我们在另外一个线程里从完成端口不断地取出IO操作结果,然后根据需要再发出WSASend/WSARecvIO操作。

二:提高完成端口效率的几种有效方法
1
:使用AcceptEx代替acceptAcceptEx函数是微软的Winsosk扩展函数,这个函数和accept的区别就是:accept是阻塞的,一直要到有客户端连接上来后accept才返回,而AcceptEx是异步的,直接就返回了,所以我们利用AcceptEx可以发出多个AcceptEx调用
等待客户端连接。另外,如果我们可以预见到客户端一连接上来后就会发送数据(比如WEBSERVER的客户端浏览器),那么可以随着AcceptEx投递一个BUFFER进去,这样连接一建立成功,就可以接收客户端发出的数据到BUFFER里,这样使用的话,一次AcceptEx调用相当于accpetrecv的一次连续调用。同时,微软的几个扩展函数针对操作系统优化过,效率优于WINSOCK的标准API函数。

2
:在套接字上使用SO_RCVBUFSO_SNDBUF选项来关闭系统缓冲区。这个方法见仁见智,详细的介绍可以参考《WINDOWS核心编程》第9章。这里不做详细介绍,我封装的类中也没有使用这个方法。

3
:内存分配方法。因为每次为一个新建立的套接字都要动态分配一个IO数据单句柄数据的数据结构,然后在套接字关闭的时候释放,这样如果有成千上万个客户频繁连接时候,会使得程序很多开销花费在内存分配和释放上。这里我们可以使用lookasidelist。开始在微软的platformsdk里的SAMPLE里看到lookasidelist,我一点不明白,MSDN里有没有。后来还是在DDK的文档中找到了,,

lookasidelist
Asystem-managedqueuefromwhichentriesofafixedsizecanbeallocatedandintowhichentriescanbedeallocateddynamically.CallersoftheEx(ecutive)Supportlookasidelistroutinescanusealookasidelisttomanageanydynamicallysizedsetoffixed-sizebuffersorstructureswithcaller-determinedcontents.

Forexample,theI/OManagerusesalookasideforfastallocationanddeallocationofIRPsandMDLs.Asanotherexample,someofthesystem-suppliedSCSIclassdriversuselookasideliststoallocateandreleasememoryforSRBs.

lookasidelist
名字比较古怪(也许是我孤陋寡闻,第一次看到),其实就是一种内存管理方法,和内存池使用方法类似。我个人的理解:就是一个单链表。每次要分配内存前,先查看这个链表是否为空,如果不为空,就从这个链表中解下一个结点,则不需要新分配。如果为空,再动态分配。使用完成后,把这个数据结构不释放,而是把它插入到链表中去,以便下一次使用。这样相比效率就高了很多。在我的程序中,我就使用了这种单链表来管理。

在我们使用AcceptEx并随着AcceptEx投递一个BUFFER后会带来一个副作用:比如某个客户端只执行一个connect操作,并不执行send操作,那么AcceptEx这个请求不会完成,相应的,我们用GetQueuedCompletionStatus在完成端口中得不到操作结果,这样,如果有很多个这样的连接,对程序性能会造成巨大的影响,我们需要用一种方法来定时检测,当某个连接已经建立并且连接时间超过我们规定的时间而且没有收发过数据,那么我们就把它关闭。检测连接时间可以用SO_CONNECT_TIME来调用getsockopt得到。
还有一个值得注意的地方:就是我们不能一下子发出很多AcceptEx调用等待客户连接,这样对程序的性能有影响,同时,在我们发出的AcceptEx调用耗尽的时候需要新增加AcceptEx调用,我们可以把FD_ACCEPT事件和一个EVENT关联起来,然后
WaitForSingleObject等待这个Event,当已经发出AccpetEx调用数目耗尽而又有新的客户端需要连接上来,FD_ACCEPT事件将被触发,EVENT变为已传信状态,
WaitForSingleObject
返回,我们就重新发出足够的AcceptEx调用。
关于完成端口模型就介绍到这里。下面介绍我封装的类,这个类写完后,我用这个类做了个ECHOSERVER

你可能感兴趣的:(多线程,数据结构,编程,windows,浏览器)