socket编程之登峰造极(1)----完成端口

“完成端口”模型是迄今为止最为复杂的—种I/O 模型。然而。假若—个应用程序同时需要管理为数众多的套接字,那么采用这种模型。往往可以达到最佳的系统性能, 然而不幸的是, 该模型只适用于以下操作系统( 微软的) Windows NT Windows 2000 操作系统。因其设计的复杂性,只有在你的应用程序需要同时管理数百乃至上千个套接字的时候、而且希望随着系统内安装的CPU 数量的增多、应用程序的性能也可以线性提升,才应考虑采用“完成端口”模型。要记住的一个基本准则是,假如要为Windows NT windows 2000 开发高性能的服务器应用, 同时希望为大量套接字I/O 请求提供服务(Web 服务器便是这方面的典型例子) ,那么I/O 完成端口模型便是最佳选择.
    从本质上说, 完成端口模型要求我们创建一个Win32 完成端口对象, 通过指定数量的线程对重叠I/O 请求进行管理。以便为已经完成的重叠I/O 请求提供服务。要注意的是。所谓“完成端口”, 实际是Win32 Windows NT 以及windows 2000 采用的一种I/O 构造机制, 除套接字句柄之外, 实际上还可接受其他东西。然而,本节只打算讲述如何使用套接字句柄,来发挥完成端口模型的巨大威力。使用这种模型之前,首先要创建一个I/O 完成端口对象, 用它面向任意数量的套接字句柄。管理多个I/O 请求。要做到这—点, 需要调用CreateIoCompletionPort 函数。该函数定义如下:   

 

HANDLE CreateIoCompletionPort (

 

                              HANDLE FileHandle ,

 

                               HANDLE ExistingCompletionPort ,

 

                              DWORD CompletionKey ,

 

                              DWORD   NumberOfConcurrentThreads

 

                              );

 

在我们深入探讨其中的各个参数之前, 首先要注意意该函数实际用于两个明显有别的目的:
    ■用于创建—个完成端口对象。

 

     ■将一个句柄同完成端口关联到一起。

 

    最开始创建—个完成端口的时候,唯一感兴趣的参数便是NumberOfConcurrentThreads 并发线程的数量) ;前面三个参数都会被忽略。NumberOfConcurrentThreads 参数的特殊之处在于.它定义了在一个完成端口上,同时允许执行的线程数量。理想情况下我们希望每个处理器各自负责—个线程的运行,为完成端口提供服务, 避免过于频繁的线程“场景”切换。若将该参数设为0, 说明系统内安装了多少个处理器, 便允许同时运行多少个线程!可用下述代码创建一个I/O 完成端口:
CompetionPort = CreateIoCompletionPort ( INVALID_HANDLE_VALUE , NULL ,0,0)
    该语加的作用是返问一个句柄.在为完成端口分配了—个套接字句柄后,用来对那个端
口进行标定( 引用)
  
 1 .工作者线程与完成端口

 

    成功创建一个完成端口后,便可开始将套接字句柄与对象关联到一起。但在关联套接字之前、首先必须创建—个或多个“工作者线程”,以便在I/O 请求投递给完成端口对象后。为完成端口提供服务。在这个时候,大家或许会觉得奇怪、到底应创建多少个线程。以便为完成端口提供服务呢? 这实际正是完成端口模型显得颇为“复杂”的—个方面, 因为服务I/O 请求所需的数量取决于应用程序的总体设计情况。在此要记住的—个重点在于,在我们调用CreateIoComletionPort 时指定的并发线程数量, 与打算创建的工作者线程数量相比, 它们代表的并非同—件事情。早些时候, 我们曾建议大家用CreateIoCompletionPort 函数为每个处理器都指定一个线程( 处理器的数量有多少, 便指定多少线程) 以避免由于频繁的线程“场景”交换活动,从而影响系统的整体性能。CreateIoCompletionPort 函数的NumberofConcurrentThreads 参数明确指示系统:  在一个完成端口上,一次只允许n 个工作者线程运行。假如在完成端门上创建的工作者线程数量超出n 个.那么在同一时刻,最多只允许n 个线程运行。但实际上,在—段较短的时间内, 系统有可能超过这个值。但很快便会把它减少至事先在CreateIoCompletionPort 函数中设定的值。那么, 为何实际创建的工作者线程数最有时要比CreateIoCompletionPort 函数设定的多—些呢? 这样做有必要吗? 如先前所述。这主要取决于应用程序的总体设计情况,假设我们的工作者线程调用了一个函数,比如Sleep() 或者WaitForSingleobject() ,但却进入了暂停( 锁定或挂起) 状态、那么允许另—个线程代替它的位置。换行之,我们希望随时都能执行尽可能多的线程;当然,最大的线程数量是事先在CreateIoCompletonPort 调用里设定好的。这样—来。假如事先预料到自己的线程有可能暂时处于停顿状态, 那么最好能够创建比CreateIoCompletionPort NumberofConcurrentThreads 参数的值多的线程.以便到时候充分发挥系统的潜力。—旦在完成端口上拥有足够多的工作者线程来为I/O 请求提供服务, 便可着手将套接字句柄同完成端口关联到一起。这要求我们在—个现有的完成端口上调用CreateIoCompletionPort 函数,同时为前三个参数: FileHandle,ExistingCompletionPort CompletionKey ——提供套接字的信息。其中,FileHandle 参数指定—个要同完成端口关联在—一起的套接字句柄。
    ExistingCompletionPort 参数指定的是一个现有的完成端口。CompletionKey( 完成键) 参数则指定要与某个特定套接字句柄关联在—起的“单句柄数据”, 在这个参数中,应用程序可保存与—个套接字对应的任意类型的信息。之所以把它叫作“单句柄数据”, 是由于它只对应着与那个套接字句柄关联在—起的数据。可将其作为指向一个数据结构的指针、来保存套接字句柄;在那个结构中,同时包含了套接字的句柄, 以及与那个套接字有关的其他信息。就象本章稍后还会讲述的那样, 为完成端口提供服务的线程例程可通过这个参数。取得与其套字句柄有关的信息。
根据我们到目前为止学到的东西。首先来构建—个基本的应用程序框架。
程序清单8 9 向人家阐述了如何使用完成端口模型。来开发—个回应( 或“反射’) 服务器应用
在这个程序中。我们基本上按下述步骤行事:
1)      创建一个完成端口。第四个参数保持为0 ,指定在完成端口上,每个处理器一次只允许执行一个工作者线程。
2)      判断系统内到底安装了多少个处理器。
3)      创建工作者线程, 根据步骤2) 得到的处理器信息,在完成端口上,为已完成的I/O 请求提供服务。在这个简单的例子中, 我们为每个处理器都只创建—个工作者线程。这是出于事先已经预计到, 到时候不会有任何线程进入“挂起”状态, 造成由于线程数量的不足,而使处理器空闲的局面( 没有足够的线程可供执行) 。调用CreateThread 函数时, 必须同时提供—个工作者线程,由线程在创建好执行。本节稍后还会详细讨论线程的职责。
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) 。直到服务器终止。
程序清单8 完成端口的建立
StartWinsock()
// 步骤一, 创建一个完成端口
CompletionPort=CreateIoCompletionPort(INVALI_HANDLE_VALUE,NULL,0,0);
// 步骤二判断有多少个处理器
GetSystemInfo(&SystemInfo);
// 步骤三:根据处理器的数量创建工作线程, 本例当中,工作线程的数目和处理器数目是相同的
for(i = 0; i < SystemInfo.dwNumberOfProcessers,i++){
HANDLE ThreadHandle;
// 创建工作者线程,并把完成端口作为参数传给线程
ThreadHandle=CreateThread(NULL,0
ServerWorkerThread CompletionPort,
    0  &ThreadID);
// 关闭线程句柄( 仅仅关闭句柄,并非关闭线程本身)
CloseHandle(ThreadHandle);
}
// 步骤四: 创建监听套接字
Listen=WSASocket(AF_INET,S0CK_STREAM,0,NULL,
    WSA_FLAG_OVERLAPPED);
InternetAddr.sin_famlly=AF_INET;
InternetAddr.sin_addr.s_addr = htonl(INADDR_ANY);
InternetAddr.sln_port = htons(5150);
bind(Listen,(PSOCKADDR)&InternetAddr sizeof(InternetAddr));
// 准备监听套接字
listen(Listen 5);
while(TRUE){
// 步骤五,接入Socket, 并和完成端口关联
Accept = WSAAccept(Listen,NULL,NULL,NULL,0);
// 步骤六 创建一个perhandle 结构,并和端口关联
PerHandleData=(LPPER_HANDLE_DATA)GlobalAlloc(GPTR sizeof(PER_HANDLE_DATA));
printf("Socket number %d connected/n",Accept);
PerHandleData->Socket=Accept;
// 步骤七, 接入套接字和完成端口关联
CreateIoCompletionPort((HANDLE)Accept,
 CompletionPort,(DWORD)PerHandleData,0);
// 步骤八
// 开始进行I/O 操作,用重叠I/O 发送一些WSASend() WSARecv()
WSARecv( ...)
 

你可能感兴趣的:(socket编程之登峰造极(1)----完成端口)