一般要开发网络应用程序并非一件轻松的事情,不过,实际上只要掌握几个关键的原则也就能够了——建立和链接一个套接字,尝试进行链接,而后收发数 据。真正难的是要写出一个能够接纳少则一个,多则数千个链接的网络应用程序。本文将讨论如何经过Winsock2在Windows NT 和 Windows 2000上开发高扩展能力的Winsock应用程序。文章主要的焦点在客户机/服务器模型的服务器这一方,固然,其中的许多要点对模型的双方都适用。程序员
经过Win32的重叠I/O机制,应用程序能够提请一项I/O操做,重叠的操做请求在后台完成,而同一时间提请操做的线程去作其余的事情。等重叠操做完成后线程收到有关的通知。这种机制对那些耗时的操做而言特别有用。不过,像Windows 3.1上的WSAAsyncSelect()及Unix下的select()那样的函数虽然易于使用,可是它们不能知足响应规模的须要。而完成端口机制是针对操做系统内部进行了优化,在Windows NT 和 Windows 2000上,使用了完成端口的重叠I/O机制才可以真正扩大系统的响应规模。编程
一个完成端口其实就是一个通知队列,由操做系统把已经完成的重叠I/O请求的通知放入其中。当某项I/O操做一旦完成,某个能够对该操做结果进行处理的工做者线程就会收到一则通知。而套接字在被建立后,能够在任什么时候候与某个完成端口进行关联。数组
一般状况下,咱们会在应用程序中建立必定数量的工做者线程来处理这些通知。线程数量取决于应用程序的特定须要。理想的状况是,线程数量等于处理器的数量,不 过这也要求任何线程都不该该执行诸如同步读写、等待事件通知等阻塞型的操做,以避免线程阻塞。每一个线程都将分到必定的CPU时间,在此期间该线程能够运行, 而后另外一个线程将分到一个时间片并开始执行。若是某个线程执行了阻塞型的操做,操做系统将剥夺其未使用的剩余时间片并让其它线程开始执行。也就是说,前一 个线程没有充分使用其时间片,当发生这样的状况时,应用程序应该准备其它线程来充分利用这些时间片。缓存
完成端口的使用分为两步。首先建立完成端口,如如下代码所示:服务器
HANDLE hIocp; hIocp = CreateIoCompletionPort( INVALID_HANDLE_VALUE, NULL, (ULONG_PTR)0, 0); if (hIocp == NULL) { // Error }
完成端口建立后,要把将使用该完成端口的套接字与之关联起来。方法是再次调用CreateIoCompletionPort ()函数,第一个参数FileHandle设为套接字的句柄,第二个参数ExistingCompletionPort 设为刚刚建立的那个完成端口的句柄。网络
如下代码建立了一个套接字,并把它和前面建立的完成端口关联起来:数据结构
SOCKET s; s = socket(AF_INET, SOCK_STREAM, 0); if (s == INVALID_SOCKET) { // Error if (CreateIoCompletionPort((HANDLE)s, hIocp, (ULONG_PTR)0, 0) == NULL) { // Error } ... }
这时就完成了套接字与完成端口的关联操做。在这个套接字上进行的任何重叠操做都将经过完成端口发出完成通知。注意,CreateIoCompletionPort()函数中的第三个参数用来设置一个与该套接字相关的“完成键(completion key)”(译者注:完成键能够是任何数据类型)。每当完成通知到来时,应用程序能够读取相应的完成键,所以,完成键可用来给套接字传递一些背景信息。架构
在建立了完成端口、将一个或多个套接字与之相关联以后,咱们就要建立若干个线程来处理完成通知。这些线程不断循环调用GetQueuedCompletionStatus ()函数并返回完成通知。并发
下面,咱们先来看看应用程序如何跟踪这些重叠操做。当应用程序调用一个重叠操做函数时,要把指向一个overlapped结构的指针包括在其参数中。当操做 完成后,咱们能够经过GetQueuedCompletionStatus()函数中拿回这个指针。不过,单是根据这个指针所指向的overlapped 结构,应用程序并不能分辨究竟完成的是哪一个操做。要实现对操做的跟踪,你能够本身定义一个OVERLAPPED结构,在其中加入所需的跟踪信息。app
不管什么时候调用重叠操做函数时,老是会经过其lpOverlapped参数传递一个OVERLAPPEDPLUS结构(例如WSASend、 WSARecv等函数)。这就容许你为每个重叠调用操做设置某些操做状态信息,当操做结束后,你能够经过 GetQueuedCompletionStatus()函数得到你自定义结构的指针。注意OVERLAPPED字段不要求必定是这个扩展后的结构的第一 个字段。当获得了指向OVERLAPPED结构的指针之后,能够用CONTAINING_RECORD宏取出其中指向扩展结构的指针。
OVERLAPPED 结构的定义以下:
typedef struct _OVERLAPPEDPLUS { OVERLAPPED ol; SOCKET s, sclient; int OpCode; WSABUF wbuf; DWORD dwBytes, dwFlags; // 其它有用的信息 } OVERLAPPEDPLUS; #define OP_READ 0 #define OP_WRITE 1 #define OP_ACCEPT 2
下面让咱们来看看工做者线程的状况。工做线程WorkerThread代码:
DWORD WINAPI WorkerThread(LPVOID lpParam) { ULONG_PTR *PerHandleKey; OVERLAPPED *Overlap; OVERLAPPEDPLUS *OverlapPlus, *newolp; DWORD dwBytesXfered; while (1) { ret = GetQueuedCompletionStatus( hIocp, &dwBytesXfered, (PULONG_PTR)&PerHandleKey, &Overlap, INFINITE); if (ret == 0) { // Operation failed continue; } OverlapPlus = CONTAINING_RECORD(Overlap, OVERLAPPEDPLUS, ol); switch (OverlapPlus->OpCode) { case OP_ACCEPT: // Client socket is contained in OverlapPlus.sclient // Add client to completion port CreateIoCompletionPort( (HANDLE)OverlapPlus->sclient, hIocp, (ULONG_PTR)0, 0); // Need a new OVERLAPPEDPLUS structure // for the newly accepted socket. Perhaps // keep a look aside list of free structures. newolp = AllocateOverlappedPlus(); if (!newolp) { // Error } newolp->s = OverlapPlus->sclient; newolp->OpCode = OP_READ; // This function prepares the data to be sent PrepareSendBuffer(&newolp->wbuf); ret = WSASend( newolp->s, &newolp->wbuf, 1, &newolp->dwBytes, 0, &newolp.ol, NULL); if (ret == SOCKET_ERROR) { if (WSAGetLastError() != WSA_IO_PENDING) { // Error } } // Put structure in look aside list for later use FreeOverlappedPlus(OverlapPlus); // Signal accept thread to issue another AcceptEx SetEvent(hAcceptThread); break; case OP_READ: // Process the data read // ... // Repost the read if necessary, reusing the same // receive buffer as before memset(&OverlapPlus->ol, 0, sizeof(OVERLAPPED)); ret = WSARecv( OverlapPlus->s, &OverlapPlus->wbuf, 1, &OverlapPlus->dwBytes, &OverlapPlus->dwFlags, &OverlapPlus->ol, NULL); if (ret == SOCKET_ERROR) { if (WSAGetLastError() != WSA_IO_PENDING) { // Error } } break; case OP_WRITE: // Process the data sent, etc. break; } // switch } // while } // WorkerThread
其中每句柄键(PerHandleKey)变量的内容,是在把完成端口与套接字进行关联时所设置的完成键参数;Overlap参数返回的是一个指向发出重叠操做时所使用的那个OVERLAPPEDPLUS结构的指针。
要记住,若是重叠操做调用失败时(也就是说,返回值是SOCKET_ERROR,而且错误缘由不是WSA_IO_PENDING),那么完成端口将不会收到 任何完成通知。若是重叠操做调用成功,或者发生缘由是WSA_IO_PENDING的错误时,完成端口将老是可以收到完成通知。
对于开发大响应规模的Winsock应用程序而言,对Windows NT和Windows 2000的套接字架构有基本的了解是颇有帮助的。下图是Windows 2000中的Winsock架构:
与其它类型操做系统不一样,Windows NT和Windows 2000的传输协议没有一种风格像套接字那样的、能够和应用程序直接交谈的界面,而是采用了一种更为底层的API,叫作传输驱动程序界面(Transport Driver Interface,TDI)。Winsock的核心模式驱动程序负责链接和缓冲区管理,以便向应用程序提供套接字仿真(在AFD.SYS文件中实现),同时负责与底层传输驱动程序对话。
正如上面所说的,应用程序经过Winsock来和传输协议驱动程序交谈,而AFD.SYS负责为应用程序进行缓冲区管理。也就是说,当应用程序调用 send()或WSASend()函数来发送数据时,AFD.SYS将把数据拷贝进它本身的内部缓冲区(取决于SO_SNDBUF设定值),而后 send()或WSASend()函数当即返回。也能够这么说,AFD.SYS在后台负责把数据发送出去。不过,若是应用程序要求发出的数据超过了 SO_SNDBUF设定的缓冲区大小,那么WSASend()函数会阻塞,直至全部数据发送完毕。
从远程客户端接收数据的状况也相似。只要不用从应用程序那里接收大量的数据,并且没有超出SO_RCVBUF设定的值,AFD.SYS将把数据先拷贝到其内部缓冲区中。当应用程序调用recv()或WSARecv()函数时,数据将从内部缓冲拷贝到应用程序提供的缓冲区。
多数状况下,这样的架构运行良好,特别在是应用程序采用传统的套接字下非重叠的send()和receive()模式编写的时候。不过程序员要当心的是,尽 管能够经过setsockopt()这个API来把SO_SNDBUF和SO_RCVBUF选项值设成0(关闭内部缓冲区),可是程序员必须十分清楚把 AFD.SYS的内部缓冲区关掉会形成什么后果,避免收发数据时有关的缓冲区拷贝可能引发的系统崩溃。
举例来讲,一个应用 程序经过设定SO_SNDBUF为0把缓冲区关闭,而后发出一个阻塞send()调用。在这样的状况下,系统内核会把应用程序的缓冲区锁定,直到接收方确 认收到了整个缓冲区后send()调用才返回。彷佛这是一种断定你的数据是否已经为对方所有收到的简洁的方法,实际上却并不是如此。想一想看,即便远端TCP 通知数据已经收到,其实也根本不表明数据已经成功送给客户端应用程序,好比对方可能发生资源不足的状况,致使AFD.SYS不能把数据拷贝给应用程序。另 一个更要紧的问题是,在每一个线程中每次只能进行一次发送调用,效率极其低下。
把SO_RCVBUF设为0,关闭AFD.SYS的接收缓冲区也不能让性能获得提高,这只会迫使接收到的数据在比Winsock更低的层次进行缓冲,当你发出receive调用时,一样要进行缓冲区拷贝,所以你原本想避免缓冲区拷贝的阴谋不会得逞。
如今咱们应该清楚了,关闭缓冲区对于多数应用程序而言并非什么好主意。只要要应用程序注意随时在某个链接上保持几个WSARecvs重叠调用,那么一般没有必要关闭接收缓冲区。若是AFD.SYS老是有由应用程序提供的缓冲区可用,那么它将没有必要使用内部缓冲区。
高性能的服务器应用程序能够关闭发送缓冲区,同时不会损失性能。不过,这样的应用程序必须十分当心,保证它老是发出多个重叠发送调用,而不是等待某个重叠发 送结束了才发出下一个。若是应用程序是按一个发完再发下一个的顺序来操做,那浪费掉两次发送中间的空档时间,总之是要保证传输驱动程序在发送完一个缓冲区 后,马上能够转向另外一个缓冲区。
在设计任何服务器应用程序时,其强健性是主要的目标。也就是说,你的应用程序要可以应对任何突发的问题,例如并发客户请求数达到峰值、可用内存临时出现不足、以及其它短期的现象。这就要求程序的设计者注意Windows NT和2000系统下的资源限制条件的问题,从容地处理突发性事件。
你能够直接控制的、最基本的资源就是网络带宽。一般,使用用户数据报协议(UDP)的应用程序均可能会比较注意带宽方面的限制,以最大限度地减小包的丢失。 然而,在使用TCP链接时,服务器必须十分当心地控制好,防止网络带宽过载超过必定的时间,不然将须要重发大量的包或形成大量链接中断。关于带宽管理的方 法应根据不一样的应用程序而定,这超出了本文讨论的范围。
虚拟内存的使用也必须很当心地管理。经过谨慎地申请和释放内存,或 者应用lookaside lists(一种高速缓存)技术来从新使用已分配的内存,将有助于控制服务器应用程序的内存开销(原文为“让服务器应用程序留下的脚印小一点”),避免操 做系统频繁地将应用程序申请的物理内存交换到虚拟内存中(原文为“让操做系统可以老是把更多的应用程序地址空间更多地保留在内存中”)。你也能够经过 SetWorkingSetSize()这个Win32 API让操做系统分配给你的应用程序更多的物理内存。
在使用Winsock时还可能碰到另外两个非直接 的资源不足状况。一个是被锁定的内存页面的极限。若是你把AFD.SYS的缓冲关闭,当应用程序收发数据时,应用程序缓冲区的全部页面将被锁定到物理内存 中。这是由于内核驱动程序须要访问这些内存,在此期间这些页面不能交换出去。若是操做系统须要给其它应用程序分配一些可分页的物理内存,而又没有足够的内 存时就会发生问题。咱们的目标是要防止写出一个病态的、锁定全部物理内存、让系统崩溃的程序。也就是说,你的程序锁定内存时,不要超出系统规定的内存分页极限。
在Windows NT和2000系统上,全部应用程序总共能够锁定的内存大约是物理内存的1/8(不过这只是一个大概的估计,不是你计算内存的依据)。若是你的应用程序不 注意这一点,当你的发出太多的重叠收发调用,并且I/O没来得及完成时,就可能偶尔发生ERROR_INSUFFICIENT_RESOURCES的错 误。在这种状况下你要避免过分锁定内存。同时要注意,系统会锁定包含你的缓冲区所在的整个内存页面,所以缓冲区靠近页边界时是有代价的(译者理解,缓冲区 若是正好超过页面边界,那怕是1个字节,超出的这个字节所在的页面也会被锁定)。
另一个限制是你的程序可能会遇到系统未分页池资源不足的状况。所谓未分页池是一块永远不被交换出去的内存区域,这块内存用来存储一些供各类内核组件访问的数据,其中有的内核组件是不能访问那些被交换出去的页面空间的。Windows NT和2000的驱动程序可以从这个特定的未分页池分配内存。
当 应用程序建立一个套接字(或者是相似的打开某个文件)时,内核会从未分页池中分配必定数量的内存,并且在绑定、链接套接字时,内核又会从未分页池中再分配 一些内存。当你注意观察这种行为时你将发现,若是你发出某些I/O请求时(例如收发数据),你会从未分页池里再分配多一些内存(好比要追踪某个待决的I /O操做,你可能须要给这个操做添加一个自定义结构,如前文所说起的)。最后这就可能会形成必定的问题,操做系统会限制未分页内存的用量。
在Windows NT和2000这两种操做系统上,给每一个链接分配的未分页内存的具体数量是不一样的,将来版本的Windows极可能也不一样。为了使应用程序的生命期更长,你就不该该计算对未分页池内存的具体需求量。
你的程序必须防止消耗到未分页池的极限。当系统中未分页池剩余空间过小时,某些与你的应用程序毫无关系的内核驱动就会发疯,甚至形成系统崩溃,特别是当系统 中有第三方设备或驱动程序时,更容易发生这样的惨剧(并且没法预测)。同时你还要记住,同一台电脑上还可能运行有其它一样消耗未分页池的其它应用程序,因 此在设计你的应用程序时,对资源量的预估要特别保守和谨慎。
处理资源不足的问题是十分复杂的,由于发生上述状况时你不会收到特别的错误代码,一般你只能收到通常性的WSAENOBUFS或者ERROR_INSUFFICIENT_RESOURCES 错误。要处理这些错误,首先,把你的应用程序工做配置调整到合理的最大值(译者注:所谓工做配置,是指应用程序各部分运行中所需的内存用量,请参考 http://msdn.microsoft.com/msdnmag/issues/1000/Bugslayer/Bugslayer1000.asp,关于内存优化,译者另有译文),若是错误继续出现,那么注意检查是不是网络带宽不足的问题。以后,请确认你没有同时发出太多的收发调用。最后,若是仍是 收到资源不足的错误,那就极可能是遇到了未分页内存池不足的问题了。要释放未分页内存池空间,请关闭应用程序中至关部分的链接,等待系统自行渡过和修正这 个瞬时的错误。
服务器要作的最普通的事情之一就是接受来自客户端的链接请求。在套接字上使用重叠I/O接受链接的唯一API就是AcceptEx()函数。有趣的是,通 常的同步接受函数accept()的返回值是一个新的套接字,而AcceptEx()函数则须要另一个套接字做为它的参数之一。这是由于 AcceptEx()是一个重叠操做,因此你须要事先建立一个套接字(但不要绑定或链接它),并把这个套接字经过参数传给AcceptEx()。如下是一 小段典型的使用AcceptEx()的伪代码:
do { -等待上一个 AcceptEx 完成 -建立一个新套接字并与完成端口进行关联 -设置背景结构等等 -发出一个 AcceptEx 请求 }while(TRUE);
做为一个高响应能力的服务器,它必须发出足够的AcceptEx调用,守候着,一旦出现客户端链接请求就马上响应。至于发出多少个AcceptEx才够, 就取决于你的服务器程序所期待的通讯交通类型。好比,若是进入链接率高的状况(由于链接持续时间较短,或者出现交通高峰),那么所须要守候的 AcceptEx固然要比那些偶尔进入的客户端链接的状况要多。聪明的作法是,由应用程序来分析交通情况,并调整AcceptEx守候的数量,而不是固定在某个数量上。
对于Windows2000,Winsock提供了一些机制,帮助你断定AcceptEx的数量是否足够。这就是,在建立监听套接 字时建立一个事件,经过WSAEventSelect()这个API并注册FD_ACCEPT事件通知来把套接字和这个事件关联起来。一旦系统收到一个连 接请求,若是系统中没有AcceptEx()正在等待接受链接,那么上面的事件将收到一个信号。经过这个事件,你就能够判断你有没有发出足够的 AcceptEx(),或者检测出一个非正常的客户请求(下文述)。这种机制对Windows NT 4.0不适用。
使用AcceptEx()的一大好处是,你能够经过一次调用就完成接受客户端链接 请求和接受数据(经过传送lpOutputBuffer参数)两件事情。也就是说,若是客户端在发出链接的同时传输数据,你的AcceptEx()调用在 链接建立并接收了客户端数据后就能够马上返回。这样多是颇有用的,可是也可能会引起问题,由于AcceptEx()必须等所有客户端数据都收到了才返 回。具体来讲,若是你在发出AcceptEx()调用的同时传递了lpOutputBuffer参数,那么AcceptEx()再也不是一项原子型的操做, 而是分红了两步:接受客户链接,等待接收数据。当缺乏一种机制来通知你的应用程序所发生的这种状况:“链接已经创建了,正在等待客户端数据”,这将意味着 有可能出现客户端只发出链接请求,可是不发送数据。若是你的服务器收到太多这种类型的链接时,它将拒绝链接更多的合法客户端请求。这就是黑客进行“拒绝服 务”攻击的常见手法。
要预防此类攻击,接受链接的线程应该不时地经过调用getsockopt()函数(选项参数为 SO_CONNECT_TIME)来检查AcceptEx()里守候的套接字。getsockopt()函数的选项值将被设置为套接字被链接的时间,或者 设置为-1(表明套接字还没有创建链接)。这时,WSAEventSelect()的特性就能够很好地利用来作这种检查。若是发现链接已经创建,可是好久都 没有收到数据的状况,那么就应该终止链接,方法就是关闭做为参数提供给AcceptEx()的那个套接字。注意,在多数非紧急状况下,若是套接字已经传递 给AcceptEx()并开始守候,但还未创建链接,那么你的应用程序不该该关闭它们。这是由于即便关闭了这些套接字,出于提升系统性能的考虑,在链接进 入以前,或者监听套接字自身被关闭以前,相应的内核模式的数据结构也不会被干净地清除。
发出AcceptEx()调用的线程,彷佛与那个进行完成端口关联操做、处理其它I/O完成通知的线程是同一个,可是,别忘记线程里应该尽力避免执行阻塞型的操做。Winsock2分层结构的一个反作用是调用socket()或WSASocket() API的上层架构可能很重要(译者不太明白原文意思,抱歉)。每一个AcceptEx()调用都须要建立一个新套接字,因此最好有一个独立的线程专门调用AcceptEx(),而不参与其它I/O处理。你也能够利用这个线程来执行其它任务,好比事件记录。
有关AcceptEx()的最后一个注意事项:要实现这些API,并不须要其它提供商提供的Winsock2实现。这一点对微软特有的其它API也一样适 用,好比TransmitFile()和GetAcceptExSockAddrs(),以及其它可能会被加入到新版Windows的API. 在Windows NT和2000上,这些API是在微软的底层提供者DLL(mswsock.dll)中实现的,可经过与mswsock.lib编译链接进行调用,或者通 过WSAIoctl() (选项参数为SIO_GET_EXTENSION_FUNCTION_POINTER)动态得到函数的指针。
若是在没有事先得到函数指针的状况下直接调用函数(也就是说,编译时静态链接mswsock.lib,在程序中直接调用函数),那么性能将很受影响。由于 AcceptEx()被置于Winsock2架构以外,每次调用时它都被迫经过WSAIoctl()取得函数指针。要避免这种性能损失,须要使用这些 API的应用程序应该经过调用WSAIoctl()直接从底层的提供者那里取得函数的指针。
Winsock 提供两个专门为文件和内存数据传输进行了优化的函数。其中TransmitFile()这个API函数在Windows NT 4.0 和 Windows 2000上均可以使用,而TransmitPackets()则将在将来版本的Windows中实现。
TransmitFile() 用来把文件内容经过Winsock进行传输。一般发送文件的作法是,先调用CreateFile()打开一个文件,而后不断循环调用ReadFile() 和WSASend ()直至数据发送完毕。可是这种方法很没有效率,由于每次调用ReadFile() 和 WSASend ()都会涉及一次从用户模式到内核模式的转换。若是换成TransmitFile(),那么只须要给它一个已打开文件的句柄和要发送的字节数,而所涉及的 模式转换操做将只在调用CreateFile()打开文件时发生一次,而后TransmitFile()时再发生一次。这样效率就高多了。
TransmitPackets()比TransmitFile()更进一步,它容许用户只调用一次就能够发送指定的多个文件和内存缓冲区。函数原型以下:
BOOL TransmitPackets( SOCKET hSocket, LPTRANSMIT_PACKET_ELEMENT lpPacketArray, DWORD nElementCount, DWORD nSendSize, LPOVERLAPPED lpOverlapped, DWORD dwFlags );
其中,lpPacketArray是一个结构的数组,其中的每一个元素既能够是一个文件句柄或者内存缓冲区,该结构定义以下:
typedef struct _TRANSMIT_PACKETS_ELEMENT { DWORD dwElFlags; DWORD cLength; union { struct { LARGE_INTEGER nFileOffset; HANDLE hFile; }; PVOID pBuffer; }; } TRANSMIT_FILE_BUFFERS;
其中各字段是自描述型的(self explanatory)。
使用这两个API的另外一个好处,是能够经过指定TF_REUSE_SOCKET和TF_DISCONNECT标志来重用套接字句柄。每当API完成数据的传 输工做后,就会在传输层级别断开链接,这样这个套接字就又能够从新提供给AcceptEx()使用。采用这种优化的方法编程,将减轻那个专门作接受操做的 线程建立套接字的压力(前文述及)。
这两个API也都有一个共同的弱点:Windows NT Workstation 或 Windows 2000 专业版中,函数每次只能处理两个调用请求,只有在Windows NT、Windows 2000服务器版、Windows 2000高级服务器版或 Windows 2000 Data Center中才得到彻底支持。
以上各节中,咱们讨论了开发高性能的、大响应规模的应用程序所需的函数、方法和可能遇到的资源瓶颈问题。这些对你意味着什么呢?其实,这取决于你如何构造你的服务器和客户端。当你可以在服务器和客户端设计上进行更好地控制时,那么你越可以避开瓶颈问题。
来看一个示范的环境。咱们要设计一个服务器来响应客户端的链接、发送请求、接收数据以及断开链接。那么,服务器将须要建立一个监听套接字,把它与某个完成端 口进行关联,为每颗CPU建立一个工做线程。再建立一个线程专门用来发出AcceptEx()。咱们知道客户端会在发出链接请求后马上传送数据,因此若是 咱们准备好接收缓冲区会使事情变得更为容易。固然,不要忘记不时地轮询AcceptEx()调用中使用的套接字(使用SO_CONNECT_TIME选项 参数)来确保没有恶意超时的链接。
该设计中有一个重要的问题要考虑,咱们应该容许多少个AcceptEx()进行守候。这 是由于,每发出一个AcceptEx()时咱们都同时须要为它提供一个接收缓冲区,那么内存中将会出现不少被锁定的页面(前文说过了,每一个重叠操做都会消 耗一小部分未分页内存池,同时还会锁定全部涉及的缓冲区)。这个问题很难回答,没有一个确切的答案。最好的方法是把这个值作成能够调整的,经过反复作性能 测试,你就能够得出在典型应用环境中最佳的值。
好了,当你测算清楚后,下面就是发送数据的问题了,考虑的重点是你但愿服务 器同时处理多少个并发的链接。一般状况下,服务器应该限制并发链接的数量以及等候处理的发送调用。由于并发链接数量越多,所消耗的未分页内存池也越多;等 候处理的发送调用越多,被锁定的内存页面也越多(当心别超过了极限)。这一样也须要反复测试才知道答案。
对于上述环境,通 常不须要关闭单个套接字的缓冲区,由于只在AcceptEx()中有一次接收数据的操做,而要保证给每一个到来的链接提供接收缓冲区并非太难的事情。但 是,若是客户机与服务器交互的方式变一变,客户机在发送了一次数据以后,还须要发送更多的数据,在这种状况下关闭接收缓冲就不太妙了,除非你想办法保证在 每一个链接上都发出了重叠接收调用来接收更多的数据。
开发大响应规模的Winsock服务器并非很可怕,其实也就是设置一个监听套接字、接受链接请求和进行重叠收发调用。经过设置合理的进行守候的重叠调用的 数量,防止出现未分页内存池被耗尽,这才是最主要的挑战。按照咱们前面讨论的一些原则,你就能够开发出大响应规模的服务器应用程序。