基于事件的重叠IO模型

时间:2021-09-06 00:15:44

 Windows socket重叠IO模型开发。

 

     利用套接字重叠IO模型,应用程序能一次投递一个或多个IO请求,当系统完成IO操作后通知应用程序。该模型以win32异步IO机制为基础。与前面介绍的所有IO模型相比较,该模型是真正意义上的异步IO模型,它能使Windows socket应用程序达到更高的性能。

     关于异步IO机制可以参考:《Windows核心编程系列》谈谈同步设备IO与异步设备IO异步设备IO

     Windows socket重叠IO延续了win32 IO模型。从发送和接收的角度来看,重叠IO模型与前面介绍的Select模型、WSAAsyncSelect模型和WSAEventSelect模型都不同。因为在这三个模型中IO操作还是同步的,例如:在应用程序调用recv函数时,都会在recv函数内阻塞,直到接收数据完毕后才返回。而重叠IO模型会在调用recv后立即返回。等数据准备好后再通知应用程序。

      系统向应用程序发送通知的形式有两种:一是事件通知。二是完成例程。后面将会介绍这两种形式。

     注意:套接字的重叠IO属性不会对套接字的当前工作模式产生影响。创建具有重叠属性的套接字执行重叠IO操作,并不会改变套接字的阻塞模式。套接字的阻塞模式与重叠IO操作不相关。重叠IO模型仅仅对WSASend和WSARecv的行为有影响。 对listen,如果是阻塞模式,直到有客户请求到达时才会返回。这点要特别注意。

     与其他模型的区别

      在Windows socket中,接收数据的过程可以分为:等待数据和将数据从系统复制到用户空间两个阶段。各种IO模型的区别在于这两个阶段上。

     前三个模型的第一个阶段的区别: select模型利用select函数主动检查系统中套接字是否满足可读条件。WSAAsyncSelect模型和WSAEventSelect模型则被动等待系统的通知。

     第二个阶段的区别:此阶段前三个模型基本相同,在将数据从系统复制到用户缓冲区时,线程阻塞。而重叠IO与它们都不同,应用程序在调用输入函数后,只需等待接收系统完成的IO操作完成通知。

与其他模式不同之处:其他模式是通过事件Event 或者 windows消息,来判断是否有信息的到来,
根据信息类型进行处理,如果是read类型则recv接收,以达到异步处理的效果。
重叠IO模式是为每一个客户端开一个线程来recv,当客户端连接后,就开始开线程recv它了,
信息recv完毕后,通过事件Event或者回掉函数来处理接收到的消息,处理完毕后继续recv它。

     套接字重叠IO模型主要有一下相关函数:

     WSASocket():创建套接字。 

     WSASend和WSASendTo:发送数据。

     WSARecv和WSARecvFrom:接收数据。

     WSAIoctl:控制套接字模式。

     AcceptEx:接受连接。

 

     创建套接字

     要在套接字上使用重叠IO模型,在创建套接字时,必须使用WSA_FLAG_OVERLAPPED标志创建。

   

[cpp] view plaincopy
  1. SOCKET s=WSASocket(AF_INET,SOCK_STREAM,IPPROTO_TCP,  
  2.   
  3.                   NULL,0,WSA_FLAG_OVERLAPPED);  


 

     当使用socket函数创建套接字时,会默认设置WSA_FLAG_OVERLAPPED标志。

 

     接收数据

 

     应用程序调用WSARecv和WSARecvFrom函数接收数据。

WSARecv函数 介绍如下:

 

 

[html] view plaincopy
  1. int WSARecv(  
  2.   
  3.      SOCKET s,  
  4.   
  5.      LPWSABUF lpBuffers,  
  6.   
  7.      DWORD dwBufferCount,  
  8.   
  9.      LPDWORD lpNumberOfBytesRecvd,  
  10.   
  11.      LPDWORD lpFlags,  
  12.   
  13.      LPWSWOVERLAPPED lpOverlapped,  
  14.   
  15.      LPWSOVERLAPPED_COMPLETION_ROUTINE lpCompletionROUNTINE);  


 

     s为接收套接字。

     lpBuffers:接受缓冲区。

         dwBufferCount为接收缓冲区数组元素的个数。如果就一个缓冲区,就指定为1.

     lpNumberOfBytesRecvd:如果接收操作立即完成,该参数指明接收数据的字节数。

     lpFlags:标志位 。一般为0.

     lpOverlapped:指向WSAOVERLAPPED结构指针。

     lpCompletionROUTINE:完成例程。

     该函数可以使用一个或多个缓冲区接收数据。如果接收操作未能立即完成。应用程序利用完成例程或是事件对象获得通知。

     如果操作立即完成,该函数返回值为0。lpNumberOfBytesRecvd参数指明接收数据的字节数。如果未能立即完成,函数返回SOCKET_ERROR值。WSAGetLastError返回WSA_IO_PENDING。

      如果lpOverlapped和lpCompletionROUTINE都为NULL,则该套接字作为同步IO套接字使用。

     如果lpCompletionRoutine参数为NULL,当接收完成时,lpOverlapped参数的事件变为触发状态。在应用程序中可以调用WSAWaitForMultipleEvents或者是WSAGetOverlappedResult等待该事件。

      重叠IO采用事件或者是完成例程通知程序异步操作已完成。这可以通过调用WSASend或WSARecv来实现。之所以前面首先介绍WSARecv是因为:我们并不知道客户发送的数据何时到来。当在有客户请求进来时,我们就调用了WSARecv函数,并采用重叠IO模型,并且在收到数据后,再次调用WSARecv这样在任何时刻只要此客户的数据到来我们就会收到通知。如果应用程序调用WSARecv的时间先于数据到达的时间,则数据到达时会直接放入用户的接收缓冲区。这就避免了从系统缓冲区向用户缓冲区的复制操作。对于提高性能很有帮助。

        WSBUF定义如下:

 

[cpp] view plaincopy
  1. typedef struct _WSABUF  
  2.   
  3. {  
  4.   
  5.     u_long len;//缓冲区长度。   
  6.   
  7.      char *buf;//缓冲区。  
  8.   
  9. }WSABUF,*LPWSABUF;  


 

     WSAOVERLAPPED结构。

     WSARecv函数使用WSAOVERLAPPED结构作为参数,将事件对象和重叠IO关联在一起。该结构声明如下:

 

 

[cpp] view plaincopy
  1. typdef struct _WSAOVERLAPPED  
  2.   
  3. {  
  4.   
  5.     DWROD Internal;//错误代码。    
  6.   
  7.     DWORD InternalHigh;//已传输字节数  
  8.   
  9.     DWORD Offset;//低32位文件偏移。  
  10.   
  11.     DWORD OffsetHigh;//高32位文件偏移  
  12.   
  13.      WSAEVENT hEvent;//事件对象句柄。  
  14.   
  15. }WSAOVERLAPPED,*LPWSAOVERLAPPED;  


 

      应用程序可以执行一下步骤将一个事件对象与套接字关联起来:

      1:调用WSACreateEvent创建事件对象。

      2:将该事件赋值给WSAOVERLAPPED结构的hEvent字段。

      3:使用该重叠结构,调用WSASendWSARecv函数。

 

      当重叠操作完成时,重叠IO结构的事件对象变为已触发状态。可以调用WSAWaitForMultipleEvents函数等待该事件发生。关于该函数,前面在介绍WSAEventSelect模型时有详细的介绍,此处不再赘述。注意它最多只能等待64个事件对象。

 

     WSAGetOverlappedResult函数。

     该函数返回在套接字重叠IO的结构:

 

 

[cpp] view plaincopy
  1. bool WSAGetOverlappedResult(  
  2.   
  3.       SOCKET s,  
  4.   
  5.       LPWSAOVERLAPPED lpOverlapped,  
  6.   
  7.       LPDWORD lpcbTransfer,  
  8.   
  9.       BOOL fWait,  
  10.   
  11.       LPDWORD lpdwFlags);  


 

      s为发起重叠操作的套接字。

      lpOverlapped发起重叠操作的WSAOVERLAPPED结构指针。

      lpcbTransfer:实际发送或接收的字节数。

      fWait:函数返回的方式。如果为TRUE,该函数直到重叠IO完成时才返回。当为false时,如果操作仍然处于等待执行状态,则函数返回false。错误代码为WSA_IO_INCOMPLELE

       lpdwFlags:接收完成状态的附加标志。

 

      当函数返回TRUE时,重叠IO已经完成,lpOverlapped指明实际返回的数据。当函数返回false时,重叠IO还未完成。

 

       注意:由于微软已经公布了WSAOVERLAPPEDOVERLAPPED结构的个字节的意义。Internal表示错误代码。InternalHigh表示已传输字节数。因此在调用WSAWaitForMultipleEvents等待事件对象返回后,得到此事件对应的WSAOVERLAPPED结构后,就可以根据这两个字段判断异步IO的执行结果。无需再调用GetOverlappedResult结构。具体请参考《Windows核心编程》。

 

      发送数据

 

      应用程序调用WSASend或者WSASendTo发送数据。

 

      WSASend函数声明如下:

 

 

[cpp] view plaincopy
  1. int WSASend(  
  2.   
  3.       SOCKET s,  
  4.   
  5.       LPWSABUF lpBuffers,  
  6.   
  7.       DWORD dwBufferCount,  
  8.   
  9.       LPDWORD lpNumberOfBytesSent,  
  10.   
  11.       DWORD dwFlags,  
  12.   
  13.       LPWSAOVERLAPPED lpOverlapped,  
  14.   
  15.       LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionROUTINE);  


 

s:为发送套接字。

lpBuffer:指向WSABUF结构指针,用于发送数据。

dwBufferCount:lpBuffers数组中元素数量。

lpNumberOfBytesSent:如果发送立即完成,该参数指明发送字节数。

lpFlags:标志位。

lpOverlapped:指向WSAOVERLAPPED结构指针。

lpCompletionROUTINE:完成例程。

 

 事件通知

 

套接字重叠IO的事件通知方法要求事件对象与WSAOVERLAPPED结构关联在一起。当IO操作完成后,该事件对象从未触发状态变为触发状态。在应用程序中先调用WSAWaitForMultipleEvents函数等待该事件的发生。获得该事件对象对应的WSAOVERLAPPED结构后可以根据InternalInternalHigh字段(也可以调用WSAGetOverlappedResult函数)判断IO完成的情况。

 

分为以下步骤:

 

1:创建具有WSAOVERLAPPED标志的套接字。如果调用socket()函数,则默认创建具有WSAOVERLAPPED标志的套接字。如果调用WSASocket函数,需要指定WSAOVERLAPPED标志。

2:为套接字定义WSAOVERLAPPED结构,并清零。

3:调用WSACreateEvent函数创建事件对象,并将该事件句柄分配给WSAOVERLAPPED结构的hEvent字段。

4:调用输入或者输出函数。

5:调用WSAWaitForMultipleEvents函数等待与重叠IO关联在一起的事件变为已触发状态。

6:WSAWaitForMultipleEvents返回后,调用WSAResetEvent函数,将该事件对象恢复为未触发态。

7:调用WSAGetOverlappedResult函数判断重叠IO的完成状态。

 

步骤如下:

    (1)创建一个事件句柄表和一个对应的套接字句柄表。还有客户端信息结构体对象,其中成员包括WSARecv函数的参数等等信息

    (2)每创建一个套接字,就创建一个事件对象,把它们的句柄分别放入上面的两个表中,创建overloap对象,为客户端套接字调用WSARecv

    (3)调用WSAWaitForMultipleEvents在客户端事件对象上等待,此函数返回后, WSAResetEvent()重置事;WSAGetOverlappedResult()d得到事件结果,

    (4)处理套接字上的数据,然后再次调用WSARecv。

注意: WSAGetOverlappedResult没有自动重置事件,所以需要手动调用 WSAResetEvent重置事件

下面一个实例演示了使用socket 重叠IO模型开发服务程序的步骤。该程序设计两个线程,接收线程用于接受客户端连接请求,初始化重叠IO操作。服务线程用于重叠IO处理。

 

CClient:该类在前面各模型的介绍中都涉及过。它主要用于管理连接后的客户端。

 

管理客户端链表:存储CClient*,用于管理连接的客户端。

接受线程:

[cpp] view plaincopy
  1. #include"iostream"  
  2. #include"Client.h"  
  3. #pragma  comment(lib,"WS2_32.lib")  
  4. SOCKET sListenSocket;  
  5. UINT totalEvent;//事件数组元素个数。  
  6. WSAEVENT eventArray[WSA_MAXIMUM_WAIT_EVENTS];//事件对象数组。  
  7. bool InitSocket()  
  8. {  
  9.     WSAData wsa;  
  10.     WSAStartup(MAKEWORD(2,2),&wsa);  
  11.     //ServSocket=socket(AF_INET,SOCK_STREAM,0);  
  12.     sListenSocket=WSASocket(AF_INET,SOCK_STREAM,0,NULL,0,WSA_FLAG_OVERLAPPED);  
  13.     if(sListenSocket==INVALID_SOCKET)  
  14.     {  
  15.         return false;  
  16.     }  
  17.     sockaddr_in  addr;  
  18.     addr.sin_family=AF_INET;  
  19.     addr.sin_addr.S_un.S_addr=inet_addr("192.168.1.100");  
  20.     addr.sin_port=htons(4000);  
  21.     int ret=bind(sListenSocket,(SOCKADDR*)&addr,sizeof(addr));  
  22.     if(ret==SOCKET_ERROR)  
  23.     {  
  24.         return false;  
  25.     }  
  26.     ret=listen(sListenSocket,10);  
  27.     if(ret==SOCKET_ERROR)  
  28.     {  
  29.         return false;  
  30.     }  
  31.     return true;  
  32. }  
  33. DWORD WINAPI AcceptThread(PVOID ppram)  
  34. {  
  35.     SOCKET newSocket;  
  36.     while(true)  
  37.     {  
  38.         newSocket=accept(sListenSocket,NULL,NULL);  
  39.           
  40.         if(totalEvent>WSA_MAXIMUM_WAIT_EVENTS)  
  41.         {  
  42.             return -1;  
  43.         }  
  44.         if(WSA_INVALID_EVENT==([totalEvent]=WSACreateEvent()))  
  45.             return -1;  
  46.         CClient*pClient=new CClient(newSocket,eventArray[totalEvent]);  
  47.         //m_list.add(newSocket)//新接受的套接字加入套接字链表,用以对客户端的管理。  
  48.         totalEvent++;  
  49.     }  
  50.     //DeleteAllNode();  
  51.     closesocket(sListenSocket);  
  52.     WSACleanup();  
  53.     return 0;  
  54. }  


服务线程:

 

[cpp] view plaincopy
  1. DWORD WINAPI ServiceThread(PVOID pparam)  
  2. {  
  3.     int dwIndex;  
  4.     while(true)  
  5.     {  
  6.         if((dwIndex=WSAWaitForMultipleEvents(totalEvent,eventArray,false,WSA_INFINITE,false))==WSA_WAIT_FAILED)  
  7.         {  
  8.             return -1;  
  9.         }  
  10.         WSAEVENT h=eventArray[dwIndex-WSA_WAIT_EVENT_0];  
  11.         WSAResetEvent(h);  
  12.         CClient*pClient;  
  13.         //=获得与该套接字对应的CClient指针。  
  14.         pClient=GetClient(h);  
  15.         //判断IO操作完成情况。  
  16.         if(pClient->m_ol.InternalHigh==0)//发生错误或客户端关闭了连接。  
  17.         {  
  18.             //将此节点从客户端链表中删除。  
  19.             //将此节点对应的事件对象从事件对象数组中删除。  
  20.         }  
  21.         else  
  22.         {  
  23.             //1,数据已经到达缓冲区。  
  24.             //2,处理数据。  
  25.             //3,//投递接收数据重叠IO请求,确保任何数据到来前都有一个异步接收IO请求。。  
  26.             pClient->RecvData();  
  27.         }  
  28.   
  29.   
  30.           
  31.     }  
  32.   
  33.   
  34.   
  35.   
  36.     return 0;  
  37. }