几种winsock I/O模型的分析(全面分析)

 

概要    

   套接字是通信的基础,是支持网络协议数据通信的基本接口。Winsocket  提供了一些有趣的I/O 模型,有助于应用程序通过一种“异步”方式,一次对一个或者多个套接字上进行的通信加以管理。这些模型包括select (选择)、WSAAsynSelect (异步选择)、WSAEventSelect (事件选择)、Overlapped I/O (重叠 I/O )以及Completion port (完成端口)。

① select 模型:

       select模型是WinSock中应用最广泛的模型之一,核心就是select函数,它可用于判断套接字上是否存在数据,或者能否向一个套接字写入数据。这个函数可以有效地防止应用程序在套接字处于阻塞模式中时,send或recv进入阻塞状态;同时也可以防止产生大量的WSAEWOULDBLOCK 错误select的优势是能够从单个线程的多个套接字上进行多重连接及I/O。这就避免了伴随阻塞套接字和多重连接的线程剧增。

② WSAAsyncSelect 模型:

       因为它是以消息为基础的,关键就是WSAAsyncSelect函数,将socket消息发送到hWnd窗口上,然后在那里处理相应的FD_READ、 FD_WRITE等等消息。优点:WSAAsyncSelect和WSAEventSelect模型提供了读写数据能力的异步通知,但他们不提供异步数据传送,而重叠及完成端口提供异步数据的传送。而且它可以在系统开销不大的情况下同时处理很多连接,而select模型还需要建立fd_set结构。 缺点:必须要使用一个窗口接收消息,如果处理成千上万的套接字就力不从心了。

③ WSAEventSelect 模型:

       这个也是以时间为基础的网络事件通知,但是与WSAAsyncSelect不同的是,它主要是由事件对象句柄完成的,而不是通过窗口。优点:不需要窗口。缺点:每次只能等待64个事件,所以处理多个套接字时有必要组织一个线程池;所以伸缩性就不如后面的完成端口了。

④ 重叠模型:

       这个模型可以使程序能达到更佳的系统性能。基本设计原理就是让应用程序使用重叠的数据结构,一次投递一个或多个I/O请求。针对这些提交的请求,在他们完成之后,应用程序可为他们提供服务。它又分为两种实现方法:在事件中使用,还有就是完成例程。

⑤ 完成端口:

       完成端口提供了最好的伸缩性,往往可以使系统达到最好的性能,是处理成千上万的套接字的首选。从本质上说,完成端口模型要求创建一个windows完成端口对象,该对象通过指定数量的线程,对重叠I/O请求进行管理,以便为已经完成的重叠I/O请求提供服务。

五种 I/O 模型的性能分析

重叠I/O 模型的另外几个优点在于,微软针对重叠I/O 模 型提供了一些特有的扩展函数。当使用重叠I/O 模型时,可以选择使用不同的完成通知方式。

采用事 件对象通知的重叠I/O 模型是不可伸缩的,因为针对发出WSAWaitForMultipleEvents调 用的每个线程,该I/O 模型一次最多都只能支持6 4 个 套接字。假如想让这个模型同时管理不止64 个套接字,必须创建额外的工作者线程,以便等待更多的事 件对象。因为操作系统同时能够处理的事件对象是有限的,所以基于事件对象的I/O 模型不具备伸缩 性。

使用完 成例程通知的重叠I/O 模型,因为以下几个原因,也不是开发高性能服务器的最佳选择。首先,许多扩 展功能不允许使用APC (Asyncroneus Procedure Call ,异步过程调用)完成通知。其次,由于APC 在系统内 部特有的处理机制,应用程序线程可能无限等待而得不到完成通知。当一个线程处于“ 可警告状态” 时,所有挂起的APC 按照先进先出的顺序(FIFO )接受处理。现在考虑这样一种情况,服务器已经建立起了一个连接,并且调用含有完成例程指针的WSARecv投递了一个重叠I/O 请求。当有数据到 达时(即I/O 完成时),完成例程执行并且再次调用WSARecv 抛 出另外一个重叠I/O 请求。一个APC 抛出 的I/O 操作需要一定的时间才能完成,所以这期间可能另外一个完成例程等待执行(比如本次WSARecv还没接收完时,又有一个新的客户接入并发来数据),因为还有更多的数据需要读取(上一个客户发 来的数据尚未读完)。只要(投递WSARecv 的)那个套接字上还有“ 未决” (未接收完)的数据,就会导致调用线程长久阻 塞。

基于完 成端口通知的重叠I/O 模型是Windows NT 系 统提供的一个真正支持高伸缩性的I/O 模型。在上一章中,探讨了Winsock 几种常见的I/O 模型,并且说明了当应对 大规模客户连接时,完成端口是最佳的选择,因为它提供了最好的伸缩性。

对不同Winsock I/O 模型的性能测试结果如图1 所 示。其中服务器采用Pentium4 1.7 GHz Xeon 的CPU ,768M 内存;客户端有3 台PC ,配置分别是Pentium 2 233MHz ,128 MB 内存,Pentium 2 350 MHz ,128 MB 内存,Itanium733 MHz ,1 GB 内存。服务器、客户端安装的操作系统都是Windows XP 。

1. 分析图表1 提供的测试结果可知,在所用的I/O 模型中,阻塞模式性能最差。这个测试程序中,服务器为每个客户创建两个线程:一个负责处理数据的接收,一 个负责处理数据的发送。在多次测试中的共同问题就是,阻塞模式难以应对大规模的客户连接,因为它在创建线程上耗费了太多的系统资源。因此,服务器创建太多 的线程后,再调用CreateThread 函数时,将返回ERROR_NOT_ENOUGH_MEMORY 的 错误,这个错误码提示内存不够。那些发出连接请求的客户则收到WSAECONNREFUSED的错 误提示,表示连接的尝试被拒绝。

让我们来看看监听函数listen ,其原型如下:

WINSOCK_API_LINKAGEint WSAAPI listen (SOCKETs, int backlog );

参数一s 已绑定了地址的监听套接字。

参数二backlog 指定了正在等待连接的最大队列长度。

参数backdog 非常重要,   因为完全可能同时出 现几个对服务器的连接请求。例如,假定backlog参数为2 时 有三个客户机同时发出连接请求,那么前两个会被放在一个“ 等待处理” 队列中,以便应用程序依次为它们提供服务。而第三个连接的请求就会造成一个WSAECONNREFUSED错误。一旦服务器接受了一个连接请求,那个连接请求就会从队列中删去,以便可以 继续接收其他客户发出的连接请求。即当一个连接请求到来时队列已满,那么客户将收到一个WSAECONNREFUSED错 误。而backlog 参数本身的大小就存在着限制,这个限制是由协议提供者决定的。

故阻塞模式下,由于系 统资源的限制,其并发处理量是极难突破的。

2. 非阻塞模式表现出的性能要比阻塞模式稍好,但是占用了太多的CPU 处理时间。测试服务器将所有客户对应的socket 分 类放到FD_SET集合中,然后调用select函 数筛选出对应集合中有事件发生的socket ,并对集合更新。接下来调用FD_ISSET 宏重新判断一个套接字是否在原来加入的FD_SET 集 合中。随着客户连接数量的增多,这种模型的局限性逐渐凸现。仅仅为了判断一个套接字是否有网络事件发生,就需要对集合FD_SET执行一次遍历! 使用迭代搜索来对select 更新的FD_SET 进行扫描,性能可以得 到一些提升。瓶颈在于,服务器必须能够很快地扫描出FD_SET集合中的有网络事件发生的套接字的 相关信息。针对这个问题,可以使用更复杂的扫描算法,如哈希搜索,它的效率是极高的。还需要注意的一个问题就是,非分页池(即直接在物理内存中分配的内 存)的使用极高。这是因为AFD (Ancillary Function Driver, 由afd.sys 提供的支持WindowsSockets 应用程序的底层驱动程序,其中运行在内核模式下afd.sys驱动程序主要管理WinsockTCP/IP 通 信)和TCP 都将使用I/O 缓存,因为服务 器读取数据的速度是有限的,相对于CPU 的处理速度而言,I/O 基 本是零字节的吞吐量。

3. 基于Windows 消 息机制的WSAAsyncSelect 模型能够处理一定的客户连接量,但是扩展性也不是很好。因为 消息泵很快就会阻塞,降低了消息处理的速度。在几次测试中,服务器只能处理大约1/3 的客户端连 接。过多的客户端连接请求都将返回错误提示码WSAECONNREFUSED ,说明服务器不能及时 处理FD_ACCEPT 消息导致连接失败,这样监听队列中待处理的连接请求不致于爆满。然而,通过 上表中的数据可以发现,对那些已经建立的连接,其平均吞吐量也是极低的(即使对于那些对比特率进行了限制的客户也如此)。

4. 基于事件通知的WSAEventSelect 模 型表现得出奇的不错。在所有的测试中,大多数时候,服务器基本能够处理所有的客户连接,并且保持着较高的数据吞吐量。这种模型的缺点是,每当有一个新连接 时,需要动态管理线程池,因为每个线程只能够等待64 个事件对象。当客户连接量超过64 个后再有新客户接入时,需要创建新的线程。在最后一次测试中,建立起了超过45,000个的客户连接后,系统响应速度变得非常缓慢。这时由于为处理大规模的客户连接创建了大量的线程,占 用了过多的系统资源。791 个线程基本达到了极限,服务器不能再接受更多的连接了,原因是WSAENOBUFS:无可用的缓冲区空间,套接字无法创建。另外,客户端程序也达到了极限,不能维持已经建 立的连接。

使用事件通知的重叠I/O 模型和WSAEventSelect 模型在伸缩性上差不多。这两种模型都依赖于等待事件通知的线程池,处理客户通信 时,大量线程上下文的切换是它们共同的制约因素。重叠I/O 模型和WSAEventSelect 模型的测试结果很相似,都表现得不错,直到线程数量超过极限。

5. 最后是针对基于完成端口通知的重叠I/O 模 型的性能测试,由上表中数据可以看出,它是所有I/O 模型中性能最佳的。内存使用率(包括用户分页 池和非分页池)和支持的客户连接量与基于事件通知的重叠I/O 模型和WSAEventSelect 模型基本相同。真正不同的地方,在于对CPU 的 占用。完成端口模型只占用了60% 的CPU , 但是在维持同样规模的连接量时,另外两种模型(基于事件通知的重叠I/O 模型和WSAEventSelect 模型)占用更多的CPU 。 完成端口的另外一个明显的优势是,它维持更大的吞吐量。

对以上各种模型进行分析后,可以会发现客户端与服务器数据通信机制本身存在的缺 陷是一个瓶颈。在以上测试中,服务器 被设计成只做简单的回应,即只是将客户端发送过来的数据发送回去。客户端(即使有比特率限制)不停的发送数据给服务器,这导致大量数据阻塞在服务器上与这 个客户端对应的套接字上(无论是TCP 缓冲区还是AFD 的 单套接字缓冲区,它们都是在非分页池上)。在最后三种性能比较好的模型中,同一时间只能执行一个接受输入操作,这意味着在大多数时间,还是有很多数据处于 “ 未决” 状态。可以修改服务器程序使其以异步方式接受 数据,这样一旦有数据达到,需要将数据缓存起来。这种方案的缺点是,当一个客户连续发送数据时,异步接受到了大量的数据。这会导致其他的客户无法接入,因 为调用线程和工作者线程都不能处理其他的事件或完成通知。通常情况下,调用非阻塞异步接收函数,先返回WSAEWOULDBLOCK, 然后数据间断性的传输,而不采取连续接收的方式。

从以上测试结果,可以看出WSAEventSelect 模型 和重叠I/O 模型是性能表现最佳的。两种基于事件通知的模型中,创建线程池来等待事件完成通知并作 后续处理是很繁琐的,但是并不影响以它们来架构中型服务器的良好性能。当线程的数量随着客户端连接数量而逐增时,CPU 将 花费大量时间在线程的上下文切换上,这将影响服务器的伸缩性,因为连接量达到一定数量后,便饱和了。完成端口模型提供了最佳的可扩展性,因为CPU 使用率低,其支持的客户连接量相对其他模型最多。

     I/O   模型的选择

通过上一节对各种模型的测试分析,对于如何挑选最适合自己应用程序的I/O 模 型已经很明晰了。同开发一个简单的运行多线程的锁定模式应用相比,其他每种I/O 模型都需要更为复 杂的编程工作。因此,针对客户机和服务器应用开发模型的选择,有以下原则。

1. 客户端

若打算开发一个客户机应用,令其同时管理一个或多个套接字,那么建议采用重叠I/O 或WSAEventSelect 模型,以便在一定程度上提升性能。然而,假如开发的是一个以Windows为基础的应用程序,要进行窗口消息的管理,那么WSAAsyncSelect模 型恐怕是一种最好的选择,因为WSAAsyncSelect 本身便是从Windows 消息模型借鉴来的。采用这种模型,程序需具备消息处理功能。

2. 服务器端

若开发的是一个服务器应用,要在一个给定的时间,同时控制多个套接字,建议采用 重叠I/O 模型,这同样是从性能角度考虑的。但是,如果服务器在任何给定的时间,都会为大量I/O 请求提供服务,便应考虑使用I/O 完成端口模型, 从而获得更佳的性能。

你可能感兴趣的:(几种winsock I/O模型的分析(全面分析))