完成端口模型IOCP

完成端口模型
“完成端口”模型是迄今为止最为复杂的一种 I / O模型。然而,假若一个应用程序同时需
要管理为数众多的套接字,那么采用这种模型,往往可以达到最佳的系统性能!但不幸的是,
该模型只适用于Windows NT和Windows 2000操作系统。因其设计的复杂性,只有在你的应用
程序需要同时管理数百乃至上千个套接字的时候,而且希望随着系统内安装的 C P U数量的增
多,应用程序的性能也可以线性提升,才应考虑采用“完成端口”模型。要记住的一个基本
准则是,假如要为Windows NT或Windows 2000开发高性能的服务器应用,同时希望为大量套
接字I / O请求提供服务(We b服务器便是这方面的典型例子),那么I / O完成端口模型便是最佳
选择!
从本质上说,完成端口模型要求我们创建一个Wi n 3 2完成端口对象,通过指定数量的线程,
对重叠I / O请求进行管理,以便为已经完成的重叠I / O请求提供服务。要注意的是,所谓“完成
端口”,实际是Wi n 3 2、Windows NT以及Windows 2000采用的一种I / O构造机制,除套接字句
柄之外,实际上还可接受其他东西。然而,本节只打算讲述如何使用套接字句柄,来发挥完
成端口模型的巨大威力。使用这种模型之前,首先要创建一个 I / O完成端口对象,用它面向任
意数量的套接字句柄,管理多个I / O请求。要做到这一点,需要调用CreateIoComletionPort函数。
该函数定义如下:
HANDLE CreateIoCompletionPort (
  HANDLE FileHandle,              // handle to file
  HANDLE ExistingCompletionPort,  // handle to I/O completion port
  ULONG_PTR CompletionKey,        // completion key
  DWORD NumberOfConcurrentThreads // number of threads to execute concurrently
);

 

在我们深入探讨其中的各个参数之前,首先要注意该函数实际用于两个明显有别的目的:
■ 用于创建一个完成端口对象。
■ 将一个句柄同完成端口关联到一起。
最开始创建一个完成端口时,唯一感兴趣的参数便是 NumberOfConcurrentThreads(并发
线程的数量);前面三个参数都会被忽略。NumberOfConcurrentThreads参数的特殊之处在于,
它定义了在一个完成端口上,同时允许执行的线程数量。理想情况下,我们希望每个处理器
各自负责一个线程的运行,为完成端口提供服务,避免过于频繁的线程“场景”切换。若将
该参数设为0,表明系统内安装了多少个处理器,便允许同时运行多少个线程!可用下述代码
创建一个I / O完成端口:
CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0)

该语句的作用是返回一个句柄,在为完成端口分配了一个套接字句柄后,用来对那个端
口进行标定(引用)。

GetQueuedCompletionStatus函数解释:
实现从指定的IOCP获取CP。当CP队列为空时,对此函数的调用将被阻塞,而不是一直等
待I/O的完成。当CP队列不为空时,被阻塞的线程将以后进先出(LIFO)顺序被释放。
对于IOCP机制,它允许多线程并发调用GetQueuedCompletionStatus函数,最大并发数
是在调用CreateIoCompletionPort函数时指定的,超出最大并发数的调用线程,将被阻塞。
函数解释如下:
声明:
BOOL GetQueuedCompletionStatus(
HANDLE CompletionPort,
LPDWORD lpNumberOfBytes,
PULONG_PTR lpCompletionKey,
LPOVERLAPPED *lpOverlapped,
DWORD dwMilliseconds);
调用参数:
CompletionPort:指定的IOCP,该值由CreateIoCompletionPort函数创建。
lpnumberofbytes:一次完成后的I/O操作所传送数据的字节数。
lpcompletionkey:当文件I/O操作完成后,用于存放与之关联的CK。
lpoverlapped:为调用IOCP机制所引用的OVERLAPPED结构。
dwmilliseconds:用于指定调用者等待CP的时间。
返回值:
调用成功,则返回非零数值,相关数据存于lpNumberOfBytes、lpCompletionKey、lpoverlapped变量中。失败则返回零值。


int WSASend (
SOCKET s,
LPWSABUF lpBuffers
DWORD dwBufferCount,
LPDWORD lpNumberOfBytesSent,
DWORD dwFlags,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);

参数

s:标识一个已连接套接口的描述字。
lpBuffers:一个指向WSABUF结构数组的指针。每个WSABUF结构包含缓冲区的指针和缓冲区的大小。
dwBufferCount:lpBuffers数组中WSABUF结构的数目。
lpNumberOfBytesSent:如果发送操作立即完成,则为一个指向所发送数据字节数的指针。
dwFlags:标志位。
lpOverlapped:指向WSAOVERLAPPED结构的指针(对于非重叠套接口则忽略)。
lpCompletionRoutine:一个指向发送操作完成后调用的完成例程的指针。(对于非重叠套接口则忽略)。

返回值:
若无错误发生且发送操作立即完成,则WSASend()函数返回0。这时,完成例程(Completion Routine)应该已经被调度,一旦调用线程处于alertable状态时就会调用它。否则,返回SOCKET_ERROR 。通过WSAGetLastError获得详细的错误代码。WSA_IO_PENDING 这个错误码(其实表示没有错误)表示重叠操作已经提交成功(就是异步IO的意思了),稍后会提示完成(这个完成可不一定是发送成功,没准出问题也不一定)。其他的错误代码都代表重叠操作没有正确开始,也不会有完成标志出现。

int WSARecv(
SOCKET s, // 当然是投递这个操作的套接字
,与Recv函数不同
// 这里需要一个由WSABUF结构构成的数组
DWORD dwBufferCount, // 数组中WSABUF结构的数量
LPDWORD lpNumberOfBytesRecvd, // 如果接收操作立即完成,这里会返回函数调用所接收到的字节数
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine // 完成例程中将会用到的参数,我们这里设置为 NULL
);
返回值:
WSA_IO_PENDING : 最常见的返回值,这是说明我们的WSARecv操作成功了,但是I/O操作还没有完成,所以我们就需要绑定一个事件来通知我们操作何时完成


1. 工作者线程与完成端口
成功创建一个完成端口后,便可开始将套接字句柄与对象关联到一起。但在关联套接字
之前,首先必须创建一个或多个“工作者线程”,以便在I / O请求投递给完成端口对象后,为
完成端口提供服务。在这个时候,大家或许会觉得奇怪,到底应创建多少个线程,以便为完
成端口提供服务呢?这实际正是完成端口模型显得颇为“复杂”的一个方面,因为服务 I / O请
求所需的数量取决于应用程序的总体设计情况。在此要记住的一个重点在于,在我们调用
CreateIoComletionPort时指定的并发线程数量,与打算创建的工作者线程数量相比,它们代表
的并非同一件事情。早些时候,我们曾建议大家用 CreateIoComletionPort函数为每个处理器
都指定一个线程(处理器的数量有多少,便指定多少线程)以避免由于频繁的线程“场景”
交换活动,从而影响系统的整体性能。CreateIoComletionPort函数的NumberOfConcurrentThreads参数明确指示系统:在一个完成端口上,一

次只允许 n个工作者线程运行。假如在完
成端口上创建的工作者线程数量超出 n个,那么在同一时刻,最多只允许 n个线程运行。但实
际上,在一段较短的时间内,系统有可能超过这个值,但很快便会把它减少至事先在
CreateIoComletionPort函数中设定的值。那么,为何实际创建的工作者线程数量有时要比
CreateIoComletionPort函数设定的多一些呢?这样做有必要吗?如先前所述,这主要取决于
应用程序的总体设计情况。假定我们的某个工作者线程调用了一个函数,比如 Sleep或
WaitForSingleObject,但却进入了暂停(锁定或挂起)状态,那么允许另一个线程代替它的位
置。换言之,我们希望随时都能执行尽可能多的线程;当然,最大的线程数量是事先在
CreateIoCompletonPort调用里设定好的。这样一来,假如事先预计到自己的线程有可能暂时
处于停顿状态,那么最好能够创建比 CreateIoCompletonPort的NumberOfConcurrentThreads参数的值多的线程,以便到时候充分发挥系统的潜

力。
一旦在完成端口上拥有足够多的工作者线程来为I / O请求提供服务,便可着手将套接字句柄
同完成端口关联到一起。这要求我们在一个现有的完成端口上,调用CreateIo CompletionPort函
数,同时为前三个参数—FileHandle,ExistingCompletionPort和CompletionKey—提供套
接字的信息。其中,FileHandle参数指定一个要同完成端口关联在一起的套接字句柄。
ExistingCompletionPort参数指定的是一个现有的完成端口。 CompletionKey(完成键)参
数则指定要与某个特定套接字句柄关联在一起的“单句柄数据”;在这个参数中,应用程序
可保存与一个套接字对应的任意类型的信息。之所以把它叫作“单句柄数据”,是由于它只对
应着与那个套接字句柄关联在一起的数据。可将其作为指向一个数据结构的指针,来保存套
接字句柄;在那个结构中,同时包含了套接字的句柄,以及与那个套接字有关的其他信息。
就象本章稍后还会讲述的那样,为完成端口提供服务的线程例程可通过这个参数,取得与套
接字句柄有关的信息。
根据我们到目前为止学到的东西,首先来构建一个基本的应用程序框架。程序清单 8 - 9向
大家阐述了如何使用完成端口模型,来开发一个回应(或“反射”)服务器应用。在这个程序
中,我们基本上按下述步骤行事:
1) 创建一个完成端口。第四个参数保持为0,指定在完成端口上,每个处理器一次只允许
执行一个工作者线程。
2) 判断系统内到底安装了多少个处理器。
3) 创建工作者线程,根据步骤2 )得到的处理器信息,在完成端口上,为已完成的 I / O请求
提供服务。在这个简单的例子中,我们为每个处理器都只创建一个工作者线程。这是由于事
先已预计到,到时不会有任何线程进入“挂起”状态,造成由于线程数量的不足,而使处理
器空闲的局面(没有足够的线程可供执行)。调用CreateThread函数时,必须同时提供一个工
作者例程,由线程在创建好执行。本节稍后还会详细讨论线程的职责。
4) 准备好一个监听套接字,在端口5150上监听进入的连接请求。

5) 使用accept函数,接受进入的连接请求。
6) 创建一个数据结构,用于容纳“单句柄数据”,同时在结构中存入接受的套接字句柄。
7) 调用CreateIoComletionPort,将自accept返回的新套接字句柄同完成端口关联到一起。
通过完成键(CompletionKey)参数,将单句柄数据结构传递给CreateIoComletionPort。
8) 开始在已接受的连接上进行 I / O操作。在此,我们希望通过重叠 I / O机制,在新建的套
接字上投递一个或多个异步 WSARecv或WSASend请求。这些I / O请求完成后,一个工作者线
程会为I / O请求提供服务,同时继续处理未来的 I / O请求,稍后便会在步骤3 )指定的工作者例程
中,体验到这一点。

9) 重复步骤5 ) ~ 8 ),直至服务器中止。
程序清单8-9   完成端口的建立

 代码说明:

1 OVERLAPPED 是如何到达工作线程的?或者说工作线程中的GetQueuedCompletionStatus函数的第四个参数为什么能到得到客户端对应的

OVERLAPPED?

答:CreateIoCompletionPort函数将完成端口对象与客户端套接字绑定,WSARecv函数在接收完成后,将OVERLAPPED投递到了与客户端套接字绑定的完成端口对象,所以在该完成端口对象中,GetQueuedCompletionStatus可以获得OVERLAPPED。

2 GetQueuedCompletionStatus获得了OVERLAPPED,为什么可以当作PER_IO_OPERATION_DATA结构来使用?

答:这用到了一点编程技巧--“尾随数据”,PER_IO_OPERATION_DATA结构体的设计将OVERLAPPED成员放在了第一个,所以PER_IO_OPERATION_DATA结构体的地址正是OVERLAPPED成员的首地址,在问题1中,OVERLAPPED是通过首地址来传递的,所以GetQueuedCompletionStatus获得OVERLAPPED首地址后,可以当作PER_IO_OPERATION_DATA结构来使用。

3注意一点:WSARecv函数和WSASend函数的 dwFlags参数一般设置为0,传参前记得将参数赋值为0.

  1. #include <winsock2.h>
  2. #include <windows.h>
  3. #include <stdio.h>
  4. #define PORT 5150
  5. #define DATA_BUFSIZE 8192
  6. typedef struct
  7. {
  8.    OVERLAPPED Overlapped;
  9.    WSABUF DataBuf;
  10.    CHAR Buffer[DATA_BUFSIZE];
  11.    DWORD BytesSEND;
  12.    DWORD BytesRECV;
  13. } PER_IO_OPERATION_DATA, * LPPER_IO_OPERATION_DATA;
  14. typedef struct 
  15. {
  16.    SOCKET Socket;
  17. } PER_HANDLE_DATA, * LPPER_HANDLE_DATA;
  18. DWORD WINAPI ServerWorkerThread(LPVOID CompletionPortID);
  19. void main(void)
  20. {
  21.    SOCKADDR_IN InternetAddr;
  22.    SOCKET Listen;
  23.    SOCKET Accept;
  24.    HANDLE CompletionPort;
  25.    SYSTEM_INFO SystemInfo;
  26.    LPPER_HANDLE_DATA PerHandleData;
  27.    LPPER_IO_OPERATION_DATA PerIoData;
  28.    int i;
  29.    DWORD RecvBytes;
  30.    DWORD Flags;
  31.    DWORD ThreadID;
  32.    WSADATA wsaData;
  33.    DWORD Ret;
  34.    if ((Ret = WSAStartup(0x0202, &wsaData)) != 0)
  35.    {
  36.       printf("WSAStartup failed with error %d/n", Ret);
  37.       return;
  38.    }
  39.    // Setup an I/O completion port.
  40.    if ((CompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0)) == NULL)
  41.    {
  42.       printf( "CreateIoCompletionPort failed with error: %d/n", GetLastError());
  43.       return;
  44.    }
  45.    // Determine how many processors are on the system.
  46.    GetSystemInfo(&SystemInfo);
  47.    // Create worker threads based on the number of processors available on the
  48.    // system. Create two worker threads for each processor.
  49.    for(i = 0; i < SystemInfo.dwNumberOfProcessors * 2; i++)
  50.    {
  51.       HANDLE ThreadHandle;
  52.       // Create a server worker thread and pass the completion port to the thread.
  53.       if ((ThreadHandle = CreateThread(NULL, 0, ServerWorkerThread, CompletionPort,
  54.          0, &ThreadID)) == NULL)
  55.       {
  56.          printf("CreateThread() failed with error %d/n", GetLastError());
  57.          return;
  58.       }
  59.       // Close the thread handle
  60.       CloseHandle(ThreadHandle);
  61.    }
  62.    // Create a listening socket
  63.    if ((Listen = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0,
  64.       WSA_FLAG_OVERLAPPED)) == INVALID_SOCKET)
  65.    {
  66.       printf("WSASocket() failed with error %d/n", WSAGetLastError());
  67.       return;
  68.    } 
  69.    InternetAddr.sin_family = AF_INET;
  70.    InternetAddr.sin_addr.s_addr = htonl(INADDR_ANY);
  71.    InternetAddr.sin_port = htons(PORT);
  72.    if (bind(Listen, (PSOCKADDR) &InternetAddr, sizeof(InternetAddr)) == SOCKET_ERROR)
  73.    {
  74.       printf("bind() failed with error %d/n", WSAGetLastError());
  75.       return;
  76.    }
  77.    // Prepare socket for listening
  78.    if (listen(Listen, 5) == SOCKET_ERROR)
  79.    {
  80.       printf("listen() failed with error %d/n", WSAGetLastError());
  81.       return;
  82.    }
  83.    // Accept connections and assign to the completion port.
  84.    while(TRUE)
  85.    {
  86.       if ((Accept = WSAAccept(Listen, NULL, NULL, NULL, 0)) == SOCKET_ERROR)
  87.       {
  88.          printf("WSAAccept() failed with error %d/n", WSAGetLastError());
  89.          return;
  90.       }
  91.       // Create a socket information structure to associate with the socket
  92.       if ((PerHandleData = (LPPER_HANDLE_DATA) GlobalAlloc(GPTR, 
  93.          sizeof(PER_HANDLE_DATA))) == NULL)
  94.       {
  95.          printf("GlobalAlloc() failed with error %d/n", GetLastError());
  96.          return;
  97.       }
  98.       // Associate the accepted socket with the original completion port.
  99.       printf("Socket number %d connected/n", Accept);
  100.       PerHandleData->Socket = Accept;
  101.       if (CreateIoCompletionPort((HANDLE) Accept, CompletionPort, (DWORD) PerHandleData,
  102.          0) == NULL)
  103.       {
  104.          printf("CreateIoCompletionPort failed with error %d/n", GetLastError());
  105.          return;
  106.       }
  107.       // Create per I/O socket information structure to associate with the 
  108.       // WSARecv call below.
  109.       if ((PerIoData = (LPPER_IO_OPERATION_DATA) GlobalAlloc(GPTR,          sizeof(PER_IO_OPERATION_DATA))) == NULL)
  110.       {
  111.          printf("GlobalAlloc() failed with error %d/n", GetLastError());
  112.          return;
  113.       }
  114.       ZeroMemory(&(PerIoData->Overlapped), sizeof(OVERLAPPED));
  115.       PerIoData->BytesSEND = 0;
  116.       PerIoData->BytesRECV = 0;
  117.       PerIoData->DataBuf.len = DATA_BUFSIZE;
  118.       PerIoData->DataBuf.buf = PerIoData->Buffer;
  119.       Flags = 0;
  120.       if (WSARecv(Accept, &(PerIoData->DataBuf), 1, &RecvBytes, &Flags,
  121.          &(PerIoData->Overlapped), NULL) == SOCKET_ERROR)
  122.       {
  123.          if (WSAGetLastError() != ERROR_IO_PENDING)
  124.          {
  125.             printf("WSARecv() failed with error %d/n", WSAGetLastError());
  126.             return;
  127.          }
  128.       }
  129.    }
  130. }
  131. DWORD WINAPI ServerWorkerThread(LPVOID CompletionPortID)
  132. {
  133.    HANDLE CompletionPort = (HANDLE) CompletionPortID;
  134.    DWORD BytesTransferred;
  135.    LPOVERLAPPED Overlapped;
  136.    LPPER_HANDLE_DATA PerHandleData;
  137.    LPPER_IO_OPERATION_DATA PerIoData;
  138.    DWORD SendBytes, RecvBytes;
  139.    DWORD Flags;
  140.    
  141.    while(TRUE)
  142.    {
  143.         
  144.       if (GetQueuedCompletionStatus(CompletionPort, &BytesTransferred,
  145.          (LPDWORD)&PerHandleData, (LPOVERLAPPED *) &PerIoData, INFINITE) == 0)
  146.       {
  147.          printf("GetQueuedCompletionStatus failed with error %d/n", GetLastError());
  148.          return 0;
  149.       }
  150.       // First check to see if an error has occured on the socket and if so
  151.       // then close the socket and cleanup the SOCKET_INFORMATION structure
  152.       // associated with the socket.
  153.       if (BytesTransferred == 0)
  154.       {
  155.          printf("Closing socket %d/n", PerHandleData->Socket);
  156.          if (closesocket(PerHandleData->Socket) == SOCKET_ERROR)
  157.          {
  158.             printf("closesocket() failed with error %d/n", WSAGetLastError());
  159.             return 0;
  160.          }
  161.          GlobalFree(PerHandleData);
  162.          GlobalFree(PerIoData);
  163.          continue;
  164.       }
  165.       // Check to see if the BytesRECV field equals zero. If this is so, then
  166.       // this means a WSARecv call just completed so update the BytesRECV field
  167.       // with the BytesTransferred value from the completed WSARecv() call.
  168.       if (PerIoData->BytesRECV == 0)
  169.       {
  170.          PerIoData->BytesRECV = BytesTransferred;
  171.          PerIoData->BytesSEND = 0;
  172.       }
  173.       else
  174.       {
  175.          PerIoData->BytesSEND += BytesTransferred;
  176.       }
  177.       if (PerIoData->BytesRECV > PerIoData->BytesSEND)
  178.       {
  179.          // Post another WSASend() request.
  180.          // Since WSASend() is not gauranteed to send all of the bytes requested,
  181.          // continue posting WSASend() calls until all received bytes are sent.
  182.          ZeroMemory(&(PerIoData->Overlapped), sizeof(OVERLAPPED));
  183.          PerIoData->DataBuf.buf = PerIoData->Buffer + PerIoData->BytesSEND;
  184.          PerIoData->DataBuf.len = PerIoData->BytesRECV - PerIoData->BytesSEND;
  185.          if (WSASend(PerHandleData->Socket, &(PerIoData->DataBuf), 1, &SendBytes, 0,
  186.             &(PerIoData->Overlapped), NULL) == SOCKET_ERROR)
  187.          {
  188.             if (WSAGetLastError() != ERROR_IO_PENDING)
  189.             {
  190.                printf("WSASend() failed with error %d/n", WSAGetLastError());
  191.                return 0;
  192.             }
  193.          }
  194.       }
  195.       else
  196.       {
  197.          PerIoData->BytesRECV = 0;
  198.          // Now that there are no more bytes to send post another WSARecv() request.
  199.          Flags = 0;
  200.          ZeroMemory(&(PerIoData->Overlapped), sizeof(OVERLAPPED));
  201.          PerIoData->DataBuf.len = DATA_BUFSIZE;
  202.          PerIoData->DataBuf.buf = PerIoData->Buffer;
  203.          if (WSARecv(PerHandleData->Socket, &(PerIoData->DataBuf), 1, &RecvBytes, &Flags,
  204.             &(PerIoData->Overlapped), NULL) == SOCKET_ERROR)
  205.          {
  206.             if (WSAGetLastError() != ERROR_IO_PENDING)
  207.             {
  208.                printf("WSARecv() failed with error %d/n", WSAGetLastError());
  209.                return 0;
  210.             }
  211.          }
  212.       }
  213.    }
  214. }

你可能感兴趣的:(异步,服务器,高性能,MFC网络,重叠IO)