完成端口模型 “完成端口”模型是迄今为止最为复杂的一种 I / O模型。然而,假若一个应用程序同时需 要管理为数众多的套接字,那么采用这种模型,往往可以达到最佳的系统性能!但不幸的是, 该模型只适用于Windows NT和Windows 2000操作系统。因其设计的复杂性,只有在你的应用 程序需要同时管理数百乃至上千个套接字的时候,而且希望随着系统内安装的 C P U数量的增 多,应用程序的性能也可以线性提升,才应考虑采用“完成端口”模型。要记住的一个基本 准则是,假如要为Windows NT或Windows 2000开发高性能的服务器应用,同时希望为大量套 接字I / O请求提供服务(We b服务器便是这方面的典型例子),那么I / O完成端口模型便是最佳 选择! 从本质上说,完成端口模型要求我们创建一个Wi n 3 2完成端口对象,通过指定数量的线程, 对重叠I / O请求进行管理,以便为已经完成的重叠I / O请求提供服务。要注意的是,所谓“完成 端口”,实际是Wi n 3 2、Windows NT以及Windows 2000采用的一种I / O构造机制,除套接字句 柄之外,实际上还可接受其他东西。然而,本节只打算讲述如何使用套接字句柄,来发挥完 成端口模型的巨大威力。使用这种模型之前,首先要创建一个 I / O完成端口对象,用它面向任 意数量的套接字句柄,管理多个I / O请求。要做到这一点,需要调用CreateIoComletionPort函数。 该函数定义如下: HANDLE CreateIoCompletionPort ( HANDLE FileHandle, // handle to file HANDLE ExistingCompletionPort, // handle to I/O completion port ULONG_PTR CompletionKey, // completion key DWORD NumberOfConcurrentThreads // number of threads to execute concurrently );
在我们深入探讨其中的各个参数之前,首先要注意该函数实际用于两个明显有别的目的: ■ 用于创建一个完成端口对象。 ■ 将一个句柄同完成端口关联到一起。 最开始创建一个完成端口时,唯一感兴趣的参数便是 NumberOfConcurrentThreads(并发 线程的数量);前面三个参数都会被忽略。NumberOfConcurrentThreads参数的特殊之处在于, 它定义了在一个完成端口上,同时允许执行的线程数量。理想情况下,我们希望每个处理器 各自负责一个线程的运行,为完成端口提供服务,避免过于频繁的线程“场景”切换。若将 该参数设为0,表明系统内安装了多少个处理器,便允许同时运行多少个线程!可用下述代码 创建一个I / O完成端口: CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0)
该语句的作用是返回一个句柄,在为完成端口分配了一个套接字句柄后,用来对那个端 口进行标定(引用)。 1. 工作者线程与完成端口 成功创建一个完成端口后,便可开始将套接字句柄与对象关联到一起。但在关联套接字 之前,首先必须创建一个或多个“工作者线程”,以便在I / O请求投递给完成端口对象后,为 完成端口提供服务。在这个时候,大家或许会觉得奇怪,到底应创建多少个线程,以便为完 成端口提供服务呢?这实际正是完成端口模型显得颇为“复杂”的一个方面,因为服务 I / O请 求所需的数量取决于应用程序的总体设计情况。在此要记住的一个重点在于,在我们调用 CreateIoComletionPort时指定的并发线程数量,与打算创建的工作者线程数量相比,它们代表 的并非同一件事情。早些时候,我们曾建议大家用 CreateIoComletionPort函数为每个处理器 都指定一个线程(处理器的数量有多少,便指定多少线程)以避免由于频繁的线程“场景” 交换活动,从而影响系统的整体性能。CreateIoComletionPort函数的NumberOfConcurrentThreads参数明确指示系统:在一个完成端口上,一
次只允许 n个工作者线程运行。假如在完 成端口上创建的工作者线程数量超出 n个,那么在同一时刻,最多只允许 n个线程运行。但实 际上,在一段较短的时间内,系统有可能超过这个值,但很快便会把它减少至事先在 CreateIoComletionPort函数中设定的值。那么,为何实际创建的工作者线程数量有时要比 CreateIoComletionPort函数设定的多一些呢?这样做有必要吗?如先前所述,这主要取决于 应用程序的总体设计情况。假定我们的某个工作者线程调用了一个函数,比如 Sleep或 WaitForSingleObject,但却进入了暂停(锁定或挂起)状态,那么允许另一个线程代替它的位 置。换言之,我们希望随时都能执行尽可能多的线程;当然,最大的线程数量是事先在 CreateIoCompletonPort调用里设定好的。这样一来,假如事先预计到自己的线程有可能暂时 处于停顿状态,那么最好能够创建比 CreateIoCompletonPort的NumberOfConcurrentThreads参数的值多的线程,以便到时候充分发挥系统的潜
力。 一旦在完成端口上拥有足够多的工作者线程来为I / O请求提供服务,便可着手将套接字句柄 同完成端口关联到一起。这要求我们在一个现有的完成端口上,调用CreateIo CompletionPort函 数,同时为前三个参数—FileHandle,ExistingCompletionPort和CompletionKey—提供套 接字的信息。其中,FileHandle参数指定一个要同完成端口关联在一起的套接字句柄。 ExistingCompletionPort参数指定的是一个现有的完成端口。 CompletionKey(完成键)参 数则指定要与某个特定套接字句柄关联在一起的“单句柄数据”;在这个参数中,应用程序 可保存与一个套接字对应的任意类型的信息。之所以把它叫作“单句柄数据”,是由于它只对 应着与那个套接字句柄关联在一起的数据。可将其作为指向一个数据结构的指针,来保存套 接字句柄;在那个结构中,同时包含了套接字的句柄,以及与那个套接字有关的其他信息。 就象本章稍后还会讲述的那样,为完成端口提供服务的线程例程可通过这个参数,取得与套 接字句柄有关的信息。 根据我们到目前为止学到的东西,首先来构建一个基本的应用程序框架。程序清单 8 - 9向 大家阐述了如何使用完成端口模型,来开发一个回应(或“反射”)服务器应用。在这个程序 中,我们基本上按下述步骤行事: 1) 创建一个完成端口。第四个参数保持为0,指定在完成端口上,每个处理器一次只允许 执行一个工作者线程。 2) 判断系统内到底安装了多少个处理器。 3) 创建工作者线程,根据步骤2 )得到的处理器信息,在完成端口上,为已完成的 I / O请求 提供服务。在这个简单的例子中,我们为每个处理器都只创建一个工作者线程。这是由于事 先已预计到,到时不会有任何线程进入“挂起”状态,造成由于线程数量的不足,而使处理 器空闲的局面(没有足够的线程可供执行)。调用CreateThread函数时,必须同时提供一个工 作者例程,由线程在创建好执行。本节稍后还会详细讨论线程的职责。 4) 准备好一个监听套接字,在端口5150上监听进入的连接请求。
5) 使用accept函数,接受进入的连接请求。 6) 创建一个数据结构,用于容纳“单句柄数据”,同时在结构中存入接受的套接字句柄。 7) 调用CreateIoComletionPort,将自accept返回的新套接字句柄同完成端口关联到一起。 通过完成键(CompletionKey)参数,将单句柄数据结构传递给CreateIoComletionPort。 8) 开始在已接受的连接上进行 I / O操作。在此,我们希望通过重叠 I / O机制,在新建的套 接字上投递一个或多个异步 WSARecv或WSASend请求。这些I / O请求完成后,一个工作者线 程会为I / O请求提供服务,同时继续处理未来的 I / O请求,稍后便会在步骤3 )指定的工作者例程 中,体验到这一点。
9) 重复步骤5 ) ~ 8 ),直至服务器中止。 程序清单8-9 完成端口的建立
#include <winsock2.h> #include <windows.h> #include <stdio.h> #define PORT 5150 #define DATA_BUFSIZE 8192 typedef struct { OVERLAPPED Overlapped; WSABUF DataBuf; CHAR Buffer[DATA_BUFSIZE]; DWORD BytesSEND; DWORD BytesRECV; } PER_IO_OPERATION_DATA, * LPPER_IO_OPERATION_DATA; typedef struct { SOCKET Socket; } PER_HANDLE_DATA, * LPPER_HANDLE_DATA; DWORD WINAPI ServerWorkerThread(LPVOID CompletionPortID); void main( void ) { SOCKADDR_IN InternetAddr; SOCKET Listen; SOCKET Accept; HANDLE CompletionPort; SYSTEM_INFO SystemInfo; LPPER_HANDLE_DATA PerHandleData; LPPER_IO_OPERATION_DATA PerIoData; int i; DWORD RecvBytes; DWORD Flags; DWORD ThreadID; WSADATA wsaData; DWORD Ret; if ((Ret = WSAStartup(0x0202, &wsaData)) != 0) { printf( "WSAStartup failed with error %d/n" , Ret); return ; } if ((CompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0)) == NULL) { printf( "CreateIoCompletionPort failed with error: %d/n" , GetLastError()); return ; } GetSystemInfo(&SystemInfo); for (i = 0; i < SystemInfo.dwNumberOfProcessors * 2; i++) { HANDLE ThreadHandle; if ((ThreadHandle = CreateThread(NULL, 0, ServerWorkerThread, CompletionPort, 0, &ThreadID)) == NULL) { printf( "CreateThread() failed with error %d/n" , GetLastError()); return ; } CloseHandle(ThreadHandle); } if ((Listen = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED)) == INVALID_SOCKET) { printf( "WSASocket() failed with error %d/n" , WSAGetLastError()); return ; } InternetAddr.sin_family = AF_INET; InternetAddr.sin_addr.s_addr = htonl(INADDR_ANY); InternetAddr.sin_port = htons(PORT); if (bind(Listen, (PSOCKADDR) &InternetAddr, sizeof (InternetAddr)) == SOCKET_ERROR) { printf( "bind() failed with error %d/n" , WSAGetLastError()); return ; } if (listen(Listen, 5) == SOCKET_ERROR) { printf( "listen() failed with error %d/n" , WSAGetLastError()); return ; } while (TRUE) { if ((Accept = WSAAccept(Listen, NULL, NULL, NULL, 0)) == SOCKET_ERROR) { printf( "WSAAccept() failed with error %d/n" , WSAGetLastError()); return ; } if ((PerHandleData = (LPPER_HANDLE_DATA) GlobalAlloc(GPTR, sizeof (PER_HANDLE_DATA))) == NULL) { printf( "GlobalAlloc() failed with error %d/n" , GetLastError()); return ; } printf( "Socket number %d connected/n" , Accept); PerHandleData->Socket = Accept; if (CreateIoCompletionPort((HANDLE) Accept, CompletionPort, (DWORD) PerHandleData, 0) == NULL) { printf( "CreateIoCompletionPort failed with error %d/n" , GetLastError()); return ; } if ((PerIoData = (LPPER_IO_OPERATION_DATA) GlobalAlloc(GPTR, sizeof (PER_IO_OPERATION_DATA))) == NULL) { printf( "GlobalAlloc() failed with error %d/n" , GetLastError()); return ; } ZeroMemory(&(PerIoData->Overlapped), sizeof (OVERLAPPED)); PerIoData->BytesSEND = 0; PerIoData->BytesRECV = 0; PerIoData->DataBuf.len = DATA_BUFSIZE; PerIoData->DataBuf.buf = PerIoData->Buffer; Flags = 0; if (WSARecv(Accept, &(PerIoData->DataBuf), 1, &RecvBytes, &Flags, &(PerIoData->Overlapped), NULL) == SOCKET_ERROR) { if (WSAGetLastError() != ERROR_IO_PENDING) { printf( "WSARecv() failed with error %d/n" , WSAGetLastError()); return ; } } } } DWORD WINAPI ServerWorkerThread(LPVOID CompletionPortID) { HANDLE CompletionPort = (HANDLE) CompletionPortID; DWORD BytesTransferred; LPOVERLAPPED Overlapped; LPPER_HANDLE_DATA PerHandleData; LPPER_IO_OPERATION_DATA PerIoData; DWORD SendBytes, RecvBytes; DWORD Flags; while (TRUE) { if (GetQueuedCompletionStatus(CompletionPort, &BytesTransferred, (LPDWORD)&PerHandleData, (LPOVERLAPPED *) &PerIoData, INFINITE) == 0) { printf( "GetQueuedCompletionStatus failed with error %d/n" , GetLastError()); return 0; } if (BytesTransferred == 0) { printf( "Closing socket %d/n" , PerHandleData->Socket); if (closesocket(PerHandleData->Socket) == SOCKET_ERROR) { printf( "closesocket() failed with error %d/n" , WSAGetLastError()); return 0; } GlobalFree(PerHandleData); GlobalFree(PerIoData); continue ; } if (PerIoData->BytesRECV == 0) { PerIoData->BytesRECV = BytesTransferred; PerIoData->BytesSEND = 0; } else { PerIoData->BytesSEND += BytesTransferred; } if (PerIoData->BytesRECV > PerIoData->BytesSEND) { ZeroMemory(&(PerIoData->Overlapped), sizeof (OVERLAPPED)); PerIoData->DataBuf.buf = PerIoData->Buffer + PerIoData->BytesSEND; PerIoData->DataBuf.len = PerIoData->BytesRECV - PerIoData->BytesSEND; if (WSASend(PerHandleData->Socket, &(PerIoData->DataBuf), 1, &SendBytes, 0, &(PerIoData->Overlapped), NULL) == SOCKET_ERROR) { if (WSAGetLastError() != ERROR_IO_PENDING) { printf( "WSASend() failed with error %d/n" , WSAGetLastError()); return 0; } } } else { PerIoData->BytesRECV = 0; Flags = 0; ZeroMemory(&(PerIoData->Overlapped), sizeof (OVERLAPPED)); PerIoData->DataBuf.len = DATA_BUFSIZE; PerIoData->DataBuf.buf = PerIoData->Buffer; if (WSARecv(PerHandleData->Socket, &(PerIoData->DataBuf), 1, &RecvBytes, &Flags, &(PerIoData->Overlapped), NULL) == SOCKET_ERROR) { if (WSAGetLastError() != ERROR_IO_PENDING) { printf( "WSARecv() failed with error %d/n" , WSAGetLastError()); return 0; } } } } }