WinSock完成端口I/O模型

from  http://blog.csdn.net/phunxm/article/details/5085944



关于重叠I/O,参考《WinSock重叠I/O模型》;关于完成端口的概念及内部机制,参考译文《深度探索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_VALUENULL, 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, WorkerThreadhCompletionPort,

         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(ListenNULLNULLNULL, 0);

PerHandleData->Socket = AcceptSocket;

CreateIoCompletionPort((HANDLEAcceptSockethCompletionPort, (DWORDPerHandleData, 0)

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

将套接字句柄与一个完成端口关联在一起后,便可以套接字句柄为基础,投递发送与接收请求,开始对I/O请求的处理。接下来,可开始依赖完成端口,来接收有关I/O操作完成情况的通知。从本质上说,完成端口模型利用了Win32重叠I/O机制。在这种机制中,像WSARecv()和WSASend()这样的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;

}OVERLAPPEDPLUS,PER_IO_OPERATION_DATA,*LPPER_IO_OPERATION_DATA;

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

一般在调用CreateIoCompletionPort将套接字句柄与完成端口hCompletionPort关联后,还需要为AcceptSocket创建PerIOData,以便为后面调用WSARecv()/WSASend()提供(WSA)OVERLAPPED结构和缓冲区。为确保单I/O数据的生存周期延续到I/O完成,一般动态分配,待I/O完成再回收。对于I/O频繁的系统,则可以使用内存池,每次只是回收到空闲池,最后再真正释放。这样,可避免频繁的内存分配。可参考《MFC基于CPlex结构的内存池化管理》。

下面的是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)

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

由于调用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 lpNumberOfBytesTransferredlpCompletionKey, andlpOverlapped 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 andlpCompletionKey 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的其他字段,这体现了“扩展”二字的用意。

GetQueuedCompletionStatus返回FALSE,可以调用(WSA)GetOverlappedResult/(WSA)GetLastError获知具体错误。

如前面所言,完成端口模型利用了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操作函数,如WSARecvWSASend,抛出重叠的I/O请求。这时需要将单I/O数据的第一个字段—OVERLAPPED结构—传递给WSARecvWSASend,以表示它们投递的是“重叠”的I/O请求,需要等待系统的I/O完成通知。

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

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

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

 

下面补充完成端口的项目应用实例。

Windows下的IIS采用了完成端口模型,参考《完成端口与高性能服务器程序开发》、《I/O完成端口(Windows核心编程)》、《A simple application using I/O Completion Ports and WinSock》。

Apache Httpd/httpd/server/mpm/winnt/child.c中的winnt_accept()(AcceptEx)和winnt_get_connection()使用了完成端口ThreadDispatchIOCP,但并没有关联套接字,而是自己构造完成包(mpm_post_completion_contextPostQueuedCompletionStatus),完成键为枚举io_state_e,单句柄为PCOMP_CONTEXT

// Apache Httpd/httpd/server/mpm/winnt/mpm_winnt.h

typedef enum {

    IOCP_CONNECTION_ACCEPTED = 1,

    IOCP_WAIT_FOR_RECEIVE = 2,

    IOCP_WAIT_FOR_TRANSMITFILE = 3,

    IOCP_SHUTDOWN = 4

} io_state_e;

Apache源码中只使用到IOCP_CONNECTION_ACCEPTEDIOCP_SHUTDOWN两种状态。除此之外,Apache里面没有真正的完成端口成分,ntmpm似乎依然是线程池加进程池来处理。具体I/O过程参考Apache源码Apache Httpd/httpd-2.2.15/srclib/apr/file_io/win32/readwrite.c和Apache Httpd/httpd/srclib/apr/network_io/win32/sendrecv.c。

Nginx是由Igor Sysoev为俄罗斯访问量第二的Rambler.ru站点开发的,其特点是占有内存少,并发能力强,Nginx的并发能力确实在同类型的网页伺服器中表现较好。新浪、网易、腾讯、迅雷、CSDN等大型网站都采用了Nginx。Nginx每一个客户端请求进来以后会通过事件处理机制,在Linux是Epoll,在FreeBSD下是KQueue放到空闲的连接里。相关源码参考nginx/src/event/modules下的ngx_epoll_module.c、ngx_kqueue_module.h(c)。参考《基于IO完成端口与WSAEventSelect的nginx事件处理模块:ngx_iocp_module》、《Windows下完成端口移植Linux下的epoll》。

 

参考:

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网络与通信程序设计》  王艳平

 

IOCP完成端口原理

Write Scalable Winsock Apps Using Completion Ports

Design Issues When Using IOCP in a Winsock Server


  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
最近有项目要做一个高性能网络服务器,决定下功夫搞定完成端口(IOCP),最终花了一个星期终于把它弄清楚了,并用C++写了一个版本,效率很不错。 但,从项目的总体需求来考虑,最终决定上.net平台,因此又花了一天一夜弄出了一个C#版,在这与大家分享。 一些心得体会: 1、在C#中,不用去面对完成端口的操作系统内核对象,Microsoft已经为我们提供了SocketAsyncEventArgs类,它封装了IOCP的使用。请参考:http://msdn.microsoft.com/zh-cn/library/system.net.sockets.socketasynceventargs.aspx?cs-save-lang=1&cs-lang=cpp#code-snippet-1。 2、我的SocketAsyncEventArgsPool类使用List对象来存储对客户端来通信的SocketAsyncEventArgs对象,它相当于直接使用内核对象时的IoContext。我这样设计比用堆栈来实现的好处理是,我可以在SocketAsyncEventArgsPool池中找到任何一个与服务器连接的客户,主动向它发信息。而用堆栈来实现的话,要主动给客户发信息,则还要设计一个结构来存储已连接上服务器的客户。 3、对每一个客户端不管还发送还是接收,我使用同一个SocketAsyncEventArgs对象,对每一个客户端来说,通信是同步进行的,也就是说服务器高度保证同一个客户连接上要么在投递发送请求,并等待;或者是在投递接收请求,等待中。本例只做echo服务器,还未考虑由服务器主动向客户发送信息。 4、SocketAsyncEventArgs的UserToken被直接设定为被接受的客户端Socket。 5、没有使用BufferManager 类,因为我在初始化时给每一个SocketAsyncEventArgsPool中的对象分配一个缓冲区,发送时使用Arrary.Copy来进行字符拷贝,不去改变缓冲区的位置,只改变使用的长度,因此在下次投递接收请求时恢复缓冲区长度就可以了!如果要主动给客户发信息的话,可以new一个SocketAsyncEventArgs对象,或者在初始化中建立几个来专门用于主动发送信息,因为这种需求一般是进行信息群发,建立一个对象可以用于很多次信息发送,总体来看,这种花销不大,还减去了字符拷贝和消耗。 6、测试结果:(在我的笔记本上时行的,我的本本是T420 I7 8G内存) 100客户 100,000(十万次)不间断的发送接收数据(发送和接收之间没有Sleep,就一个一循环,不断的发送与接收) 耗时3004.6325 秒完成 总共 10,000,000 一千万次访问 平均每分完成 199,691.6 次发送与接收 平均每秒完成 3,328.2 次发送与接收 整个运行过程中,内存消耗在开始两三分种后就保持稳定不再增涨。 看了一下对每个客户端的延迟最多不超过2秒。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值