端口与高性能服务器程序开发



用完成端口开发大响应规模的Winsock应用程序(5/完)

2006-04-18 17:21:12 happy0123@-axj4 /article/-axj4-s__gQw.html 复制 评论

TransmitFile 和 TransmitPackets

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)。
dwElFlags字段:指定当前元素是一个文件句柄还是内存缓冲区(分别通过常量TF_ELEMENT_FILE 和TF_ELEMENT_MEMORY指定);
cLength字段:指定将从数据源发送的字节数(如果是文件,这个字段值为0表示发送整个文件);
结构中的无名联合体:包含文件句柄的内存缓冲区(以及可能的偏移量)。

使用这两个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服务器并不是很可怕,其实也就是设置一个监听套接字、接受连接请求和进行重叠收发调用。通过设置合理的进行守候的重叠调用的数量,防止出现未分页内存池被耗尽,这才是最主要的挑战。按照我们前面讨论的一些原则,你就可以开发出大响应规模的服务器应用程序。

 



2006-04-18 17:19:46 happy0123@-axj4 /article/-axj4-s__MVw.html 复制 评论
 

接受连接请求

服务器要做的最普通的事情之一就是接受来自客户端的连接请求。在套接字上使用重叠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()直接从底层的提供者那里取得函数的指针。

参见Figure 3 套接字架构:

 



用完成端口开发大响应规模的Winsock应用程序(3)

2006-04-18 17:16:54 happy0123@-axj4 /article/-axj4-s_ZhTq.html 复制 评论
 

资源的限制条件

在设计任何服务器应用程序时,其强健性是主要的目标。也就是说,

你的应用程序要能够应对任何突发的问题,例如并发客户请求数达到峰值、可用内存临时出现不足、以及其它短时间的现象。这就要求程序的设计者注意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 ,关于内存优化,译者另有译文),如果错误继续出现,那么注意检查是否是网络带宽不足的问题。之后,请确认你没有同时发出太多的收发调用。最后,如果还是收到资源不足的错误,那就很可能是遇到了未分页内存池不足的问题了。要释放未分页内存池空间,请关闭应用程序中相当部分的连接,等待系统自行渡过和修正这个瞬时的错误。

 



 

用完成端口开发大响应规模的Winsock应用程序(2)

2006-04-18 17:15:42 happy0123@-axj4 /article/-axj4-s_ZQz4.html 复制 评论
 

Windows NT和Windows 2000的套接字架构

对于开发大响应规模的Winsock应用程序而言,对Windows NT和Windows 2000的套接字架构有基本的了解是很有帮助的。

与其它类型操作系统不同,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总是有由应用程序提供的缓冲区可用,那么它将没有必要使用内部缓冲区。

高性能的服务器应用程序可以关闭发送缓冲区,同时不会损失性能。不过,这样的应用程序必须十分小心,保证它总是发出多个重叠发送调用,而不是等待某个重叠发送结束了才发出下一个。如果应用程序是按一个发完再发下一个的顺序来操作,那浪费掉两次发送中间的空档时间,总之是要保证传输驱动程序在发送完一个缓冲区后,立刻可以转向另一个缓冲区。





    摘要:用完成端口开发大响应规模的Winsock应用程序(1)     (全文共13159字)——点击 此处阅读全文


2006年07月18日

Email:kruglinski_at_gmail_dot_com
Blog:kruglinski.blogchina.com

早在两年前我就已经能很熟练的运用完成端口这种技术了,只是一直没有机会将它用在什么项目中,这段时间见到这种技术被过分炒作,过分的神秘化,就想写一篇解释它如何工作的文章.想告诉大家它没有传说中的那么高深难懂!有什么错误的地方还请高人指正.转载请注明出处及作者,谢谢!

以一个文件传输服务端为例,在我的机器上它只起两个线程就可以为很多个个客户端同时提供文件下载服务,程序的性能会随机器内CPU个数的增加而线性增长,我尽可能做到使它清晰易懂,虽然程序很小却用到了NT 5的一些新特性,重叠IO,完成端口以及线程池,基于这种模型的服务端程序应该是NT系统上性能最好的了.

首先.做为完成端口的基础,我们应该理解重叠IO,这需要你已经理解了内核对象及操作系统的一些概念概念,什么是信号/非信号态,什么是等待函数,什么是成功等待的副作用,什么是线程挂起等,如果这些概令还没有理解,你应该先看一下Windows 核心编程中的相关内容.如果已经理解这些,那么重叠IO对你来说并不难.

你可以这样认为重叠IO,现在你已经进入一个服务器/客户机环境,请不要混淆概念,这里的服务器是指操作系统,而客户机是指你的程序(它进行IO操作),是当你进行IO操作(send,recv,writefile,readfile....)时你发送一个IO请求给服务器(操作系统),由服务器来完成你需要的操作,然后你什么事都没有了,当服务器完成IO请求时它会通知你,当然在这期间你可以做任何事,一个常用的技巧是在发送重叠IO请求后,程序在一个循环中一边调用PeekMessage,TranslateMessage和DispatchMessage更新界面,同时调用GetOverlappedResult等待服务器完成IO操作,更高效一点的做法是使用IO完成例程来处理服务器(操作系统)返回的结果,但并不是每个支持重叠IO操作的函数都支持完成例程如TransmitFile函数.

例1.一次重叠写操作过程(GetOverlappedResult方法):
1.填写一个OVERLAPPED结构
2.进行一次写操作,并指定重叠操作参数(上面的OVERLAPPED结构变量的指针)
3.做其它事(如更新界面)
4.GetOverlappedResult取操作结果
5.如果IO请求没有完成,并且没有出错则回到期3
6.处理IO操作结果

例2.一次重叠写操作过程(完成例程方法):
1.填写一个OVERLAPPED结构
2.进行一次写操作,并指定重叠操作参数(上面的OVERLAPPED结构变量的指针),并指定完成例程
3.做其它事(如更新界面)
4.当完成例程被调用说明IO操作已经完成或出错,现在可以对操作结果进行处理了


如果你已经理解上面的概念,就已经很接近IO完成端口了,当然这只是很常规的重叠操作它已经非常高效,但如果再结合多线程对一个File或是Socket进行重叠IO操作就会非常复杂,通常程序员很难把握这种复杂度.完成端口可以说就是为了充分发挥多线程和重叠IO操作相结合的性能而设计的.很多人都说它复杂,其实如果你自己实现一个多线程的对一个File或是Socket进行重叠IO操作的程序(注意是多个线程对一个HANDLE或SOCKET进行重叠IO操作,而不是启一个线程对一个HANDLE进行重叠IO操作)就会发现完成端口实际上简化了多线程里使用重叠IO的复杂度,并且性能更高,性能高在哪?下面进行说明.

我们可能写过这样的服务端程序:

例3.主程序:
1.监听一个端口
2.等待连接
3.当有连接来时
4.启一个线程对这个客户端进行处理
5.回到2

服务线程:
1.读客户端请求
2.如果客户端不再有请求,执行6
3.处理请求
4.返回操作结果
5.回到1
6.退出线程

这是一种最简单的网络服务器模型,我们把它优化一下

例4.主程序:
1.开一个线程池,里面有机器能承受的最大线程数个线程,线程都处于挂起(suspend)状态
1.监听一个端口
2.等待连接
3.当有连接来时
4.从线程池里Resume一个线程对这个客户端进行处理
5.回到2

服务线程与例3模型里的相同,只是当线程处理完客户端所有请求后,不是退出而是回到线程池,再次挂起让出CPU时间,并等待为下一个客户机服务.当然在此期间线程会因为IO操作(服务线程的第1,5操作,也许还有其它阻塞操作)挂起自己,但不会回到线程池,也就是说它一次只能为一个客户端服务.

这可能是你能想到的最高效的服务端模型了吧!它与第一个服务端模型相比少了很多个用户态到内核态的CONTEXT Switch,反映也更加快速,也许你可能觉得这很微不足道,这说明你缺少对大规模高性能服务器程序(比如网游服务端)的认识,如果你的服务端程序要对几千万个客户端进行服务呢?这也是微软Windows NT开发组在NT 5以上的系统中添加线程池的原因.

思考一下什么样的模型可以让一个线程为多个客户端服务呢!那就要跳出每来一个连接启线程为其服务的固定思维模式,我们把线程服务的最小单元分割为单独的读或写操作(注意是读或写不是读和写),而不是一个客户端从连接到断开期间的所有读写操作.每个线程都使用重叠IO进行读写操作,投递了读写请求后线程回到线程池,等待为其它客户机服务,当操作完成或出错时再回来处理操作结果,然后再回到线程池.

看看这样的服务器模型:
例5.主程序:
1.开一个线程池,里面有机器内CPU个数两倍的线程,线程都处于挂起(suspend)状态,它们在都等处理一次重叠IO操作的完成结果
1.监听一个端口
2.等待连接
3.当有连接来时
4.投递一个重叠读操作读取命令
5.回到2

服务线程:
1.如果读完成,则处理读取的内容(如HTTP GET命令),否则执行3
2.投递一个重叠写操作(如返回HTTP GET命令需要的网页)
3.如果是一个写操作完成,可以再投递一个重叠读操作,读取客户机的下一个请求,或者是关闭连接(如HTTP协议里每发完一个网页就断开)
4.取得下一个重叠IO操作结果,如果IO操作没有完成或没有IO操作则回到线程池

假设这是一个WEB服务器程序,可以看到工作者线程是以读或写为最小的工作单元运行的,在主程序里面进行了一次重叠读操作

当读操作完成时一个线程池中的一个工作者线程被激活取得了操作结果,处理GET或POST命令,然后发送一个网页内容,发送也是一个重叠操作,然后处理对其它客户机的IO操作结果,如果没有其它的东西需要处理时回到线程池等待.可以看到使用这种模型发送和接收可以是也可以不是一个线程.

当发送操作完成时,线程池中的一个工作者线程池激活,它关闭连接(HTTP协议),然后处理其它的IO操作结果,如果没有其它的东西需要处理时回到线程池等待.

看看在这样的模型中一个线程怎么为多个客户端服务,同样是模拟一个WEB服务器例子:

假如现在系统中有两个线程,ThreadA,ThreadB它们在都等处理一次重叠IO操作的完成结果

当一个客户机ClientA连接来时主程序投递一个重叠读操作,然后等待下一个客户机连接,当读操作完成时ThreadA被激活,它收到一个HTTP GET命令,然后ThreadA使用重叠写操作发送一个网页给ClientA,然后立即回到线程池等待处理下一个IO操作结果,这时发送操作还没有完成,又有一个客户机ClientB连接来,主程序再投递一个重叠读操作,当读操作完成时ThreadA(当然也可能是ThreadB)再次被激活,它重复同样步骤,收到一个GET命令,使用重叠写操作发送一个网页给ClientB,这次它没有来得及回到线程池时,又有一个连接ClientC连入,主程序再投递一个重叠读操作,读操作完成时ThreadB被激活(因为ThreadA还没有回到线程池)它收到一个HTTP GET命令,然后ThreadB使用重叠写操作发送一个网页给ClientC,然后ThreadB回到线程池,这时ThreadA也回到了线程池.

可以想象现在有三个挂起的发送操作分别是ThreadA发送给ClientA和ClientB的网页,以及ThreadB发送给ClientC的网页,它们由操作系统内核来处理.ThreadA和ThreadB现在已经回到线程池,可以继续为其它任何客户端服务.

当对ClientA的重叠写操作已经完成,ThreadA(也可以是ThreadB)又被激活它关闭与ClientA连接,但还没有回到线程池,与此同时发送给ClientB的重叠写操作也完成,ThreadB被激活(因为ThreadA还没有回到线程池)它关闭与ClientB的连接,然后回到线程池,这时ClientC的写操作也完成,ThreadB再次被激活(因为ThreadA还是没有回到线程池),它再关闭与ClientC的连接,这时ThreadA回到线程池,ThreadB也回到线程池.这时对三个客户端的服务全部完成.可以看到在整个服务过程中,"建立连接","读数据","写数据"和"关闭连接"等操作是逻辑上连续而实际上分开的.

到现在为止两个线程处理了三次读操作和三次写操作,在这些读写操作过程中所出现的状态机(state machine)是比较复杂的,我们模拟的是经过我简化过的,实际上的状态要比这个还要复杂很多,然而这样的服务端模型在客户端请求越多时与前两个模型相比的性能越高.而使用完成端口我们可以很容易实现这样的服务器模型.

微软的IIS WEB服务器就是使用这样的服务端模型,很多人说什么阿帕奇服务器比IIS的性能好什么什么的我表示怀疑,除非阿帕奇服务器可以将线程分割成,为更小的单元服务,我觉得不太可能!这种完成端口模型已经将单个读或写操作作为最小的服务单元,我觉得在相同机器配置的情况下IIS的性能要远远高于其它WEB服务器,这也是从实现机理上来分析的,如果出现性能上的差别可能是在不同的操作系统上,也许Linux的内核比Windows的要好,有人真的研究过吗?还是大家一起在炒作啊.

对于状态机概念,在很多方面都用到,TCPIP中有,编译原理中有,OpengGL中有等等,我的离散数学不好(我是会计专业不学这个),不过还是搞懂了些,我想如果你多花些时间看,还是可以搞懂的.最后是一个简单的文件传输服务器程序代码,只用了两个线程(我的机器里只有一块CPU)就可以服务多个客户端.我调试时用它同时为6个nc客户端提供文件下载服务都没有问题,当然更多也不会有问题,只是略为使用了一下NT 5的线程池和完成端口技术就可以有这样高的性能,更不用说IIS的性能咯!

希望大家不要陷在这个程序的框架中,Ctrl+C,Ctrl+V没有什么意义,要理解它的实质.程序使用Visual C++ 6.0 SP5+2003 Platform SDK编译通过,在Windows XP Professional下调试运行通过.程序运行的最低要求是Windows 2000操作系统.

/********************************************************************
    created:    2005/12/24
    created:    24:12:2005   20:25
    modified:    2005/12/24
    filename:     d:/vcwork/iocomp/iocomp.cpp
    file path:    d:/vcwork/iocomp
    file base:    iocomp
    file ext:    cpp
    author:        kruglinski(kruglinski_at_gmail_dot_com)
    
    purpose:    利用完成端口技术实现的高性能文件下载服务程序
*********************************************************************/

#define _WIN32_WINNT    0x0500

#include <cstdlib>
#include <clocale>
#include <ctime>
#include <iostream>//一使用输入输出流程序顿时增大70K
#include <vector>
#include <algorithm>
#include <winsock2.h>
#include <mswsock.h>

using namespace std;

#pragma comment(lib,"ws2_32.lib")
#pragma comment(lib,"mswsock.lib")

const int MAX_BUFFER_SIZE=1024;
const int PRE_SEND_SIZE=1024;
const int QUIT_TIME_OUT=3000;
const int PRE_DOT_TIMER=QUIT_TIME_OUT/80;

typedef enum{IoTransFile,IoSend,IoRecv,IoQuit} IO_TYPE;

typedef struct
{
    SOCKET hSocket;
    SOCKADDR_IN ClientAddr;
}PRE_SOCKET_DATA,*PPRE_SOCKET_DATA;

typedef struct
{
    OVERLAPPED    oa;
    WSABUF        DataBuf;
    char        Buffer[MAX_BUFFER_SIZE];
    IO_TYPE        IoType;
}PRE_IO_DATA,*PPRE_IO_DATA;

typedef vector<PPRE_SOCKET_DATA>    SocketDataVector;
typedef vector<PPRE_IO_DATA>        IoDataVector;

SocketDataVector    gSockDataVec;
IoDataVector        gIoDataVec;

CRITICAL_SECTION    csProtection;

char* TimeNow(void)
{
    time_t t=time(NULL);
    tm *localtm=localtime(&t);
    static char timemsg[512]={0};
    
    strftime(timemsg,512,"%Z: %B %d %X,%Y",localtm);
    return timemsg;
}

BOOL TransFile(PPRE_IO_DATA pIoData,PPRE_SOCKET_DATA pSocketData,DWORD dwNameLen)
{
    //这一句是为nc做的,你可以修改它
    pIoData->Buffer[dwNameLen-1]='/0';
    
    HANDLE hFile=CreateFile(pIoData->Buffer,GENERIC_READ,0,NULL,OPEN_EXISTING,0,NULL);
    BOOL bRet=FALSE;

    if(hFile!=INVALID_HANDLE_VALUE)
    {
        cout<<"Transmit File "<<pIoData->Buffer<<" to client"<<endl;
        pIoData->IoType=IoTransFile;
        memset(&pIoData->oa,0,sizeof(OVERLAPPED));
        *reinterpret_cast<HANDLE*>(pIoData->Buffer)=hFile;
        TransmitFile(pSocketData->hSocket,hFile,GetFileSize(hFile,NULL),PRE_SEND_SIZE,reinterpret_cast<LPOVERLAPPED>(pIoData),NULL,TF_USE_SYSTEM_THREAD);
        bRet=WSAGetLastError()==WSA_IO_PENDING;
    }
    else
        cout<<"Transmit File "<<"Error:"<<GetLastError()<<endl;

    return bRet;
}

DWORD WINAPI ThreadProc(LPVOID IocpHandle)
{
    DWORD dwRecv=0;
    DWORD dwFlags=0;
    
    HANDLE hIocp=reinterpret_cast<HANDLE>(IocpHandle);
    DWORD dwTransCount=0;
    PPRE_IO_DATA pPreIoData=NULL;
    PPRE_SOCKET_DATA pPreHandleData=NULL;

    while(TRUE)
    {
        if(GetQueuedCompletionStatus(hIocp,&dwTransCount,
            reinterpret_cast<LPDWORD>(&pPreHandleData),
            reinterpret_cast<LPOVERLAPPED*>(&pPreIoData),INFINITE))
        {
            if(0==dwTransCount&&IoQuit!=pPreIoData->IoType)
            {
                cout<<"Client:"
                    <<inet_ntoa(pPreHandleData->ClientAddr.sin_addr)
                    <<":"<<ntohs(pPreHandleData->ClientAddr.sin_port)
                    <<" is closed"<<endl;

                closesocket(pPreHandleData->hSocket);

                EnterCriticalSection(&csProtection);
                    IoDataVector::iterator itrIoDelete=find(gIoDataVec.begin(),gIoDataVec.end(),pPreIoData);
                    SocketDataVector::iterator itrSockDelete=find(gSockDataVec.begin(),gSockDataVec.end(),pPreHandleData);
                   delete *itrIoDelete;
                   delete *itrSockDelete;
                   gIoDataVec.erase(itrIoDelete);
                   gSockDataVec.erase(itrSockDelete);
                LeaveCriticalSection(&csProtection);

                      
                continue;
            }
            
            switch(pPreIoData->IoType){
            case IoTransFile:
                cout<<"Client:"
                    <<inet_ntoa(pPreHandleData->ClientAddr.sin_addr)
                    <<":"<<ntohs(pPreHandleData->ClientAddr.sin_port)
                    <<" Transmit finished"<<endl;
                CloseHandle(*reinterpret_cast<HANDLE*>(pPreIoData->Buffer));
                goto LRERECV;
                
            case IoSend:
                cout<<"Client:"
                    <<inet_ntoa(pPreHandleData->ClientAddr.sin_addr)
                    <<":"<<ntohs(pPreHandleData->ClientAddr.sin_port)
                    <<" Send finished"<<endl;

LRERECV:
                pPreIoData->IoType=IoRecv;
                pPreIoData->DataBuf.len=MAX_BUFFER_SIZE;
                memset(&pPreIoData->oa,0,sizeof(OVERLAPPED));

                WSARecv(pPreHandleData->hSocket,&pPreIoData->DataBuf,1,
                    &dwRecv,&dwFlags,
                    reinterpret_cast<LPWSAOVERLAPPED>(pPreIoData),NULL);

                break;

            case IoRecv:
                cout<<"Client:"
                    <<inet_ntoa(pPreHandleData->ClientAddr.sin_addr)
                    <<":"<<ntohs(pPreHandleData->ClientAddr.sin_port)
                    <<" recv finished"<<endl;
                pPreIoData->IoType=IoSend;
                
                if(!TransFile(pPreIoData,pPreHandleData,dwTransCount))
                {
                    memset(&pPreIoData->oa,0,sizeof(OVERLAPPED));
                    strcpy(pPreIoData->DataBuf.buf,"File transmit error!/r/n");
                    pPreIoData->DataBuf.len=strlen(pPreIoData->DataBuf.buf);
                    
                    WSASend(pPreHandleData->hSocket,&pPreIoData->DataBuf,1,
                        &dwRecv,dwFlags,
                        reinterpret_cast<LPWSAOVERLAPPED>(pPreIoData),NULL);
                }
                break;
                
            case IoQuit:
                goto LQUIT;
                
            default:
                ;
            }
        }    
    }
    
LQUIT:
    return 0;
}

HANDLE hIocp=NULL;
SOCKET hListen=NULL;

BOOL WINAPI ShutdownHandler(DWORD dwCtrlType)
{
    PRE_SOCKET_DATA PreSockData={0};
    PRE_IO_DATA PreIoData={0};

    PreIoData.IoType=IoQuit;

    if(hIocp)
    {
        PostQueuedCompletionStatus(hIocp,1,
            reinterpret_cast<ULONG_PTR>(&PreSockData),
            reinterpret_cast<LPOVERLAPPED>(&PreIoData));

        cout<<"Shutdown at "<<TimeNow()<<endl<<"wait for a moment please"<<endl;
        
        //让出CPU时间,让线程退出
        for(int t=0;t<80;t+=1)
        {
            Sleep(PRE_DOT_TIMER);
            cout<<".";
        }
        
        CloseHandle(hIocp);
    }
    
    int i=0;

    for(;i<gSockDataVec.size();i++)
    {
        PPRE_SOCKET_DATA pSockData=gSockDataVec[i];
        closesocket(pSockData->hSocket);
        delete pSockData;
    }

    for(i=0;i<gIoDataVec.size();i++)
    {
        PPRE_IO_DATA pIoData=gIoDataVec[i];
        delete pIoData;
    }

    DeleteCriticalSection(&csProtection);
    if(hListen)
        closesocket(hListen);

    WSACleanup();
    exit(0);
    return TRUE;
}

LONG WINAPI MyExceptionFilter(struct _EXCEPTION_POINTERS *ExceptionInfo)
{
    ShutdownHandler(0);
    return EXCEPTION_EXECUTE_HANDLER;
}

u_short DefPort=8182;

int main(int argc,char **argv)
{
    if(argc==2)
        DefPort=atoi(argv[1]);

    InitializeCriticalSection(&csProtection);
    SetUnhandledExceptionFilter(MyExceptionFilter);
    SetConsoleCtrlHandler(ShutdownHandler,TRUE);

    hIocp=CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,0,0);

    WSADATA data={0};
    WSAStartup(0x0202,&data);

    hListen=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
    if(INVALID_SOCKET==hListen)
    {
        ShutdownHandler(0);
    }
    
    SOCKADDR_IN addr={0};
    addr.sin_family=AF_INET;
    addr.sin_port=htons(DefPort);
    
    if(bind(hListen,reinterpret_cast<PSOCKADDR>(&addr),
        sizeof(addr))==SOCKET_ERROR)
    {
        ShutdownHandler(0);
    }
    
    if(listen(hListen,256)==SOCKET_ERROR)
        ShutdownHandler(0);

    SYSTEM_INFO si={0};
    GetSystemInfo(&si);
    si.dwNumberOfProcessors<<=1;

    for(int i=0;i<si.dwNumberOfProcessors;i++)
    {
        
        QueueUserWorkItem(ThreadProc,hIocp,WT_EXECUTELONGFUNCTION);
    }
    
    cout<<"Startup at "<<TimeNow()<<endl
        <<"work on port "<<DefPort<<endl
        <<"press CTRL+C to shutdown"<<endl<<endl<<endl;

    while(TRUE)
    {
        int namelen=sizeof(addr);
        memset(&addr,0,sizeof(addr));
        SOCKET hAccept=accept(hListen,reinterpret_cast<PSOCKADDR>(&addr),&namelen);

        if(hAccept!=INVALID_SOCKET)
        {
            cout<<"accept a client:"<<inet_ntoa(addr.sin_addr)<<":"<<ntohs(addr.sin_port)<<endl;

            PPRE_SOCKET_DATA pPreHandleData=new PRE_SOCKET_DATA;
            pPreHandleData->hSocket=hAccept;
            memcpy(&pPreHandleData->ClientAddr,&addr,sizeof(addr));
            
            CreateIoCompletionPort(reinterpret_cast<HANDLE>(hAccept),
                hIocp,reinterpret_cast<DWORD>(pPreHandleData),0);
            
            PPRE_IO_DATA pPreIoData=new(nothrow) PRE_IO_DATA;

            if(pPreIoData)
            {
                EnterCriticalSection(&csProtection);
                    gSockDataVec.push_back(pPreHandleData);
                    gIoDataVec.push_back(pPreIoData);
                LeaveCriticalSection(&csProtection);

                memset(pPreIoData,0,sizeof(PRE_IO_DATA));
                pPreIoData->IoType=IoRecv;
                pPreIoData->DataBuf.len=MAX_BUFFER_SIZE;
                pPreIoData->DataBuf.buf=pPreIoData->Buffer;
                DWORD dwRecv=0;
                DWORD dwFlags=0;
                WSARecv(hAccept,&pPreIoData->DataBuf,1,
                    &dwRecv,&dwFlags,
                    reinterpret_cast<WSAOVERLAPPED*>(pPreIoData),NULL);
            }
            else
            {
                delete pPreHandleData;
                closesocket(hAccept);
            }
        }
    }
    
    return 0;
}

参考资料:
《MSDN 2001》
《Windows 网络编程》
《Windows 核心编程》
《TCP/IP详解》
  
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值