首页 > 代码库 > Socket编程模型之完成端口模型

Socket编程模型之完成端口模型

转载请注明来源:http://blog.csdn.net/caoshiying?viewmode=contents


一、回顾重叠IO模型



用完成例程来实现重叠I/O比用事件通知简单得多。在这个模型中,主线程只用不停的接受连接即可;辅助线程判断有没有新的客户端连接被建立,如果有,就为那个客户端套接字激活一个异步的WSARecv操作,然后调用SleepEx使线程处于一种可警告的等待状态,以使得I/O完成后CompletionROUTINE可以被内核调用。如果辅助线程不调用SleepEx,则内核在完成一次I/O操作后,无法调用完成例程(因为完成例程的运行应该和当初激活WSARecv异步操作的代码在同一个线程之内)。


完成例程内的实现代码比较简单,它取出接收到的数据,然后将数据原封不动的发送给客户端,最后重新激活另一个WSARecv异步操作。注意,在这里用到了“尾随数据”。我们在调用WSARecv的时候,参数lpOverlapped实际上指向一个比它大得多的结构PER_IO_OPERATION_DATA,这个结构除了WSAOVERLAPPED以外,还被我们附加了缓冲区的结构信息,另外还包括客户端套接
字等重要的信息。这样,在完成例程中通过参数lpOverlapped拿到的不仅仅是WSAOVERLAPPED结构,还有后边尾随的包含客户端套接字和接收数据缓冲区等重要信息。这样的C语言技巧在我介绍完成端口的时候还会使用到。


二、完成端口模型



“完成端口”模型是迄今为止最为复杂的一种I/O模型。然而,假若一个应用程序同时需要管理为数众多的套接字,那么采用这种模型,往往可以达到最佳的系统性能!但不幸的是,该模型只适用于Windows NT和Windows 2000操作系统。因其设计的复杂性,只有在你的应用程序需要同时管理数百乃至上千个套接字的时候,而且希望随着系统内安装的CPU数量的增多,应用程序的性能也可以线性提升,才应考虑采用“完成端口”模型。要记住的一个基本准则是,假如要为Windows NT或Windows 2000开发高性能的服务器应用,同时希望为大量套接字I/O请求提供服务(Web服务器便是这方面的典型例子),那么I/O完成端口模型便是最佳选择!


完成端口模型是我最喜爱的一种模型。虽然其实现比较复杂(其实我觉得它的实现比用事件通知实现的重叠I/O简单多了),但其效率是惊人的。我在T公司的时候曾经帮同事写过一个邮件服务器的性能测试程序,用的就是完成端口模型。结果表明,完成端口模型在多连接(成千上万)的情况下,仅仅依靠一两个辅助线程,就可以达到非常高的吞吐量。


三、关键函数



1、CreateIoCompletionPort



创建一个输入/输出(I / O)完成端口,并将其与一个指定的文件句柄关联,或者创建一个尚未与文件句柄关联的I / O完成端口,允许在稍后的时间关联。将已打开的文件句柄的实例与一个I / O完成端口关联,允许一个进程接收包含该文件句柄的异步I / O操作完成的通知。注意:这里所使用的术语文件句柄是指代表一个重叠的I / O端点的系统抽象,而不仅仅是磁盘上的一个文件。任何系统对象支持重叠I / o-such网络端点,TCP套接字,命名管道、邮件槽可以作为文件句柄。


函数原型:


HANDLE WINAPI CreateIoCompletionPort(
  _In_     HANDLE    FileHandle,
  _In_opt_ HANDLE    ExistingCompletionPort,
  _In_     ULONG_PTR CompletionKey,
  _In_     DWORD     NumberOfConcurrentThreads
);




函数参数:


FileHandle:一个打开的文件句柄或者INVALID_HANDLE_VALUE。这个文件句柄必须是支持重叠IO的object。如果提供了句柄, 它必须是已经给重叠I/O模型完成端口打开的句柄。例如,如果您使用CreateFile函数获取的句柄,那么您在调用这个函数时必须在参数中指定FILE_FLAG_OVERLAPPED旗标。如果指定 INVALID_HANDLE_VALUE,那么函数将创建一个没有关联文件句柄的IO完成端口模型,此外ExistingCompletionPort参数必须设为NULL,CompletionKey参数将被忽略。


ExistingCompletionPort:是已经存在的完成端口。如果为NULL,则为新建一个IOCP。


CompletionKey:用户定义的句柄包含的I/O完成包信息。当FileHandle被设为INVALID_HANDLE_VALUE时此参数被忽略。


NumberOfConcurrentThreads:操作系统可以允许同时处理I / O完成端口的I / O完成数据包的线程的最大数目。如果existingcompletionport参数不为空,则忽略此参数。如果这个参数为零,系统允许多个并发运行的线程,因为系统中有处理器。


返回值:


如果函数成功,返回值是一个I / O完成端口的句柄:如果ExistingCompletionPort参数为空,返回值是一个新的处理。如果ExistingCompletionPort参数是一个有效的I/O完成端口句柄,返回值是相同的处理。如果文件句柄参数是一个有效的处理,文件处理是现在与返回的I/O完成端口。如果函数失败,返回值为空。为了获得更多的错误信息,调用GetLastError函数。


2、GetQueuedCompletionStatus



失望的是微软官方MSDN没有提供关于这个API的说明。以下参照一篇英文文档进行翻译。文档说这个函数试图将一个I/O完成包从指定的I/O完成端口。如果没有完成数据包队列,则函数等待一个挂起的I / O操作与完成端口相关联的完成。


函数原型:


BOOL WINAPI GetQueuedCompletionStatus(
  _In_  HANDLE       CompletionPort,
  _Out_ LPDWORD      lpNumberOfBytes,
  _Out_ PULONG_PTR   lpCompletionKey,
  _Out_ LPOVERLAPPED *lpOverlapped,
  _In_  DWORD        dwMilliseconds
);




函数参数:


CompletionPort:完成端口的句柄。创建一个完成端口,使用CreateIoCompletionPort函数。


lpNumberOfBytes:指向已完成的I / O操作期间传输的字节数的变量的指针。


lpCompletionKey:指向与文件句柄关联的完成键的变量的指针,该键的I / O操作已完成。一个完成的关键是每一个文件的关键,是指定一个叫CreateIoCompletionPort。


lpOverlapped:一个指向一个变量的指针,该指针指向在已完成的I / O操作开始时指定的重叠结构的地址的变量。即使您已经通过了一个与完成端口相关联的文件句柄和一个有效的重叠结构,应用程序也可以防止完成端口通知。这是通过指定的重叠结构的hevent成员有效的事件处理完成,并设置其低阶位。一个有效的事件句柄,其低阶位设置将保持I / O完成从被队列到完成端口。


dwMilliseconds:调用方愿意等待完成数据包出现在完成端口的毫秒数。如果一个完成包没有出现在指定的时间内,功能倍出,返回false,并设置*lpOverlapped为null。如果该参数是无限的,函数将没有时间了。如果该参数为零,没有I/O操作中出列,函数将取消等待时间,立即操作。


返回值:


返回非零(真),如果成功或零(假),否则。为了获得更多的错误信息,调用GetLastError。


此功能将一个线程与指定的完成端口关联。一个线程可以与至多一个完成端口相关联的。如果因为完成端口句柄与它是封闭而调用调用GetQueuedCompletionStatus突出失败,函数返回false,*lpOverlapped会是空的,GetLastError将返回error_abandoned_wait_0。

Windows Server 2003和Windows XP:关闭完成端口句柄,调用优秀不会导致之前的行为。该函数将继续等待直到一项是从港口或直到发生超时删除,如果指定以外的无限价值。

如果GetQueuedCompletionStatus函数调用成功,它出列完成包一个成功的I/O操作完成端口和存储信息的变量所指向的下列参数:lpNumberOfBytes,lpcompletionkey,和lpOverlapped。在失败(返回值是错误的),这些相同的参数可以包含特定的值组合如下:
如果*lpOverlapped为空,功能没有出列完成包从完成端口。在这种情况下,函数不存储信息在lpNumberOfBytes and lpCompletionKey所指向的参数中,其值是不确定的。
如果*lpOverlapped不空和功能按一个失败的I/O操作的完成端口完成包的功能,存储信息有关失败操作的变量所指向的lpcompletionkey lpOverlapped lpNumberOfBytes。为了获得更多的错误信息,调用GetLastError。


3、PostQueuedCompletionStatus



将一个I / O完成数据包发送到一个I / O完成端口。I/O完成包将满足一个优秀的调用GetQueuedCompletionStatus函数。该函数返回三值传递的第二,第三,和第四个参数postqueuedcompletionstatus呼叫。该系统不使用或验证这些值。特别是,lpOverlapped参数不需要点的重叠结构。


函数原型:


BOOL WINAPI PostQueuedCompletionStatus(
  _In_     HANDLE       CompletionPort,
  _In_     DWORD        dwNumberOfBytesTransferred,
  _In_     ULONG_PTR    dwCompletionKey,
  _In_opt_ LPOVERLAPPED lpOverlapped
);




参数:


CompletionPort:一个I / O完成数据包的I / O完成端口的句柄。


dwNumberOfBytesTransferred:要通过lpnumberofbytestransferred参数GetQueuedCompletionStatus函数返回的值。0xFFFFFFFF表示处理所有尾随数据。只有准备关闭端口的时候才这样做。


dwCompletionKey:可以通过GetQueuedCompletionStatus函数返回的值lpcompletionkey参数。


lpOverlapped:要通过lpOverlapped参数GetQueuedCompletionStatus函数返回的值。


返回值:


如果函数成功,返回值是非零的。如果函数失败,返回值为零。为了获得更多的错误信息,调用GetLastError。


四、完整的示例程序



接着上面几篇Socket文章写,关于公共代码与反射式客户端请参见:Socket编程模型之简单选择模型。下面是新建的overlapped_server工程,新建了一个overlapped_server_manager类型,继承自iserver_manager接口,头文件完整代码如下:


#pragma once

#define SOCKET_MESSAGE_SIZE 1024

#include <WinSock2.h>
#include <common_callback.h>

typedef enum
{
	RECV_POSTED
}OPERATION_TYPE;

typedef struct
{
	WSAOVERLAPPED overlap;
	WSABUF buffer;
	char message[SOCKET_MESSAGE_SIZE];
	DWORD received_count;
	DWORD flags;
	OPERATION_TYPE operation_type;
}PEERIO_OPERATION_DATA, *LPPEERIO_OPERATION_DATA;

class completeio_server_manager:
	public iserver_manager
{
private:
	int iport;
	int iaddr_size;
	common_callback callback;
	BOOL brunning;
	SOCKET server;
	WSADATA wsaData;
	HANDLE hcomplete_port;
	SYSTEM_INFO system_info;
	LPPEERIO_OPERATION_DATA peer_data;
	bool bdisposed;

protected:
	bool accept_by_crt();
	bool accept_by_winapi();

public:
	void receive();
	void shutdown();
	void start_receive();
	void start_accept();

public:
	completeio_server_manager();
	virtual ~completeio_server_manager();
};




实现文件完整代码如下:

#include "completeio_server_manager.h"
#include <stdio.h>
#include <tchar.h>

completeio_server_manager::completeio_server_manager()
{
	iport = 5150;
	iaddr_size = sizeof(SOCKADDR_IN);
	brunning = FALSE;
	GetSystemInfo(&system_info);
	callback.set_manager(this);
	callback.set_receive_thread_coount(system_info.dwNumberOfProcessors);
	hcomplete_port = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
	bdisposed = false;
}


completeio_server_manager::~completeio_server_manager()
{
	if (bdisposed)
		shutdown();
}

bool completeio_server_manager::accept_by_crt()
{

	return true;
}

bool completeio_server_manager::accept_by_winapi()
{
	SOCKADDR_IN server_addr;
	SOCKADDR_IN client_addr;
	SOCKET client;
	LPPEERIO_OPERATION_DATA peer_data;
	int iresult = -1;

	WSAStartup(MAKEWORD(2, 2), &wsaData);
	server = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	server_addr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
	server_addr.sin_family = AF_INET;
	server_addr.sin_port = htons(iport);
	do
	{
		iresult = bind(server, (struct sockaddr*)&server_addr, iaddr_size);
		if (iresult == SOCKET_ERROR)
		{
			iport++;
			server_addr.sin_port = htons(iport);
		}
	} while (iresult == -1);
	listen(server, 3);
	printf("基于完成端口模型的Socket服务器启动成功。监听端口是:%d\n", iport);
	while (brunning)
	{
		printf("开始监听请求。\n");
		client = accept(server, (struct sockaddr*)&client_addr, &iaddr_size);
		if (client == SOCKET_ERROR)
			continue;
		printf("新客户端连接:%s:%d\n", inet_ntoa(client_addr.sin_addr), htons(client_addr.sin_port));
		CreateIoCompletionPort((HANDLE)client, hcomplete_port, (DWORD)client, 0);
		peer_data = http://www.mamicode.com/(LPPEERIO_OPERATION_DATA)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(PEERIO_OPERATION_DATA));>


五、效果


技术分享


六、心得体会



成功创建一个完成端口后,便可开始将套接字句柄与对象关联到一起。但在关联套接字之前,首先必须创建一个或多个“工作者线程”,以便在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(完成键)参数则指定要与某个特定套接字句柄关联在一起的“单句柄数据”;在这个参数中,应用程序可保存与一个套接字对应的任意类型的信息。之所以把它叫作“单句柄数据”,是由于它只对应着与那个套接字句柄关联在一起的数据。可将其作为指向一个数据结构的指针,来保存套接字句柄;在那个结构中,同时包含了套接字的句柄,以及与那个套接字有关的其他信息。

Socket编程模型之完成端口模型