Winsock 完成端口模型简介
一个工作者线程从GetQueuedCompletionStatus这个API调用接收到I/O完成通知后,在lpCompletionKey和lpOverlapped参数中,会包含一些必要的套接字信息。利用这些信息,可通过完成端口,继续在一个套接字上的I/O处理。通过这些参数,可获得两方面重要的套接字数据:单句柄数据,以及单I/O操作数据。其中, lpCompletionKey参数包含了“单句柄数据”,因为在一个套接字首次与完成端口关联到一起的时候,那些数据便与一个特定的套接字句柄对应起来了。这些数据正是我们在进行CreateIoCompletionPort API调用的时候,通过CompletionKey参数传递的。如早先所述,应用程序可通过该参数传递任意类型的数据。通常情况下,应用程序会将与I/O 请求有关的套接字句柄保存在这里。lpOverlapped参数则包含了一个OVERLAPPED结构,在它后面跟随“单I/O操作数据”。我们的工作者线程处理一个完成数据包时(将数据原封不动打转回去,接受连接,投递另一个线程,等等),这些信息是它必须要知道的。单I/O操作数据可以是追加到一个 OVERLAPPED结构末尾的、任意数量的字节。假如一个函数要求用到一个OVERLAPPED结构,我们便必须将这样的一个结构传递进去,以满足它的要求。要想做到这一点,一个简单的方法是定义一个结构,然后将OVERLAPPED结构作为新结构的第一个元素使用。举个例子来说,可定义下述数据结构,实现对单I/O操作数据的管理:
{
OVERLAPPED Overlapped;
WSABUF DataBuf;
CHAR Buffer[DATA_BUFSIZE];
BOOL OperationType;
}PER_IO_OPERATION_DATA
该结构演示了通常要与I/O操作关联在一起的某些重要数据元素,比如刚才完成的那个I/O操作的类型(发送或接收请求)。在这个结构中,我们认为用于已完成 I/O操作的数据缓冲区是非常有用的。要想调用一个Winsock API函数,同时为其分配一个OVERLAPPED结构,既可将自己的结构“造型”为一个OVERLAPPED指针,亦可简单地撤消对结构中的 OVERLAPPED元素的引用。如下例所示:
// 可像下面这样调用一个函数
WSARecv(socket, ..., (OVERLAPPED *)&PerIoData);
// 或像这样
WSARecv(socket, ..., &(PerIoData.Overlapped));
在工作线程的后面部分,等GetQueuedCompletionStatus函数返回了一个重叠结构(和完成键)后,便可通过撤消对 OperationType成员的引用,调查到底是哪个操作投递到了这个句柄之上(只需将返回的重叠结构造型为自己的 PER_IO_OPERATION_DATA结构)。对单I/O操作数据来说,它最大的一个优点便是允许我们在同一个句柄上,同时管理多个I/O操作(读 /写,多个读,多个写,等等)。大家此时或许会产生这样的疑问:在同一个套接字上,真的有必要同时投递多个I/O操作吗?答案在于系统的“伸缩性”,或者说“扩展能力”。例如,假定我们的机器安装了多个中央处理器,每个处理器都在运行一个工作者线程,那么在同一个时
候,完全可能有几个不同的处理器在同一个套接字上,进行数据的收发操作。
HANDLE CompletionPort,
DWORD dwNumberOfBytesTransferred,
ULONG_PTR dwCompletionKey,
LPOVERLAPPED lpOverlapped
);
其中,CompletionPort参数指定想向其发送一个完成数据包的完成端口对象。而就dwNumberOfBytesTransferred、 dwCompletionKey和lpOverlapped这三个参数来说,每一个都允许我们指定一个值,直接传递给 GetQueuedCompletionStatus函数中对应的参数。这样一来,一个工作者线程收到传递过来的三个 GetQueuedCompletionStatus函数参数后,便可根据由这三个参数的某一个设置的特殊值,决定何时应该退出。例如,可用 dwCompletionPort参数传递0值,而一个工作者线程会将其解释成中止指令。一旦所有工作者线程都已关闭,便可使用CloseHandle函数,关闭完成端口,最终安全退出程序。