完成端口中ConnectEx的问题:
完成端口用于客户端时一定要注意,创建的SOCKET要先随便绑定一个端口(默认0自动分配也可),然后再扔到完成端口中去,之后才可以ConnectEx,否则(不绑定),ConnectEx会出现10022错误。同时,getpeername似乎对ConnectEx不支持,也可能有其他细节设置我暂时不知道,返回的远程地址是无效的(仅对于完成端口用于服务器accept回来的SOCKET有效,对用于客户的connet也有效)。不过对程序没有太大影响,既然是自己要主动连出去,远程地址当然自己早就心里明白了,自己保存一份即可,不必getpeername。备注:getsockname返回本地地址一直是有效的。
getpeername之所以取不到正确的内容,是因为ConnectEx返回后,socket相关的属性还没有更新(ConnectEx的特性所导致),你应该调用一下setsockopt更新socket的属性后再调用getpeername。参数是SOL_SOCKET和SO_UPDATE_CONNECT_CONTEXT。
When the ConnectEx function returns, the socket s is in the default state for a connected socket. The socket s does not enable previously set properties or options until SO_UPDATE_CONNECT_CONTEXT is set on the socket. Use the setsockopt function to set the SO_UPDATE_CONNECT_CONTEXT option.
For example:
err = setsockopt(s, SOL_SOCKET, SO_UPDATE_CONNECT_CONTEXT, NULL, 0);
完成端口与ACE框架:
平台不同,使用的ACE框架还是有所差别的。比如windows下面的服务器端,一般都用Proactor框架,配合各种异步操作,如ACE_Asynch_Acceptor/ACE_Asynch_Connector,因为Proactor内部实现是完成端口,在windows平台上,公认可以取得最好的性能。
如果你用Linux,服务器端推荐你使用Reactor框架+Dev_Poll_Reactor实现,这个实现使用了Epoll机制,性能很棒。
客户端,一般为了兼容性考虑,都用Reactor,当然,如果是在windows上面运行,默认实现是WFMO_Reactor。
IOCP(I/O completion port,I/O完成端口)是伸缩性最好的一种I/O模型。本章将具体讨论完成端口的概念和它的用法,讲述可伸缩性服务器的体系结构,最后结合实例介绍使用IOCP进行可伸缩服务器程序设计的过程。
当应用程序必须一次管理多个套接字时,完成端口模型提供了最好的系统性能。这个模型也提供了最好的伸缩性,它非常适合用来处理上百、上千个套接字。IOCP技术广泛应用于各种类型的高性能服务器,如Apache等。本节将结合一个简单的例子详细讨论它的用法。
I/O完成端口是应用程序使用线程池处理异步I/O请求的一种机制。处理多个并发异步I/O请求时,使用I/O完成端口比在I/O请求时创建线程更快更有效。
I/O完成端口最初的设计是应用程序发出一些异步I/O请求,当这些请求完成时,设备驱动将把这些工作项目排序到完成端口,这样,在完成端口上等待的线程池便可以处理这些完成I/O。完成端口实际上是一个Windows I/O结构,它可以接收多种对象的句柄,如文件对象、套接字对象等。本节仅讲述使用完成端口模型管理套接字的方法。
使用完成端口模型,首先要调用CreateIoCompletionPort函数创建一个完成端口对象,Winsock将使用这个对象为任意数量的套接字句柄管理I/O请求。函数定义如下:
HANDLE CreateIoCompletionPort(HANDLE FileHandle,
HANDLE ExistingCompletionPort, ULONG_PTR CompletionKey, DWORD NumberOfConcurrentThreads);
在详细解释函数参数之前,笔者先介绍此函数的两个不同功能。
(1)创建一个完成端口对象。
(2)将一个或者多个文件句柄(这里是套接字句柄)关联到I/O完成端口对象。
最初创建完成端口对象时,唯一需要设置的参数是NumberOfConcurrentThreads,它定义了允许在完成端口上同时执行的线程的数量。理想情况下,我们希望每个处理器仅运行一个线程来为完成端口提供服务,以避免线程上下文切换。NumberOfConcurrentThreads为0表示系统允许的线程数量与处理器数量一样多。因此,可以简单的使用以下代码创建完成端口对象,取得标识完成端口的句柄。
HANDLE hCompletion = ::CreateIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0, 0);
成功创建完成端口对象之后,便可以向这个对象关联套接字句柄了。在关联套接字之前,需要先创建一个或者多个工作线程(称为I/O服务线程),在完成端口上执行并处理投递到完成端口上的I/O请求。这里的关键问题是要创建多少个工作线程。要注意,创建完成端口时指定的线程数量和这里要创建的线程数量不是一回事。前面我们推荐线程数量为处理器的数量,以避免上下文切换。CreateIoCompletionPort 函数的NumberOfConcurrentThreads参数明确告诉系统允许在完成端口上同时运行的线程数量。如果创建的线程多于NumberOfConcurrent Threads,也就仅有NumberOfConcurrentThreads个线程允许运行。但是有的时候,确实需要创建更多的线程,这主要取决于程序的总体设计。如果某个线程调用了一个函数,如Sleep或WaitForSingleObject,进入了暂停状态,多出来的线程中就会有一个开始运行,占据休眠线程的位置。总而言之,我们总是希望在完成端口上参加I/O处理工作的线程和CreateIoCompletionPort函数指定的线程一样多。最后的结论是,如果你觉得工作线程会遇到阻塞(进入暂停状态),那就应该创建比CreateIoCompletionPort指定的数量还要多的线程。
有了足够的工作线程来处理完成端口上的I/O请求之后,就该为完成端口关联套接字句柄了,这就用到了CreateIoCompletionPort函数的前3个参数。
l FileHandle 要关联的套接字句柄
l ExistingCompletionPort 上面创建的完成端口对象句柄
l CompletionKey 指定一个句柄唯一(per-handle)数据,它将与FileHandle套接字句柄 关联在一起。应用程序可以在此存储任意类型的信息,通常是一个指针
CompletionKey参数通常用来描述与套接字相关的信息,所以称它为句柄唯一(per-handle)数据。在后面的例子代码中,可以看到它的作用。
向完成端口关联套接字句柄之后,便可以通过在套接字上投递重叠发送和接收请求处理I/O了。在这些I/O操作完成时,I/O系统会向完成端口对象发送一个完成通知封包。I/O完成端口以先进先出的方式为这些封包排队。应用程序使用GetQueuedCompletionStatus函数可以取得这些队列中的封包。这个函数应该在处理完成对象I/O的服务线程中调用。
BOOL GetQueuedCompletionStatus(
HANDLE CompletionPort, // 完成端口对象句柄
LPDWORD lpNumberOfBytes, // 取得I/O操作期间传输的字节数
PULONG_PTR lpCompletionKey, // 取得在关联套接字时指定的句柄唯一数据
LPOVERLAPPED* lpOverlapped, // 取得投递I/O操作时指定的OVERLAPPED结构
DWORD dwMilliseconds // 如果完成端口没有完成封包,此参数指定了等待的事件,INFINITE为无穷大
);
I/O服务线程调用GetQueuedCompletionStatus函数取得有事件发生的套接字的信息,通过lpNumberOfBytes 参数得到传输的字节数量,通过lpCompletionKey参数得到与套接字关联的句柄唯一(per-handle)数据,通过lpOverlapped参数得到投递I/O请求时使用的重叠对象地址,进一步得到I/O唯一(per-I/O)数据。
这些参数中,最重要的是per-handle数据和per-I/O数据。
lpCompletionKey参数包含了我们称为per-handle的数据,因为当套接字第一次与完成端口关联时,这个数据就关联到了一个套接字句柄。这是传递给CreateIoCompletionPort函数的CompletionKey参数。如前所述,可以给这个参数传递任何类型的数据。
lpOverlapped参数指向一个OVERLAPPED结构,结构后面便是我们称为per-I/O的数据,这可以是工作线程处理完成封包时想要知道的任何信息。
下面是一个简单的使用IOCP模型的TCP服务器例子,它仅打印出从客户端接收到的数据。后面还要在这个例子的基础上设计高性能、可伸缩的服务器类CIOCPServer。
例子中有两种类型的线程-主线程和它创建的线程。主线程创建监听套接字,创建额外的工作线程,关联IOCP,负责等待和接受到来的连接等。由主线程创建的线程负责处理I/O事件,这些线程调用GetQueuedCompletionStatus函数在完成端口对象上等待完成的I/O操作。
GetQueuedCompletionStatus函数返回后,说明发生了如下事件之一。
(1)GetQueuedCompletionStatus调用失败,说明在此套接字上有错误发生。
(2)BytesTransferred为0说明套接字被对方关闭。注意,per-handle数据用来引用与I/O操作相关的套接字。
(3)I/O请求成功完成。通过per-I/O数据(这是程序自定义的结构)中的OperationType域查看哪个I/O请求完成了。
程序首先定义了per-handle数据和per-I/O操作数据的结构类型。
// 初始化Winsock库
CInitSock theSock;
#define BUFFER_SIZE 1024
typedef struct _PER_HANDLE_DATA // per-handle数据
{
SOCKET s; // 对应的套接字句柄
sockaddr_in addr; // 客户方地址
} PER_HANDLE_DATA, *PPER_HANDLE_DATA;
typedef struct _PER_IO_DATA // per-I/O数据
{
OVERLAPPED ol; // 重叠结构
char buf[BUFFER_SIZE]; // 数据缓冲区
int nOperationType; // 操作类型
#define OP_READ 1
#define OP_WRITE 2
#define OP_ACCEPT 3
} PER_IO_DATA, *PPER_IO_DATA;
主线程首先创建完成端口对象,创建工作线程处理完成端口对象中的事件;然后再创建监听套接字,开始监听服务端口;接下来便进入无限循环,处理到来的连接请求,这个过程如下:
(1)调用accept函数等待接受未决的连接请求。
(2)接受到新连接之后,为它创建一个per-handle数据,并将它们关联到完成端口对象。
(3)在新接受的套接字上投递一个接收请求。这个I/O完成之后,由工作线程负责处理。
下面是具体的实现代码。
void main()
{
int nPort = 4567;
// 创建完成端口对象,创建工作线程处理完成端口对象中事件
HANDLE hCompletion = ::CreateIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0, 0);
::CreateThread(NULL, 0, ServerThread, (LPVOID)hCompletion, 0, 0);
// 创建监听套接字,绑定到本地地址,开始监听
SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, 0);
SOCKADDR_IN si;
si.sin_family = AF_INET;
si.sin_port = ::ntohs(nPort);
si.sin_addr.S_un.S_addr = INADDR_ANY;
::bind(sListen, (sockaddr*)&si, sizeof(si));
::listen(sListen, 5);
// 循环处理到来的连接
while(TRUE)
{ // 等待接受未决的连接请求
SOCKADDR_IN saRemote;
int nRemoteLen = sizeof(saRemote);
SOCKET sNew = ::accept(sListen, (sockaddr*)&saRemote, &nRemoteLen);
// 接受到新连接之后,为它创建一个per-handle数据,并将它们关联到完成端口对象。
PPER_HANDLE_DATA pPerHandle =
(PPER_HANDLE_DATA)::GlobalAlloc(GPTR, sizeof(PER_HANDLE_DATA));
pPerHandle->s = sNew;
memcpy(&pPerHandle->addr, &saRemote, nRemoteLen);
::CreateIoCompletionPort((HANDLE)pPerHandle->s, hCompletion, (DWORD)pPerHandle, 0);
// 投递一个接收请求
PPER_IO_DATA pPerIO = (PPER_IO_DATA)::GlobalAlloc(GPTR, sizeof(PER_IO_DATA));
pPerIO->nOperationType = OP_READ;
WSABUF buf;
buf.buf = pPerIO->buf;
buf.len = BUFFER_SIZE;
DWORD dwRecv;
DWORD dwFlags = 0;
::WSARecv(pPerHandle->s, &buf, 1, &dwRecv, &dwFlags, &pPerIO->ol, NULL);
}
}
I/O服务线程循环调用GetQueuedCompletionStatus函数从I/O完成端口移除完成的I/O封包,然后根据封包的类型进行处理。具体程序代码如下:
DWORD WINAPI ServerThread(LPVOID lpParam)
{ // 得到完成端口对象句柄
HANDLE hCompletion = (HANDLE)lpParam;
DWORD dwTrans;
PPER_HANDLE_DATA pPerHandle;
PPER_IO_DATA pPerIO;
while(TRUE)
{ // 在关联到此完成端口的所有套接字上等待I/O完成
BOOL bOK = ::GetQueuedCompletionStatus(hCompletion,
&dwTrans, (LPDWORD)&pPerHandle, (LPOVERLAPPED*)&pPerIO, WSA_INFINITE);
if(!bOK) // 在此套接字上有错误发生
{ ::closesocket(pPerHandle->s);
::GlobalFree(pPerHandle);
::GlobalFree(pPerIO);
continue;
}
if(dwTrans == 0 && // 套接字被对方关闭
(pPerIO->nOperationType == OP_READ || pPerIO->nOperationType == OP_WRITE))
{ ::closesocket(pPerHandle->s);
::GlobalFree(pPerHandle);
::GlobalFree(pPerIO);
continue;
}
switch(pPerIO->nOperationType) // 通过per-I/O数据中的nOperationType域查看什么I/O请求完成了
{
case OP_READ: // 完成一个接收请求
{ pPerIO->buf[dwTrans] = '/0';
printf(pPerIO -> buf);
// 继续投递接收I/O请求
WSABUF buf;
buf.buf = pPerIO->buf ;
buf.len = BUFFER_SIZE;
pPerIO->nOperationType = OP_READ;
DWORD nFlags = 0;
::WSARecv(pPerHandle->s, &buf, 1, &dwTrans, &nFlags, &pPerIO->ol, NULL);
}
break;
case OP_WRITE: // 本例中没有投递这些类型的I/O请求
case OP_ACCEPT:
break;
}
}
return 0;
}
4.1.3小节的例子中没有涉及如何恰当地关闭I/O完成端口,特别是当有多个线程在套接字上执行I/O的时候。主要要避免的事情是当重叠操作正在进行的时候释放它的OVERLAPPED结构,阻止其发生的最好的方法是在每个套接字句柄上调用closesocket函数—所有未决的重叠I/O操作都会完成。一旦所有的套接字句柄关闭,就该终止完成端口上处理I/O的工作线程了。这可以通过使用PostQueuedCompletionStatus函数向工作线程发送特定的完成封包来实现,这个完成封包通知工作线程立即退出。PostQueuedCompletionStatus函数定义如下:
BOOL PostQueuedCompletionStatus(
HANDLE CompletionPort, // 完成端口对象句柄
DWORD dwNumberOfBytesTransferred, // 指定GetQueuedCompletionStatus函数的
// lpNumberOfBytesTransferred参数的返回值
ULONG_PTR dwCompletionKey, // 指定GetQueuedCompletionStatus函数的lpCompletionKey参数的返回值
LPOVERLAPPED lpOverlapped // 指定GetQueuedCompletionStatus函数的lpOverlapped参数的返回值
);
当工作线程接收到GetQueuedCompletionStatus的3个参数时,可以决定是否应该退出。例如,可以在向dwCompletionKey参数传递0,所有的工作线程都退出之后,可以使用CloseHandle关闭完成端口。
完成端口是到现在为止在性能和可伸缩性方面表现最好的I/O模型。关联到完成端口对象的套接字的数量并没有限制,仅需要少量的线程来处理完成I/O。本章后面将具体讨论如何使用完成端口开发可伸缩的高性能服务器程序。