SOCKET编程进阶之完成端口

SOCKET编程进阶之完成端口

原文地址:http://blog.csdn.net/echoff/archive/2007/09/23/1797326.aspx

一、什么是完成端口?
完成端口---是一种WINDOWS内核对象。完成端口用于异步方式的重叠I/0情况下,当然重叠I/O不一定非使用完成端口不可,还有设备内核对象、事件对象、告警I/0等。但是完成端口内部提供了线程池的管理,可以避免反复创建线程的开销,同时可以根据CPU的个数灵活的决定线程个数,而且可以让减少线程调度的次数从而提高性能。
 
二、完成端口的内部机制

1)创建完成端口
完成端口是一个内核对象,使用时他总是要和至少一个有效的设备句柄进行关联,完成端口是一个复杂的内核对象,创建它的函数是:
HANDLE CreateIoCompletionPort(
    IN HANDLE FileHandle,
    IN HANDLE ExistingCompletionPort,
    IN ULONG_PTR CompletionKey,
    IN DWORD NumberOfConcurrentThreads
    );
通常创建工作分两步:
 
第一步,创建一个新的完成端口内核对象,可以使用下面的函数:
HANDLE CreateNewCompletionPort(DWORD dwNumberOfThreads)
{
     return CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,NULL,dwNumberOfThreads);
};
 
第二步,将刚创建的完成端口和一个有效的设备句柄关联起来,可以使用下面的函数:
bool AssicoateDeviceWithCompletionPort(HANDLE hCompPort,HANDLE hDevice,DWORD dwCompKey)
{
    HANDLE h=CreateIoCompletionPort(hDevice,hCompPort,dwCompKey,0);
    return h==hCompPort;
};
 
说明
a)CreateIoCompletionPort函数也可以一次性的既创建完成端口对象,又关联到一个有效的设备句柄
b)CompletionKey是一个可以自己定义的参数,我们可以把一个结构的地址赋给它,然后在合适的时候取出来使用,最好要保证结构里面的内存不是分配在栈上,除非你有十分的把握内存会保留到你要使用的那一刻。
c)NumberOfConcurrentThreads通常用来指定要允许同时运行的的线程的最大个数。通常我们指定为0,这样系统会根据CPU的个数来自动确定。
创建和关联的动作完成后,系统会将完成端口关联的设备句柄、完成键作为一条纪录加入到这个完成端口的设备列表中。如果你有多个完成端口,就会有多个对应的设备列表。如果设备句柄被关闭,则表中自动删除该纪录。
 
2)完成端口线程的工作原理
完成端口可以帮助我们管理线程池,但是线程池中的线程需要我们使用_beginthreadex来创建,凭什么通知完成端口管理我们的新线程呢?答案在函数GetQueuedCompletionStatus。该函数原型:
BOOL GetQueuedCompletionStatus(
    IN  HANDLE CompletionPort,
    OUT LPDWORD lpNumberOfBytesTransferred,
    OUT PULONG_PTR lpCompletionKey,
    OUT LPOVERLAPPED *lpOverlapped,
    IN  DWORD dwMilliseconds
);
这个函数试图从指定的完成端口的I/0完成队列中抽取纪录。只有当重叠I/O动作完成的时候,完成队列中才有纪录。凡是调用这个函数的线程将被放入到完成端口的等待线程队列中,因此完成端口就可以在自己的线程池中帮助我们维护这个线程。
完成端口的I/0完成队列中存放了当重叠I/0完成的结果---- 一条纪录,该纪录拥有四个字段,前三项就对应GetQueuedCompletionStatus函数的2、3、4参数,最后一个字段是错误信息dwError。我们也可以通过调用PostQueudCompletionStatus模拟完成了一个重叠I/0操作。
当I/0完成队列中出现了纪录,完成端口将会检查等待线程队列,该队列中的线程都是通过调用GetQueuedCompletionStatus函数使自己加入队列的。等待线程队列很简单,只是保存了这些线程的ID。完成端口会按照后进先出的原则将一个线程队列的ID放入到释放线程列表中,同时该线程将从等待GetQueuedCompletionStatus函数返回的睡眠状态中变为可调度状态等待CPU的调度。
基本上情况就是如此,所以我们的线程要想成为完成端口管理的线程,就必须要调用
GetQueuedCompletionStatus函数。出于性能的优化,实际上完成端口还维护了一个暂停线程列表,具体细节可以参考《Windows高级编程指南》,我们现在知道的知识,已经足够了。
 
3)线程间数据传递
线程间传递数据最常用的办法是在_beginthreadex函数中将参数传递给线程函数,或者使用全局变量。但是完成端口还有自己的传递数据的方法,答案就在于CompletionKey和OVERLAPPED参数。
CompletionKey被保存在完成端口的设备表中,是和设备句柄一一对应的,我们可以将与设备句柄相关的数据保存到CompletionKey中,或者将CompletionKey表示为结构指针,这样就可以传递更加丰富的内容。这些内容只能在一开始关联完成端口和设备句柄的时候做,因此不能在以后动态改变。
OVERLAPPED参数是在每次调用ReadFile这样的支持重叠I/0的函数时传递给完成端口的。我们可以看到,如果我们不是对文件设备做操作,该结构的成员变量就对我们几乎毫无作用。我们需要附加信息,可以创建自己的结构,然后将OVERLAPPED结构变量作为我们结构变量的第一个成员,然后传递第一个成员变量的地址给ReadFile函数。因为类型匹配,当然可以通过编译。当GetQueuedCompletionStatus函数返回时,我们可以获取到第一个成员变量的地址,然后一个简单的强制转换,我们就可以把它当作完整的自定义结构的指针使用,这样就可以传递很多附加的数据了。太好了!只有一点要注意,如果跨线程传递,请注意将数据分配到堆上,并且接收端应该将数据用完后释放。我们通常需要将ReadFile这样的异步函数的所需要的缓冲区放到我们自定义的结构中,这样当GetQueuedCompletionStatus被返回时,我们的自定义结构的缓冲区变量中就存放了I/0操作的数据。
CompletionKey和OVERLAPPED参数,都可以通过GetQueuedCompletionStatus函数获得。
 
4)线程的安全退出
很多线程为了不止一次的执行异步数据处理,需要使用如下语句
while (true)
{
       .。。。。。。
       GetQueuedCompletionStatus(...);
              。。。。。。
}
那么如何退出呢,答案就在于上面曾提到的PostQueudCompletionStatus函数,我们可以用它发送一个自定义的包含了OVERLAPPED成员变量的结构地址,里面包含一个状态变量,当状态变量为退出标志时,线程就执行清除动作然后退出。


//接上文
写了一下午,终于写完了这个“完成端口”。
到今天为止,写完了Overlapped I/O Event、Overlapped I/O completion Routine和completion Port。一路写过来的确学到了不少东西,也清楚地看到到微软在遇到问题并解决问题的方法;不得不承认,微软~还是很强的。呵呵~

这也让我明白一件事:遇到困难,不要望而却步;只要你勇于探索,一切都将是那么简单。(听起来有点自恋的感觉^_^)
“完成端口”模型是迄今为止最为复杂的一种I/O模型。然而,假若一个应用程序同时需要管理为数众多的套接字,那么采用这种模型,往往可以达到最佳的系统性能!但不幸的是,该模型只适用于Windows NT和Windows 2000操作系统。因其设计的复杂性,只有在你的应用程序需要同时管理数百乃至上千个套接字的时候,而且希望随着系统内安装的CPU数量的增多,应用程序的性能也可以线性提升,才应考虑采用“完成端口”模型。要记住的一个基本准则是,假如要为Windows NT或Windows 2000开发高性能的服务器应用,同时希望为大量套接字I/O请求提供服务(Web服务器便是这方面的典型例子),那么I/O完成端口模型便是最佳选择!
我们基本上按下述步骤行事:
1) 创建一个完成端口。第四个参数保持为0,指定在完成端口上,每个处理器一次只允许执行一个工作者线程。
2) 判断系统内到底安装了多少个处理器。
3) 创建工作者线程,根据步骤2)得到的处理器信息,在完成端口上,为已完成的I/O请求提供服务。在这个简单的例子中,我们为每个处理器都只创建一个工作者线程。这是由于事先已预计到,到时不会有任何线程进入“挂起”状态,造成由于线程数量的不足,而使处理器空闲的局面(没有足够的线程可供执行)。调用CreateThread函数时,必须同时提供一个工作者例程,由线程在创建好执行。本节稍后还会详细讨论线程的职责。
4) 准备好一个监听套接字,在端口1234上监听进入的连接请求。
5) 使用accept函数,接受进入的连接请求。
6) 创建一个数据结构,同时在结构中存入接受的套接字句柄。
7) 调用CreateIoCompletionPort,将自accept返回的新套接字句柄同完成端口关联到一起。通过完成键(CompletionKey)参数,将单句柄数据结构传递给CreateIoCompletionPort。
8) 开始在已接受的连接上进行I/O操作。在此,我们希望通过重叠I/O机制,在新建的套接字上投递一个或多个异步WSARecv或WSASend请求。这些I/O请求完成后,一个工作者线程会为I/O请求提供服务,同时继续处理未来的I/O请求,稍后便会在步骤3)指定的工作者例程
中,体验到这一点。
9) 重复步骤5) ~ 8),直至服务器中止。
  1. #pragma comment(lib,"ws2_32.lib")
  2. #include <winsock2.h>
  3. #include <stdio.h>
  4. //
  5. //仅供测试软件用
  6. #include "Protocol.h"
  7. #define DATA_BUFSIZE 1024        // 接收缓冲区大小
  8. typedef enum{ IOSEND,IORECV,IOQUIT } IO_TYPE;
  9. typedef struct _SOCKET_INFORMATION {
  10.         OVERLAPPED Overlapped;
  11.         SOCKET        Socket;
  12.         IO_TYPE  IoType;
  13.         char                buffer[DATA_BUFSIZE];
  14.         WSABUF        DataBuf;
  15.         DWORD        BytesSEND;
  16.         DWORD        BytesRECV;
  17. } SOCKET_INFORMATION, * LPSOCKET_INFORMATION;
  18. DWORD   Flags = 0,
  19.                 Bytes = 0;
  20. DWORD WINAPI WorkThread(LPVOID CompletionPortID);
  21. DWORD WINAPI AcceptThread(LPVOID lpParameter)
  22. {
  23.         WSADATA wsaData;
  24.         HANDLE hCompPort;
  25.         DWORD ThreadID;
  26.         DWORD Ret;
  27.         if ((Ret = WSAStartup(0x0202, &wsaData)) != 0)
  28.         {
  29.                 printf("WSAStartup failed with error %d/n", Ret);
  30.                 return FALSE;
  31.         }
  32.         if ((hCompPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0)) == NULL)
  33.         {
  34.                 printf( "CreateIoCompletionPort failed with error: %d/n", GetLastError());
  35.                 return FALSE;
  36.         }
  37.         //根据CPU个数来创建线程,以达到最佳性能
  38.         SYSTEM_INFO SystemInfo;
  39.         GetSystemInfo(&SystemInfo);
  40.         for(unsigned int i=0; i<SystemInfo.dwNumberOfProcessors*2; i++)
  41.         {
  42.                 HANDLE ThreadHandle;
  43.                 if ((ThreadHandle = CreateThread(NULL, 0, WorkThread, hCompPort, 0, &ThreadID)) == NULL)
  44.                 {
  45.                         printf("CreateThread() failed with error %d/n", GetLastError());
  46.                         return FALSE;
  47.                 }
  48.                 CloseHandle(ThreadHandle);
  49.         }
  50.         SOCKET ListenSocket = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, NULL, WSA_FLAG_OVERLAPPED);
  51.         SOCKADDR_IN ServerAddr;
  52.         ServerAddr.sin_family = AF_INET;
  53.         ServerAddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
  54.         ServerAddr.sin_port = htons(1234);
  55.         bind(ListenSocket,(LPSOCKADDR)&ServerAddr,sizeof(ServerAddr));
  56.         listen(ListenSocket,100);
  57.         printf("listenning.../n");
  58.         SOCKADDR_IN ClientAddr;
  59.         int addr_length=sizeof(ClientAddr);
  60.         while (TRUE)
  61.         {
  62.                 LPSOCKET_INFORMATION  SI = new SOCKET_INFORMATION;
  63.                 if ((SI->Socket = accept(ListenSocket,(SOCKADDR*)&ClientAddr, &addr_length)) != INVALID_SOCKET)
  64.                 {
  65.                         printf("accept ip:%s port:%d/n",inet_ntoa(ClientAddr.sin_addr),ClientAddr.sin_port);
  66.                         //相关参数初始化
  67.                         memset(&SI->Overlapped,0,sizeof(WSAOVERLAPPED));
  68.                         memset(SI->buffer, 0, DATA_BUFSIZE);
  69.                         SI->DataBuf.buf = SI->buffer;
  70.                         SI->DataBuf.len = DATA_BUFSIZE;
  71.                         SI->BytesRECV        = 0;
  72.                         SI->BytesSEND        = 0;
  73.                         SI->IoType                = IORECV;
  74.                         //
  75.                         //仅供测试软件用
  76.                         HeaderMessage recvMsg;
  77.                         if (recv(SI->Socket, (char*)&recvMsg, sizeof(recvMsg), 0) <= 0) 
  78.                         {
  79.                                 printf("初始参数交互失败");
  80.                         }
  81.                         if (CreateIoCompletionPort((HANDLE)SI->Socket, hCompPort, (DWORD)SI, 0) == NULL)
  82.                         {
  83.                                 printf("CreateIoCompletionPort failed with error %d/n", GetLastError());
  84.                                 return FALSE;
  85.                         }
  86.                         //发出一个重叠I/O请求
  87.                         if(WSARecv(SI->Socket, &SI->DataBuf, 1, &Bytes, &Flags, &SI->Overlapped, NULL) == SOCKET_ERROR)
  88.                         {
  89.                                 if(WSAGetLastError() != WSA_IO_PENDING)
  90.                                 {
  91.                                         printf("disconnect/n");
  92.                                         closesocket(SI->Socket); 
  93.                                         delete SI;
  94.                                         continue;
  95.                                 }
  96.                         }
  97.                 }
  98.                 
  99.         }
  100. return FALSE;
  101. }
  102. DWORD WINAPI WorkThread(LPVOID CompletionPortID)
  103. {
  104.         HANDLE hCompPort = (HANDLE)CompletionPortID;
  105.         while (TRUE)
  106.         {
  107.                 DWORD BytesTransferred = 0;
  108.                 LPSOCKET_INFORMATION SI = NULL;
  109.                 LPWSAOVERLAPPED Overlapped = NULL;
  110.                 //线程进入线程池,等待被唤醒
  111.                 if (GetQueuedCompletionStatus(hCompPort, &BytesTransferred, (LPDWORD)&SI, &Overlapped, INFINITE))
  112.                 {
  113.                         if (0 == BytesTransferred && IOQUIT != SI->IoType)
  114.                         {
  115.                                 printf("disconnect/n");
  116.                                 closesocket(SI->Socket); 
  117.                                 delete SI;
  118.                                 continue;
  119.                         }
  120.                         switch(SI->IoType)
  121.                         {
  122.                         case IORECV:
  123.                                 {
  124.                                         //目前的功能是将接收到的数据原封不动的返回
  125.                                         SI->DataBuf.len = BytesTransferred;
  126.                                         SI->BytesRECV = BytesTransferred;
  127.                                         SI->IoType = IOSEND;
  128.                                         if (WSASend(SI->Socket, &SI->DataBuf, 1, &Bytes, Flags, &SI->Overlapped, NULL) == SOCKET_ERROR)
  129.                                         {
  130.                                                 if(WSAGetLastError() != WSA_IO_PENDING)
  131.                                                 {
  132.                                                         printf("disconnect/n");
  133.                                                         closesocket(SI->Socket); 
  134.                                                         delete SI;
  135.                                                         continue;
  136.                                                 }
  137.                                         }
  138.                                 break;
  139.                                 }
  140.                         case IOSEND:
  141.                                 {
  142.                                         SI->BytesSEND += BytesTransferred;
  143.                                         //返回是否彻底,若未发完,接着发
  144.                                         if (SI->BytesSEND < SI->BytesRECV)
  145.                                         {
  146.                                                 SI->DataBuf.buf += BytesTransferred; 
  147.                                                 SI->DataBuf.len -= BytesTransferred; 
  148.                                                 SI->IoType = IOSEND;
  149.                                                 if (WSASend(SI->Socket, &SI->DataBuf, 1, &Bytes, Flags, &SI->Overlapped, NULL) == SOCKET_ERROR)
  150.                                                 {
  151.                                                         if(WSAGetLastError() != WSA_IO_PENDING)
  152.                                                         {
  153.                                                                 printf("disconnect/n");
  154.                                                                 closesocket(SI->Socket); 
  155.                                                                 delete SI;
  156.                                                                 continue;
  157.                                                         }
  158.                                                 }
  159.                                         }
  160.                                         else if (SI->BytesSEND > SI->BytesRECV)
  161.                                         {
  162.                                                 printf("BytesSEND:%d > BytesRECV:%d/n",SI->BytesSEND,SI->BytesRECV);
  163.                                                 memset(SI->buffer, 0, DATA_BUFSIZE);
  164.                                                 SI->BytesRECV = 0;
  165.                                                 SI->BytesSEND = 0;
  166.                                                 SI->IoType = IORECV;
  167.                                                 SI->DataBuf.len = DATA_BUFSIZE;
  168.                                                 SI->DataBuf.buf = SI->buffer;
  169.                                         }
  170.                                         else
  171.                                         {
  172.                                                 memset(SI->buffer, 0, DATA_BUFSIZE);
  173.                                                 SI->BytesRECV = 0;
  174.                                                 SI->BytesSEND = 0;
  175.                                                 SI->IoType = IORECV;
  176.                                                 SI->DataBuf.len = DATA_BUFSIZE;
  177.                                                 SI->DataBuf.buf = SI->buffer;
  178.                                                 if (WSARecv(SI->Socket, &SI->DataBuf, 1, &Bytes, &Flags, &SI->Overlapped, NULL) == SOCKET_ERROR)
  179.                                                 {
  180.                                                         if(WSAGetLastError() != WSA_IO_PENDING)
  181.                                                         {
  182.                                                                 printf("disconnect/n");
  183.                                                                 closesocket(SI->Socket); 
  184.                                                                 delete SI;
  185.                                                                 continue;
  186.                                                         }
  187.                                                 }
  188.                                         }
  189.                                 break;
  190.                                 }
  191.                         case IOQUIT:
  192.                                 {
  193.                                         //让线程安全退出
  194.                                         return FALSE;
  195.                                 break;
  196.                                 }
  197.                                 
  198.                         default:
  199.                                 break;
  200.                         }
  201.                 }        
  202.         }        
  203. return FALSE;
  204. }
  205. void main()   
  206. {
  207.         HANDLE hThreads = CreateThread(NULL, 0, AcceptThread, NULL, NULL, NULL); 
  208.         
  209.         WaitForSingleObject(hThreads,INFINITE);
  210.         printf("exit/n");
  211.         CloseHandle(hThreads);

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值