【Windows网络编程】完成端口IOCP原理及案例

来源:互联网 发布:中通快递淘宝价格表 编辑:程序博客网 时间:2024/04/30 14:45

IOCP(I/O Completion Port,I/O完成端口)是性能最好的一种I/O模型。它是应用程序使用线程池处理异步I/O请求的一种机制。在处理多个并发的异步I/O请求时,以往的模型都是在接收请求是创建一个线程来应答请求。这样就有很多的线程并行地运行在系统中。而这些线程都是可运行的,Windows内核花费大量的时间在进行线程的上下文切换,并没有多少时间花在线程运行上。再加上创建新线程的开销比较大,所以造成了效率的低下。

Windows Sockets应用程序在调用WSARecv()函数后立即返回,线程继续运行。当系统接收数据完成后,向完成端口发送通知包(这个过程对应用程序不可见)。

应用程序在发起接收数据操作后,在完成端口上等待操作结果。当接收到I/O操作完成的通知后,应用程序对数据进行处理。

        

完成端口其实就是上面两项的联合使用基础上进行了一定的改进

一个完成端口其实就是一个通知队列,由操作系统把已经完成的重叠I/O请求的通知放入其中。当某项I/O操作一旦完成,某个可以对该操作结果进行处理的工作者线程就会收到一则通知。而套接字在被创建后,可以在任何时候与某个完成端口进行关联。

众所皆知,完成端口是在WINDOWS平台下效率最高,扩展性最好的IO模型,特别针对于WINSOCK的海量连接时,更能显示出其威力。其实建立一个完成端口的服务器也很简单,只要注意几个函数,了解一下关键的步骤也就行了。

分为以下几步来说明完成端口:

0)       同步IO与异步IO

1)       函数

2)       常见问题以及解答

3)       步骤

4)       例程

 

0、同步IO与异步IO

同步I/O首先我们来看下同步I/O操作,同步I/O操作就是对于同一个I/O对象句柄在同一时刻只允许一个I/O操作,原理图如下:

        

由图可知,内核开始处理I/O操作到结束的时间段是T2~T3,这个时间段中用户线程一直处于等待状态,如果这个时间段比较短,则不会有什么问题,但是如果时间比较长,那么这段时间线程会一直处于挂起状态,这就会很严重影响效率,所以我们可以考虑在这段时间做些事情。

异步I/O操作则很好的解决了这个问题,它可以使得内核开始处理I/O操作到结束的这段时间,让用户线程可以去做其他事情,从而提高了使用效率

       

由图可知,内核开始I/O操作到I/O结束这段时间,用户层可以做其他的操作,然后,当内核I/O结束的时候,可以让I/O对象或者时间对象通知用户层,而用户线程GetOverlappedResult来查看内核I/O的完成情况

1、函数

我们在完成端口模型下会使用到的最重要的两个函数是:

CreateIoCompletionPort、GetQueuedCompletionStatus

CreateIoCompletionPort  的作用是创建一个完成端口和把一个IO句柄和完成端口关联起来:

// 创建完成端口

HANDLECompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);

 

// 把一个IO句柄和完成端口关联起来,这里的句柄是一个socket 句柄

CreateIoCompletionPort((HANDLE)sClient,CompletionPort, (DWORD)PerHandleData, 0);

 

其中第一个参数是句柄,可以是文件句柄、SOCKET句柄。

第二个就是我们上面创建出来的完成端口,这里就把两个东西关联在一起了。

第三个参数很关键,叫做PerHandleData,就是对应于每个句柄的数据块。我们可以使用这个参数在后面取到与这个SOCKET对应的数据

最后一个参数给0,意思就是根据CPU的个数,允许尽可能多的线程并发执行。

 

GetQueuedCompletionStatus的作用就是取得完成端口的结果:

// 从完成端口中取得结果

GetQueuedCompletionStatus(CompletionPort,&BytesTransferred, (LPDWORD)&PerHandleData,(LPOVERLAPPED*)&PerIoData, INFINITE)

第一个参数是完成端口

第二个参数是表明这次的操作传递了多少个字节的数据

第三个参数是OUT类型的参数,就是前面CreateIoCompletionPort传进去的单句柄数据,这里就是前面的SOCKET句柄以及与之相对应的数据,这里操作系统给我们返回,让我们不用自己去做列表查询等操作了。

第四个参数就是进行IO操作的结果,是我们在投递WSARecv / WSASend 等操作时传递进去的,这里操作系统做好准备后,给我们返回了。非常省事!!

个人感觉完成端口就是操作系统为我们包装了很多重叠IO的不爽的地方,让我们可以更方便的去使用,下篇我将会尝试去讲述完成端口的原理。

2、常见问题和解答

1)什么是单句柄数据(PerHandle)和单IO数据(PerIO)

单句柄数据就是和句柄对应的数据,像socket句柄,文件句柄这种东西。

单IO数据,就是对应于每次的IO操作的数据。例如每次的WSARecv/WSASend等等

其实我觉得PER是每次的意思,翻译成每个句柄数据和每次IO数据还比较清晰一点。

在完成端口中,单句柄数据直接通过GetQueuedCompletionStatus 返回,省去了我们自己做容器去管理。单IO数据也容许我们自己扩展OVERLAPPED结构,所以,在这里所有与应用逻辑有关的东西都可以在此扩展。

 

2)如何判断客户端的断开

我们要处理几种情况

a)如果客户端调用了closesocket,我们就可以这样判断他的断开:

if(0== GetQueuedCompletionStatus(CompletionPort, &BytesTransferred, 。。。)

{

}

if(BytesTransferred == 0)

{

    // 客户端断开,释放资源

}

b)如果是客户端直接退出,那就会出现64错误,指定的网络名不可再用。这种情况我们也要处理的:

if(0== GetQueuedCompletionStatus(。。。))

{

   if( (GetLastError() == WAIT_TIMEOUT) ||(GetLastError() == ERROR_NETNAME_DELETED) )

   {

        // 客户端断开,释放资源

   }

}

3)什么是IOCP?

我们已经提到IOCP 只不过是一个专门实现用来进行线程间的通信的技术,和信号量(semaphore)相似,因此IOCP并不是一个复杂的概念。一个IOCP 对象是与多个I/O对象关联的,这些对象支持挂起异步IO调用。直到一个挂起的异步IO调用结束为止,一个访问IOCP的线程都有可能被挂起。

完成端口的目标是使CPU保持在满负荷状态下工作。

4)为什么使用IOCP?

使用IOCP,我们可以克服”一个客户端一个线程”的问题。我们知道,这样做的话,如果软件不是运行在一个多核及其上性能就会急剧下降。线程是系统资源,他们既不是无限制的、也不是代价低廉的。

IOCP提供了一种只使用一些(I/O worker)线程去“相对公平地”完成多客户端的”输入输出”。线程会一直被挂起,而不会使用CPU时间片,直到有事情做完为止。

5IOCP是如何工作的?

当使用IOCP时,你必须处理三件事情:a)将一个Socket关联到完成端口;b)创建一个异步I/O调用; c)与线程进行同步。为了获得异步IO调用的结果,比如哪个客户端执行了调用,你必须传入两个参数:pCompletionKey参数和OVERLAPPED结构。

3、步骤

编写完成端口服务程序,无非就是以下几个步骤:

  1、创建一个完成端口

  2、根据CPU个数创建工作者线程,把完成端口传进去线程里

  3、创建侦听SOCKET,把SOCKET和完成端口关联起来

  4、创建PerIOData,向连接进来的SOCKET投递WSARecv操作

  5、线程里所做的事情:

 a、GetQueuedCompletionStatus,在退出的时候就可以使用PostQueudCompletionStatus使线程退出;

 b、取得数据并处理;

4、例程

下面是服务端的例程,可以使用sunxin视频中中的客户端程序来测试服务端。稍微研究一下,也就会对完成端口模型有个大概的了解了。

实例结果服务器、客户端如下:



/*

   完成端口服务器

   接收到客户端的信息,直接显示出来

*/

 

[cpp] view plain copy
  1. <span style="font-size:14px;">#include"winerror.h"  
  2. #include"Winsock2.h"  
  3. #pragmacomment(lib, "ws2_32")  
  4. #include"windows.h"  
  5. #include<iostream>  
  6. usingnamespace std;  
  7.    
  8. /// 宏定义  
  9. #define PORT 5050  
  10. #define DATA_BUFSIZE 8192  
  11.    
  12. #define OutErr(a) cout << (a) << endl \  
  13.       << "出错代码:"<< WSAGetLastError() << endl \  
  14.       << "出错文件:"<< __FILE__ << endl  \  
  15.       << "出错行数:"<< __LINE__ << endl \  
  16.    
  17. #define OutMsg(a) cout << (a) << endl;  
  18.    
  19.    
  20. /// 全局函数定义  
  21.    
  22.    
  23. ///////////////////////////////////////////////////////////////////////  
  24. //  
  25. // 函数名       : InitWinsock  
  26. // 功能描述     : 初始化WINSOCK  
  27. // 返回值       : void  
  28. //  
  29. ///////////////////////////////////////////////////////////////////////  
  30. void InitWinsock()  
  31. {  
  32.        // 初始化WINSOCK  
  33.         WSADATA wsd;  
  34.         if( WSAStartup(MAKEWORD(2, 2), &wsd) != 0)  
  35.         {  
  36.                OutErr("WSAStartup()");  
  37.         }  
  38. }  
  39.    
  40. ///////////////////////////////////////////////////////////////////////  
  41. //  
  42. // 函数名       : BindServerOverlapped  
  43. // 功能描述     : 绑定端口,并返回一个 Overlapped 的ListenSocket  
  44. // 参数         : int nPort  
  45. // 返回值       : SOCKET  
  46. //  
  47. ///////////////////////////////////////////////////////////////////////  
  48. SOCKET BindServerOverlapped(int nPort)  
  49. {  
  50.  // 创建socket  
  51.  SOCKET sServer = WSASocket(AF_INET,SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);  
  52.    
  53.  // 绑定端口  
  54.  struct sockaddr_in servAddr;  
  55.  servAddr.sin_family = AF_INET;  
  56.  servAddr.sin_port = htons(nPort);  
  57.  servAddr.sin_addr.s_addr = htonl(INADDR_ANY);  
  58.    
  59.  if(bind(sServer, (struct sockaddr*)&servAddr, sizeof(servAddr)) < 0)  
  60.  {  
  61.         OutErr("bind Failed!");  
  62.         return NULL;  
  63.  }  
  64.    
  65.  // 设置监听队列为200  
  66.  if(listen(sServer, 200) != 0)  
  67.  {  
  68.         OutErr("listen Failed!");  
  69.         return NULL;  
  70.  }  
  71.  return sServer;  
  72. }  
  73.    
  74.    
  75. /// 结构体定义  
  76. typedef struct  
  77. {  
  78.    OVERLAPPED Overlapped;  
  79.    WSABUF DataBuf;  
  80.    CHAR Buffer[DATA_BUFSIZE];  
  81. }PER_IO_OPERATION_DATA,* LPPER_IO_OPERATION_DATA;  
  82.    
  83.    
  84. typedef struct  
  85. {  
  86.    SOCKET Socket;  
  87. }PER_HANDLE_DATA,* LPPER_HANDLE_DATA;  
  88.    
  89.    
  90. DWORD WINAPI ProcessIO(LPVOID lpParam)  
  91. {  
  92.     HANDLE CompletionPort = (HANDLE)lpParam;  
  93.     DWORD BytesTransferred;  
  94.     LPPER_HANDLE_DATA PerHandleData;  
  95.     LPPER_IO_OPERATION_DATA PerIoData;  
  96.    
  97.  while(true)  
  98.  {  
  99.    
  100.        if(0 == GetQueuedCompletionStatus(CompletionPort,&BytesTransferred, (LPDWORD)&PerHandleData,(LPOVERLAPPED*)&PerIoData, INFINITE))  
  101.        {  
  102.               if( (GetLastError() ==WAIT_TIMEOUT) || (GetLastError() == ERROR_NETNAME_DELETED) )  
  103.               {  
  104.                      cout << "closingsocket" << PerHandleData->Socket << endl;   
  105.                      closesocket(PerHandleData->Socket);  
  106.    
  107.                      delete PerIoData;  
  108.                      delete PerHandleData;  
  109.                      continue;  
  110.               }  
  111.               else  
  112.               {  
  113.                OutErr("GetQueuedCompletionStatus failed!");  
  114.               }  
  115.               return 0;  
  116.        }  
  117.    
  118.        // 说明客户端已经退出  
  119.        if(BytesTransferred == 0)  
  120.        {  
  121.          cout << "closing socket" <<PerHandleData->Socket << endl;  
  122.          closesocket(PerHandleData->Socket);  
  123.          delete PerIoData;  
  124.          delete PerHandleData;  
  125.          continue;  
  126.        }  
  127.    
  128.        // 取得数据并处理  
  129.        cout << PerHandleData->Socket<< "发送过来的消息:" << PerIoData->Buffer<< endl;  
  130.    
  131.        // 继续向 socket 投递WSARecv操作  
  132.        DWORD Flags = 0;  
  133.        DWORD dwRecv = 0;  
  134.        ZeroMemory(PerIoData,sizeof(PER_IO_OPERATION_DATA));  
  135.        PerIoData->DataBuf.buf =PerIoData->Buffer;  
  136.        PerIoData->DataBuf.len = DATA_BUFSIZE;  
  137.        WSARecv(PerHandleData->Socket,&PerIoData->DataBuf, 1, &dwRecv, &Flags,&PerIoData->Overlapped, NULL);  
  138.  }  
  139.    
  140.  return 0;  
  141. }  
  142.    
  143. void main()  
  144. {  
  145.         InitWinsock();  
  146.         HANDLE CompletionPort =CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);  
  147.    
  148.         //根据系统的CPU来创建工作者线程  
  149.         SYSTEM_INFO SystemInfo;  
  150.         GetSystemInfo(&SystemInfo);  
  151.    
  152.         //线程数目=系统进程数目的两倍.  
  153.         for(int i = 0; i <SystemInfo.dwNumberOfProcessors * 2; i++)  
  154.         {  
  155.                HANDLE hProcessIO = CreateThread(NULL, 0,ProcessIO, CompletionPort, 0, NULL);  
  156.                if(hProcessIO)  
  157.                {  
  158.                       CloseHandle(hProcessIO);  
  159.                }  
  160.         }  
  161.    
  162.         //创建侦听SOCKET  
  163.         SOCKET sListen = BindServerOverlapped(PORT);  
  164.    
  165.         SOCKET sClient;  
  166.         LPPER_HANDLE_DATA PerHandleData;  
  167.         LPPER_IO_OPERATION_DATA PerIoData;  
  168.         while(true)  
  169.         {  
  170.                // 等待客户端接入  
  171.                //sClient = WSAAccept(sListen, NULL, NULL, NULL, 0);  
  172.                sClient = accept(sListen, 0, 0);  
  173.                cout << "Socket " << sClient << "连接进来"<< endl;  
  174.    
  175.                PerHandleData = new PER_HANDLE_DATA();  
  176.                PerHandleData->Socket = sClient;  
  177.    
  178.                // 将接入的客户端和完成端口联系起来  
  179.                CreateIoCompletionPort((HANDLE)sClient, CompletionPort,(DWORD)PerHandleData, 0);  
  180.    
  181.                // 建立一个Overlapped,并使用这个Overlapped结构对socket投递操作  
  182.                PerIoData = new PER_IO_OPERATION_DATA();  
  183.    
  184.                ZeroMemory(PerIoData, sizeof(PER_IO_OPERATION_DATA));  
  185.                PerIoData->DataBuf.buf = PerIoData->Buffer;  
  186.                PerIoData->DataBuf.len = DATA_BUFSIZE;  
  187.    
  188.                // 投递一个WSARecv操作  
  189.                DWORD Flags = 0;  
  190.                DWORD dwRecv = 0;  
  191.                WSARecv(sClient, &PerIoData->DataBuf, 1, &dwRecv, &Flags,&PerIoData->Overlapped, NULL);  
  192.         }  
  193.    
  194.        DWORD dwByteTrans;  
  195.        //将一个已经完成的IO通知添加到IO完成端口的队列中.  
  196.         //提供了与线程池中的所有线程通信的方式.  
  197.         PostQueuedCompletionStatus(CompletionPort,dwByteTrans, 0, 0);  //IO操作完成时接收的字节数.  
  198.           
  199.         closesocket(sListen);  
  200. }</span>  

 

/*--------------------------------------------

**---------客户端例程序-----------------------

---------------------------------------------*/

[cpp] view plain copy
  1. <span style="font-size:14px;">#include<stdio.h>  
  2. #include<Winsock2.h>  
  3. #define MAXCNT 30000  
  4. void main()  
  5. {  
  6.        WORD wVersionRequested;  
  7.        WSADATA wsaData;  
  8.        int err;  
  9.         
  10.        wVersionRequested = MAKEWORD( 2, 2);  
  11.         
  12.        err = WSAStartup( wVersionRequested,&wsaData );//WSAStartup()加载套接字库  
  13.        if ( err != 0 ) {  
  14.                
  15.               return;  
  16.        }  
  17.         
  18.        if ( LOBYTE( wsaData.wVersion ) != 2 ||  
  19.               HIBYTE( wsaData.wVersion ) != 2 ){  
  20.               WSACleanup( );  
  21.               return;  
  22.        }  
  23.    
  24.        static int nCnt = 0;  
  25.        char sendBuf[2000];  
  26. //     char recvBuf[100];  
  27.        while(nCnt < MAXCNT)  
  28.        {  
  29.               SOCKETsockClient=socket(AF_INET,SOCK_STREAM,0);  
  30.               SOCKADDR_IN addrSrv;  
  31.               addrSrv.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");//本地回路地址127,用于一台机器上测试的IP  
  32.               addrSrv.sin_family=AF_INET;  
  33.               addrSrv.sin_port=htons(5050);//和服务器端的端口号保持一致  
  34.               connect(sockClient,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR));//连接服务器端(套接字,地址转换,长度)  
  35.         
  36.    
  37.               sprintf(sendBuf,"This is TestNo : %d\n",++nCnt);  
  38.               send(sockClient,sendBuf,strlen(sendBuf)+1,0);//向服务器端发送数据,"+1"是为了给'\0'留空间  
  39.               printf("send:%s",sendBuf);  
  40.    
  41. //           memset(recvBuf,0,100);  
  42. //           recv(sockClient,recvBuf,100,0);//接收数据  
  43. //           printf("%s\n",recvBuf);//打印  
  44.                
  45.               closesocket(sockClient);//关闭套接字,释放为这个套接字分配的资源  
  46.               Sleep(1);  
  47.        }  
  48.        WSACleanup();//终止对这个套接字库的使用  
  49. }</span>  
[cpp] view plain copy
  1. <pre></pre>  
  2. <pre></pre>  
  3. <pre></pre>  
  4. <link rel="stylesheet" href="http://static.blog.csdn.net/public/res-min/markdown_views.css?v=2.0">  
  5.               
原创粉丝点击