正文

基于完成端口的Winsock程序设计2009-06-15 12:53:00

【评论】 【打印】 【字体: 】 本文链接:http://blog.pfan.cn/xman/44366.html

分享到:

关于完成端口的概念及内部机制,参考译文《深度探索I/O完成端口》。

完成端口对象取代了WSAAsyncSelect中的消息驱动和WSAEventSelect中的事件对象,当然完成端口模型的内部机制要比WSAAsyncSelectWSAEventSelect模型复杂得多。

IOCP内部机制如下图所示:

Winsock中编写完成端口程序,首先要调用CreateIoCompletionPort函数创建完成端口其原型如下:

WINBASEAPI HANDLE WINAPI

CreateIoCompletionPort(

       HANDLE FileHandle,

       HANDLE ExistingCompletionPort,

       DWORD CompletionKey,

       DWORD NumberOfConcurrentThreads );

第一次调用此函数创建一个完成端口时,通常只关注NumberOfConcurrentThreads,它定义了在完成端口上同时允许执行的线程数量。一般设为0,表示系统内安装了多少个处理器,便允许同时运行多少个线程为完成端口提供服务。每个处理器各自负责一个线程的运行,避免了过于频繁的线程上下文切换。

hCompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0)

这个类比重叠I/O事件通知模型中(WSA)CreateEvent 

然后再调用GetSystemInfo(&SystemInfo);取得系统安装的处理器的个数SystemInfo.dwNumberOfProcessors,根据CPU创建线程池,在完成端口上,为已完成的I/O请求提供服务。一般线程池的规模,即线程数 = CPU * 2 + 2

下面的代码片段演示了线程池的创建。

// 创建线程池,规模为CPU数的两倍

  for(int i = 0; i < SystemInfo.dwNumberOfProcessors * 2; i++)

   {

      HANDLE ThreadHandle;

      // 创建一个工作线程,并将完成端口作为参数传递给它。

      if ((ThreadHandle = CreateThread(NULL, 0, WorkerThread, hCompletionPort,

         0, &ThreadID)) == NULL)

      {

         printf("CreateThread() failed with error %d\n", GetLastError());

         return;

      }

      // 关闭线程句柄

      CloseHandle(ThreadHandle);

   } 

然后需要将一个句柄与已经创建的完成端口关联起来,这里主要指套接字AcceptSocket,以后针对这个套接字的I/O完成状态交由完成端口通知,程序接到完成通知后做善后处理。

这需要再次调用CreateIoCompletionPort函数。参数四NumberOfConcurrentThreads依旧填0,参数一一般就是AcceptSocket,参数二为上面创建的完成端口hCompletionPort。参数三即“完成键”,一般存放套接字句柄的背景信息,也就是所谓的“单句柄数据”之所以把它叫作“单句柄数据”,因为它是用来保存参数一套接字句柄的关联信息。一般可简单定义如下:

typedef struct {

        SOCKET Socket;

} PER_HANDLE_DATA, * LPPER_HANDLE_DATA;

下面的代码片段演示了每次Accept返回时,调用CreateIoCompletionPort使返回的AcceptSocket与完成端口关联,并传递一个PerHandleData。

AcceptSocket = WSAAccept(Listen, NULL, NULL, NULL, 0);

PerHandleData->Socket = AcceptSocket;

CreateIoCompletionPort((HANDLE) AcceptSocket, hCompletionPort, (DWORD) PerHandleData, 0)

这个类比重叠I/O事件通知模型中设置(WSAOVERLAPPED结构中的hEvent字段,使一个事件对象句柄同一个文件/套接字关联起来。 

将套接字句柄与一个完成端口关联在一起后,便可以套接字句柄为基础,投递发送与接收请求,开始对I/O请求的处理。接下来,可开始依赖完成端口,来接收有关I/O操作完成情况的通知。从本质上说,完成端口模型利用了Win32重叠I/O机制。在这种机制中,像WSASendWSARecv这样的Winsock API调用会立即返回。此时,需要由我们的应用程序负责在以后的某个时间,通过一个OVERLAPPED结构,来接收调用的结果。在完成端口模型中,要想做到这一点,工作者线程WorkerThread需要调用GetQueuedCompletionStatus函数,在完成端口上等待。

GetQueuedCompletionStatus函数原型如下:

WINBASEAPI BOOL WINAPI

GetQueuedCompletionStatus(

    HANDLE CompletionPort,

    LPDWORD lpNumberOfBytesTransferred,

    LPDWORD lpCompletionKey,

    LPOVERLAPPED *lpOverlapped,

    DWORD dwMilliseconds );

When you perform an input/output operation with a file handle that has an associated input/output completion port, the I/O system sends a completion notification packet to the completion port when the I/O operation completes. The completion port places the completion packet in a first-in-first-out queue. The GetQueuedCompletionStatus function retrieves these queued completion packets. MSDN

这个类比重叠I/O事件通知模型中的WSAWaitForMultipleEvents/WSAGetOverlappedResult

获得I/O操作结果。

参数一为创建线程池时传递的参数hCompletionPort,参数二提供一个DWORD指针,用来接收当I/O完成时实际传输的字节数。参数三即第二次调用CreateIoCompletionPort时传入的单句柄完成键,这里用于确定与CompletionPort绑定的具体哪个(套接字)句柄完成了I/O操作导致该函数返回。参数四即第二次调用CreateIoCompletionPort时传入的(套接字)句柄(AcceptSocket)投递重叠I/O请求(WSARecv/WSASend)时指定的(WSA)OVERLAPPED结构。实际操作中往往提供一个(WSA)OVERLAPPED扩展结构,这就是常说的“单I/O数据”。一种定义如下:

typedef struct{

   OVERLAPPED Overlapped;

   WSABUF DataBuf;

   CHAR Buffer[DATA_BUFSIZE];

   DWORD BytesSEND;

   DWORD BytesRECV;

} PER_IO_OPERATION_DATA, * LPPER_IO_OPERATION_DATA;

这里的最后两个参数BytesSEND和BytesRECV与GetQueuedCompletionStatus函数返回时的ByteTransfered参数一起同步收发操作。

一般在调用CreateIoCompletionPort将套接字句柄与完成端口hCompletionPort关联后,还需要为AcceptSocket创建PerIOData,以便为后面调用WSARecv/WSASend提供(WSA)OVERLAPPED结构和缓冲区。

下面的是Accept返回,调用CreateIoCompletionPort之后的代码片段。

ZeroMemory(&(PerIoData->Overlapped), sizeof(OVERLAPPED));

    PerIoData->BytesSEND = 0;

    PerIoData->BytesRECV = 0;

    PerIoData->DataBuf.len = DATA_BUFSIZE;

PerIoData->DataBuf.buf = PerIoData->Buffer;

然后调用WSARecv,投递一个等待接收数据的I/O请求。

WSARecv(AcceptSocket, &(PerIoData->DataBuf), 1, &RecvBytes, &Flags, 

        &(PerIoData->Overlapped), NULL)

注意参数一、参数二和参数六,实际上完成了每个AcceptSocketPerIoData的捆绑。

由于调用CreateIoCompletionPort将套接字句柄与完成端口hCompletionPort关联起来了,所以针对AcceptSocket这个套接字句柄上的I/O请求(WSARecv)完成时,一个完成通知包将被投递到完成端口hCompletionPort消息队列中。GetQueuedCompletionStatus函数是用来获取排队完成状态,它使调用线程挂起,直到收到一个完成通知包才返回。

If the function dequeues a completion packet for a successful I/O operation from the completion port, the return value is nonzero. The function stores information in the variables pointed to by the lpNumberOfBytesTransferred, lpCompletionKey, and lpOverlapped parameters.

If *lpOverlapped is NULL and the function does not dequeue a completion packet from the completion port, the return value is zero. The function does not store information in the variables pointed to by the lpNumberOfBytesTransferred and lpCompletionKey parameters. MSDN

在工作者线程WorkerThread中调用GetQueuedCompletionStatus

   while(TRUE)

{

GetQueuedCompletionStatus(CompletionPort, &BytesTransferred,

                             (LPDWORD)&PerHandleData,

(LPOVERLAPPED *) &PerIoData, INFINITE)

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;

 }

// 根据lpNumberOfBytesTransferred, lpCompletionKey, and lpOverlapped参数进行处理

// ……

}

GetQueuedCompletionStatus传递的参数三将PerIOData强制转换为(LPOVERLAPPED *) 结构,后面又要配合使用PerIOData的其他字段,这体现了“扩展”二字的用意。 

 

如前面所言,完成端口模型利用了Win32重叠I/O机制,它是在利用完成端口队列对象来管理线程池。下面总结一下编写基于完成端口的Winsock服务器程序的要点。

(1)首先,当然要调用CreateIoCompletionPort创建一个完成端口,一般一个应用程序只创建一个完成端口。

(2)然后,创建一个线程池,把完成端口作为参数传给线程参数,以使工作线程调用GetQueuedCompletionStatus在完成端口上等待I/O完成,收到完成通知后提供I/O数据处理服务。

(3)每当Accept(Ex)成功返回后,调用CreateIoCompletionPort将AcceptSocket与完成端口关联起来,并传递AcceptSocket的上下文信息(即“单句柄数据”)给完成键参数。同时为AcceptSocket创建一个I/O缓冲区(即“单I/O数据”,扩展OVERLAPPED结构)。

(4)接着,AcceptSocket调用异步I/O操作函数,如WSARecv和WSASend,抛出重叠的I/O请求。这时需要将单I/O数据的第一个字段—OVERLAPPED结构—传递给WSARecv和WSASend,以表示它们投递的是“重叠”的I/O请求,需要等待系统的I/O完成通知。

(5)至此,当上一步抛出的重叠I/O操作完成时,完成端口上会有一个完成通知包,工作线程收到完成通知,从GetQueuedCompletionStatus返回。通过完成键即单句柄数据提供的客户套接字上下文信息、重叠结构参数以及实际I/O的字节数,就可以正式提供I/O数据服务了。

简言之,涉及两个重要的数据结构:“单句柄数据”和“单I/O数据”(扩展的OVERLAPPED结构);涉及两个重要的API CreateIoCompletionPortGetQueuedCompletionStatus;当然,不要忘记重叠请求的投递者WSARecv和WSASend,它们是导火索—通信程序的本质工作就是“通信”。

因为完成端口模型本质上利用了Win32重叠I/O机制,故(扩展的)OVERLAPPED结构提供的沟通机制依然是数据通信重要的线索。另外,要理解完成端口内部机制和工作原理及其在通信中的作用

 

参考:

Network Programming for Microsoft Windows  Anthony Jones,Jim Ohlund

Windows Internals  Mark E. Russinovich,David A. Solomon

Windows 2000 Systems Programming Black Book  Al Williams

Multithreading Applications in Win32  Jim Beveridge,Robert Wiener.

Windows网络与通信程序设计  王艳平

Write Scalable Winsock Apps Using Completion Ports

http://msdn.microsoft.com/en-us/magazine/cc302334.aspx

A simple application using I/O Completion Ports and WinSock

http://www.codeproject.com/KB/IP/SimpleIOCPApp.aspx

Design Issues When Using IOCP in a Winsock Server

http://support.microsoft.com/kb/192800/en-us

IOCP本质论》http://doserver.net/post/The-Essence-of-IOCP.php

阅读(6428) | 评论(8)


版权声明:编程爱好者网站为此博客服务提供商,如本文牵涉到版权问题,编程爱好者网站不承担相关责任,如有版权问题请直接与本文作者联系解决。谢谢!

评论

loading...
您需要登录后才能评论,请 登录 或者 注册