1、一个对 Winsock 完成端口模型封装的类在 Windows 下进行网络服务端程序开发,毫无疑问,Winsock 完成端口模型是最高效的。Winsock 的完成端口模型借助 Widnows 的重叠 IO 和完成端口来实现,完成端口模型懂了之后是比较简单的,但是要想掌握 Winsock 完成端口模型,需要对 WINDOWS下的线程、线程同步,Winsock API 以及 WINDOWS IO 机制有一定的了解。如果不了解,推荐几本书:Inside Windows 2000, WINDOWS 核心编程 , WIN32 多线程程序设计 、 WINDOWS 网络编程技术 。在去年,我在 C 语言下用
2、完成端口模型写了一个 WebSERVER,前些天,我决定用 C+重写这个 WEBSERVER,给这个WEBSERVER 增加了一些功能,并改进完成端口操作方法,比如采用 AcceptEx 来代替accept 和使用 LOOKASIDE LIST 来管理内存,使得 WEBSERVER 的性能有了比较大的提高。在重写的开始,我决定把完成端口模型封装成一个比较通用的 C+类,针对各种网络服务端程序的开发,只要简单地继承这个类,改写其中两个虚拟函数就能满足各种需要。到昨天为止,WEBSERVER 重写完毕,我就写了这篇文章对完成端口模型做一个总结,并介绍一下我的这个类。一:完成端口模型至于完成端口和
3、Winsock 完成端口模型的详细介绍,请参见我上面介绍的那几本书,这里只是我个人对完成端口模型理解的一点心得。首先我们要抽象出一个完成端口大概的处理流程 :1:创建一个完成端口。2:创建一个线程 A。3:A 线程循环调用 GetQueuedCompletionStatus()函数来得到 IO 操作结果,这个函数是个阻塞函数。4:主线程循环里调用 accept 等待客户端连接上来。 5:主线程里 accept 返回新连接建立以后,把这个新的套接字句柄用CreateIoCompletionPort 关联到完成端口,然后发出一个异步的 WSASend 或者WSARecv 调用,因为是异步函数,WS
4、ASend/WSARecv 会马上返回,实际的发送或者接收数据的操作由 WINDOWS 系统去做。6:主线程继续下一次循环,阻塞在 accept 这里等待客户端连接。7:WINDOWS 系统完成 WSASend 或者 WSArecv 的操作,把结果发到完成端口。这里。具体的流程请看附图,其中红线表示是 Windows 系统进行的处理,不需要我们程序干预。 归根到底概括完成端口模型一句话:我们不停地发出异步的 WSASend/WSARecv IO 操作,具体的 IO 处理过程由 WINDOWS系统完成,WINDOWS 系统完成实际的 IO 处理后,把结果送到完成端口上(如果有多个IO 都完成了,
5、那么就在完成端口那里排成一个队列)。我们在另外一个线程里从完成端口不断地取出 IO 操作结果,然后根据需要再发出 WSASend/WSARecv IO 操作。二:提高完成端口效率的几种有效方法1:使用 AcceptEx 代替 accept。AcceptEx 函数是微软的 Winsosk 扩展函数,这个函数和 accept 的区别就是: accept 是阻塞的,一直要到有客户端连接上来后 accept 才返回,而 AcceptEx 是异步的,直接就返回了,所以我们利用 AcceptEx 可以发出多个AcceptEx 调用等待客户端连接。另外,如果我们可以预见到客户端一连接上来后就会发送数据(比如
6、 WebSERVER 的客户端浏览器) ,那么可以随着 AcceptEx 投递一个 BUFFER 进去,这样连接一建立成功,就可以接收客户端发出的数据到 BUFFER 里,这样使用的话,一次 AcceptEx 调用相当于 accpet 和 recv 的一次连续调用。同时,微软的几个扩展函数针对操作系统优化过,效率优于 WINSOCK 的标准 API 函数。2:在套接字上使用 SO_RCVBUF 和 SO_SNDBUF 选项来关闭系统缓冲区。这个方法见仁见智,详细的介绍可以参考Windows 核心编程第 9 章。这里不做详细介绍,我封装的类中也没有使用这个方法。3:内存分配方法。因为每次为一个新
7、建立的套接字都要动态分配一个“单 IO 数据”和“单句柄数据”的数据结构,然后在套接字关闭的时候释放,这样如果有成千上万个客户频繁连接时候,会使得程序很多开销花费在内存分配和释放上。这里我们可以使用 lookaside list。开始在微软的 platform sdk 里的 SAMPLE 里看到 lookaside list,我一点不明白,MSDN 里有没有。后来还是在 DDK 的文档中找到了, ,lookaside list A system-managed queue from which entries of a fixed size can be allocated and into
8、which entries can be deallocated dynamically. Callers of the Ex(ecutive) Support lookaside list routines can use a lookaside list to manage any dynamically sized set of fixed-size buffers or structures with caller-determined contents. For example, the I/O Manager uses a lookaside for fast allocation
9、 and deallocation of IRPs and MDLs. As another example, some of the system-supplied SCSI class drivers use lookaside lists to allocate and release memory for SRBs. lookaside list 名字比较古怪(也许是我孤陋寡闻,第一次看到) ,其实就是一种内存管理方法,和内存池使用方法类似。我个人的理解:就是一个单链表。每次要分配内存前,先查看这个链表是否为空,如果不为空,就从这个链表中解下一个结点,则不需要新分配。如果为空,再动态分
10、配。使用完成后,把这个数据结构不释放,而是把它插入到链表中去,以便下一次使用。这样相比效率就高了很多。在我的程序中,我就使用了这种单链表来管理。在我们使用 AcceptEx 并随着 AcceptEx 投递一个 BUFFER 后会带来一个副作用:比如某个客户端只执行一个 connect 操作,并不执行 send 操作,那么 AcceptEx 这个请求不会完成,相应的,我们用 GetQueuedCompletionStatus 在完成端口中得不到操作结果,这样,如果有很多个这样的连接,对程序性能会造成巨大的影响,我们需要用一种方法来定时检测,当某个连接已经建立并且连接时间超过我们规定的时间而且没有
11、收发过数据,那么我们就把它关闭。检测连接时间可以用 SO_CONNECT_TIME 来调用 getsockopt 得到。还有一个值得注意的地方:就是我们不能一下子发出很多 AcceptEx 调用等待客户连接,这样对程序的性能有影响,同时,在我们发出的 AcceptEx 调用耗尽的时候需要新增加 AcceptEx 调用,我们可以把 FD_ACCEPT 事件和一个 EVENT 关联起来,然后用 WaitForSingleObject 等待这个 Event,当已经发出 AccpetEx 调用数目耗尽而又有新的客户端需要连接上来,FD_ACCEPT 事件将被触发,EVENT 变为已传信状态,WaitF
12、orSingleObject 返回,我们就重新发出足够的 AcceptEx 调用。关于完成端口模型就介绍到这里。下面介绍我封装的类,这个类写完后,我用这个类做了个 ECHOSERVER。void main()CompletionPortModel p;p.Init();p.AllocEventMessage();if (FALSE = p.PostAcceptEx()return;p.ThreadLoop();return;我在我自己的机器上测试,客户端的代码是for (int i=0; i10000; i+)SOCKET s = socket(.);connect(.);send();rec
13、v(.)cout buffer endl;结果客户端程序在循环到 3000 多次的时候死掉,但是服务端程序运行良好,重新启动客户端程序,发送接收数据正常。使用的时候,只需要从这个类派生一个子类,并改写 HandleData 和 DataAction这两个虚函数,对于那些需要连续发送相关联的数据应用(比如传送文件),使用者需要自己扩展这两个函数,比如创建一个全局队列,每次从完成端口里得到数据后插入队列,然后用另外一个线程专门处理这个队列。从结果来看,这个类还有不少需要改进的地方,比如没考虑多处理器上运行的情况。没有考虑完成端口线程阻塞情况,如果考虑完成端口阻塞情况,那么应该创建 CPU数据*2
14、个完成端口线程等等,因为我同时正在做的毕业设计 NDIS 驱动防火墙开发正在一个比较难的地方卡住了,时间和精力有限,就没有对这个类进行进一步完善,程序中也许有不合理和错误的地方,请高手多多指教。对于高性能的服务端程序开发是比较难的,记得有次和腾讯一个技术人员聊天,他说,像腾讯 QQ 的开发,难点不在客户端,而在服务端各个服务器之间的通信和同步。服务端程程序的集群和负载平衡是一个很复杂的问题,我在这方面刚接触,希望能有更多的高手出来共享自己的经验。封装这个类的时候,我把最新的 PLATFORM SDK 里的例子看了一遍,借鉴了其中很多思路和方法,在此对写这个例子的微软程序员表示感谢:)类的详细代
15、码可以点这里下载DEMO 就是一个 ECHOSERVER,记得使用 Release 模式编译。 详细描述:用 Socket 接口实现网络异步通信VC + 4.1 以上版本的开发环境提供了 Socket 接口,可以方便地进行网络通信。本文是在一个应用程序中利用 Socket 接口实现异步通信,由于在异步通信状态下,服务端和客户端主控程序在等待信息时可以完成其他工作,因此具有更广阔的应用领域。1建立应用程序框架为了便于说明程序之间的相互关系,假设已用 VC +的 AppWizard 建立了单文档类型的应用程序框架,项目名为 Asock,它包括以下几个文件:AsockApp.h AsockApp.c
16、ppMainFraim.h MainFraim.cppChildView.h ChildView.cpp下面在上述程序框架的基础上编写一个能进行异步通信的应用程序。2流式套接字通信原理流式套接字因其可靠性高而得到广泛的应用。其通信原理为:服务端和客户端都必须建立通信套接字,而且服务端应先进入监听状态,然后客户端套接字发出连接请求,服务端收到请求后,建立另一个套接字进行通信,原来负责监听的套接字仍进行监听,如果有其他客户发来连接请求,则再建立一个套接字。默认状态下最多可同时接收 5 个客户的连接请求,并建立通信关系。3定义 MySocket 类本例中为了实现套接字的网络异步通信,通过异步套接字类
17、 CAsycnSocket 派生出两个新类。代码的生成可以利用 ClassWizard 来建立程序框架,给两个派生类取名为MySocket 和 ServeSocket,生成时使用的基类为 CAsycnSocket,并可将它们放在同一组文件中(本例是放在 MySocket.h 和 MySocket.cpp 中) 。接着在 ClassWizard 中为 MySocket类加入 OnAccept()和 OnReceive()两个函数;为 ServeSocket 类加入 OnReceive()函数。注意,这些函数都是重载函数,不能随便给其命名,加入函数的方法是:在 ClassWizard 的Objec
18、t Ids 窗口中选中最后一行,然后在 Message 窗口中选择相应的函数即可。两个派生类的功能是:MySocket 类用于在服务端和客户端建立套接字,分别用于监听和通信;ServeSocket 类用于在服务端建立通信套接字,它是在服务端监听到连接请求后才建立的,因此本例中将它作为 MySocket 类的成员变量。为了使服务端能响应多个客户的请求,可以建立 5 个 ServeSocket 类型的套接字,并设立一个记录器,记录已经收到的请求个数,该记录器在 MySocket 的构造函数中被置为 0。完成异步通信的关键在于上述三个重载函数,它们从网络中传来信息时,可以被自动调用,以完成接收工作。
19、在本例中,OnAccept()函数在收到连接请求后,会向客户发出一个代表其序号的信息;两个 OnReceive()函数都进行提示,并在确认后将收到的信息发送回去。上述两个派生类的源代码在网上,网址为 。4完成服务端或客户端的设置通过菜单项完成的设置工作可以有多种安排方法,本例为了便于显示,将设置工作安排在 CChildView 类中。首先用资源编辑器在主菜单中增加 Server 和 Client 两个选项,并定义它们的 ID,然后用 ClassWizard 在 CChildView 类中增加对这两个 ID 的响应函数,并在其中分别创建套接字后进入监听或开始连接。为了便于观察工作进程,可在其中增
20、加相应的输出语句,另外,在 ChildView.h 和 ChildView.cpp 文件前面必须有#include “mysocket.h“语句。为了简化程序,本程序直接写入服务主机的 IP 地址,因此,本程序在使用时,服务端是指定的,不能随便改变,但客户端的位置不受限制。5程序的使用本程序可以在同一网络中的不同主机之间进行异步通信。以两台主机为例,首先在指定的主机上启动本程序,并在菜单中选择 Server 选项,使程序进入监听状态;然后在另一主机上启动本程序并选择 Client 选项,向服务端发出连接请求;服务端收到连接请求后,自动调用 OnAccept()函数,根据客户端的请求顺序向其发出
21、相应信息;客户端接收到服务端发出的信息后,在屏幕上显示一个提示框,按下“确认”按钮后,客户端将此信息发回服务端;服务端收到客户端发回的信息后,处理方式与客户端相同,就是这样实现了这个信息在两台计算机之间的来回传递。值得注意的是,在等待信息期间,这个程序还可以做其他的工作,比如可以选择菜单上的某个选项等,当然也可以加入其他的工作。运行本程序并选择作为服务端时最多可以同时接收五个客户的请求,因此可以同时运行本程序的六个实例,其中一个设置为服务端,另外五个设置为客户端。由于服务端实际上是用五个套接字分别与客户端通信,因此点对点的通信过程将会互不干扰地进行。6源程序本文给出框架中被改动过的文件代码(即
22、 MySocket 和 ChildView 的.h 和.cpp 文件的源代码)如下,这些程序均在 VC + 6.0 下编译通过,并在本文作者单位的网络环境上运行成功。mysocket.h 文件:/派生套接字类class ServeSocket : public CAsyncSocketpublic:char rx_buf100;int ServeNo;public:ServeSocket();virtual ServeSocket();public:/ ClassWizard generated virtual function overrides/AFX_VIRTUAL(ServeSocke
23、t)public:virtual void OnReceive(int nErrorCode);/AFX_VIRTUALclass MySocket : public CAsyncSocketpublic:ServeSocket servesocket5;int AcceptNo;int ConnectNo;char rx_buf100;public:MySocket();virtual MySocket();public:/ ClassWizard generated virtual function overrides/AFX_VIRTUAL(MySocket)public:virtual void OnAccept(int nErrorCode);