Winsock 完成端口模型简介
摘自《Networking Programming for Microsoft Windows》第八章
“完成端口”模型是迄今为止最为复杂的一种I/O模型。然而,假若一个应用程序同时需要管理为数众多的套接字,那么采用这种模型,往往可以达到最佳的系统性能!
从本质上说,完成端口模型要求我们创建一个Win32完成端口对象,通过指定数量的线程,对重叠I/O请求进行管理,以便为已经完成的重叠I/O请求提供服务。
该函数定义如下:
HANDLE FileHandle,
HANDLE ExistingCompletionPort,
ULONG_PTR CompletionKey,
DWORD NumberOfConcurrentThreads
);
在我们深入探讨其中的各个参数之前,首先要注意该函数实际用于两个明显有别的目的:
1. 用于创建一个完成端口对象。
2. 将一个句柄同完成端口关联到一起。
该语句的作用是返回一个句柄,在为完成端口分配了一个套接字句柄后,用来对那个端口进行标定(引用)。
一、工作者线程与完成端口
成功创建一个完成端口后,便可开始将套接字句柄与对象关联到一起。但在关联套接字之前,首先必须创建一个或多个“工作者线程”,以便在I/O请求投递给完成端口对象后,为完成端口提供服务。在这个时候,大家或许会觉得奇怪,到底应创建多少个线程,以便为完成端口提供服务呢?这实际正是完成端口模型显得颇为“复杂”的一个方面,因为服务I/O请求所需的数量取决于应用程序的总体设计情况。在此要记住的一个重点在于,在我们调用CreateIoCompletionPort时指定的并发线程数量,与打算创建的工作者线程数量相比,它们代表的并非同一件事情。早些时候,我们曾建议大家用CreateIoCompletionPort函数为每个处理器
都指定一个线程(处理器的数量有多少,便指定多少线程)以避免由于频繁的线程“场景”交换活动,从而影响系统的整体性能。CreateIoCompletionPort函数的 NumberOfConcurrentThreads参数明确指示系统:在一个完成端口上,一次只允许n个工作者线程运行。假如在完成端口上创建的工作者线程数量超出n个,那么在同一时刻,最多只允许n个线程运行。但实际上,在一段较短的时间内,系统有可能超过这个值,但很快便会把它减少至事先在 CreateIoCompletionPort函数中设定的值。那么,为何实际创建的工作者线程数量有时要比 CreateIoCompletionPort函数设定的多一些呢?这样做有必要吗?如先前所述,这主要取决于
应用程序的总体设计情况。假定我们的某个工作者线程调用了一个函数,比如Sleep或WaitForSingleObject,但却进入了暂停(锁定或挂起)状态,那么允许另一个线程代替它的位置。换言之,我们希望随时都能执行尽可能多的线程;当然,最大的线程数量是事先在CreateIoCompletionPort调用里设定好的。这样一来,假如事先预计到自己的线程有可能暂时处于停顿状态,那么最好能够创建比CreateIoCompletionPort的 NumberOfConcurrentThreads参数的值多的线程,以便到时候充分发挥系统的潜力。一旦在完成端口上拥有足够多的工作者线程来为 I/O请求提供服务,便可着手将套接字句柄同完成端口关联到一起。这要求我们在一个现有的完成端口上,调用CreateIoCompletionPort 函数,同时为前三个参数——FileHandle,ExistingCompletionPort和CompletionKey——提供套接字的信息。其中, FileHandle参数指定一个要同完成端口关联在一起的套接字句柄。ExistingCompletionPort参数指定的是一个现有的完成端口。 CompletionKey(完成键)参数则指定要与某个特定套接字句柄关联在一起的“单句柄数据”;在这个参数中,应用程序可保存与一个套接字对应的任意类型的信息。之所以把它叫作“单句柄数据”,是由于它只对
应着与那个套接字句柄关联在一起的数据。可将其作为指向一个数据结构的指针,来保存套接字句柄;在那个结构中,同时包含了套接字的句柄,以及与那个套接字有关的其他信息。
根据我们到目前为止学到的东西,首先来构建一个基本的应用程序框架。下面阐述了如何使用完成端口模型,来开发一个ECHO服务器应用。在这个程序中,我们基本上按下述步骤行事:
1) 创建一个完成端口。第四个参数保持为0,指定在完成端口上,每个处理器一次只允许执行一个工作者线程。
2) 判断系统内到底安装了多少个处理器。
3) 创建工作者线程,根据步骤2)得到的处理器信息,在完成端口上,为已完成的I/O请求提供服务。
4) 准备好一个监听套接字,在端口5150上监听进入的连接请求。
5) 使用accept函数,接受进入的连接请求。
6) 创建一个数据结构,用于容纳“单句柄数据”,同时在结构中存入接受的套接字句柄。
7) 调用CreateIoCompletionPort,将自accept返回的新套接字句柄同完成端口关联到一起。通过完成键(CompletionKey)参数,将单句柄数据结构传递给CreateIoCompletionPort。
8) 开始在已接受的连接上进行I/O操作。在此,我们希望通过重叠I/O机制,在新建的套接字上投递一个或多个异步WSARecv或WSASend请求。这些 I/O请求完成后,一个工作者线程会为I/O请求提供服务,同时继续处理未来的I/O请求,稍后便会在步骤3 )指定的工作者例程中,体验到这一点。
9) 重复步骤5 ) ~ 8 ),直至服务器中止。