打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
基于完成端口的Winsock程序设计

 

关于完成端口的概念及内部机制,参考译文《深度探索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 );

Whenyou perform an input/output operation with a file handle that has anassociated input/output completion port, the I/O system sends a completion notification packetto the completion port when the I/O operation completes. The completionport 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完成时实际传输的字节数。参数三即上一步所说的单句柄完成键。参数四即为套接字AcceptSocket分配的(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提供(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函数是用来获取排队完成状态,它使调用线程挂起,直到收到一个完成通知包才返回。

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

If *lpOverlappedis NULL and the function does not dequeue a completion packet from thecompletion port, the return value is zero. The function does not storeinformation 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

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

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
GetQueuedCompletionStatus,PostQueuedCompletionStatus函数
完成端口详解
Overlapped I/O模型深入分析
Windows完成端口与Linux epoll技术简介
在c#使用IOCP(完成端口)的简单示例
深入探析c# Socket
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服