WSAEventSelect模型 用法介绍

时间:2021-07-04 00:06:42

WSAEventSelect模型

WSAEventSelect模型也是I/O模型中较为常用的一个异步模型,它也允许应用程序在一个或多个套接字上,接收以事件为基础的网络事件通知。该模型最主要是将网络事件投递至一个事件对象句柄。

   事件通知
   事件通知模型要求我们的应用程序针对打算使用的每一个套接字,首先创建一个事件对象。创建方法是调用WSACreateEvent函数,它的定义如下: WSAEVENT WSACreateEvent(void);
   函数的返回值很简单,就是一个创建好的事件对象句柄。事件对象句柄到手后,接下来必须将其与某个套接字关联在一起,同时注册自己感兴趣的网络事件类型。调用WSAEventSelect来做到这一点,定义如下:

   int WSAEventSelect(

       SOCKET s,  // 定义的套接字

       WSAEVENT hEventObject, // 与套接字关联在一起的事件对象句柄

       long lNetworkEvent // 应用程序感兴趣的各种网络事件类型的一个组合(FD_READ|FD_WRITE|FD_ClOSE)

   );

   WSAEventSelect创建的事件拥有两种工作状态,以及两种工作模式。其中,两种工作状态分别是“已传信”(signaled)和“未传信”(non signaled)。工作模式则包括“人工重设”(manual reset)和“自动重设”(auto reset)。WSAEventSelect最开始在一种未传信的工作状态中,并用一种人工重设模式,来创建事件句柄。随着网络事件触发了与一个套接字关联在一起的事件对象,工作状态便会从“未传信”转变成“已传信”。由于事件对象是在一种人工重设模式中创建的,所以在完成了一个I/O请求的处理之后,我们的应用程序需要负责将工作状态从已传信更改为未传信。要做到这一点,可调用WSAResetEvent函数,对它的定义如下:BOOL WSAResetEvent(WSAEVENT hEvent);唯一的参数是前面用WSACreateEvent函数创建的事件对象句柄,成功返回TRUE,失败返回FALSE。当应用程序完成了对一个事件对象的处理后,应调用BOOL WSACloseEvent(WSAEVENT hEvent);函数释放由hEvent句柄占用的系统资源。成功返回TRUE,失败返回FALSE。

   一个套接字同一个事件对象句柄关联在一起后,应用程序便可开始I/O处理;方法是等待网络事件触发事件对象句柄的工作状态。WSAWaitForMultipleEvents函数的设计宗旨便是用来等待一个或多个事件对象句柄,并在事先指定的一个或所有句柄进入“已传信”状态后,或在超过了一个规定的时间周期后,立即返回。定义如下:

    DWORD WSAWaitForMultipleEvents(

          DWORD cEvents, 

          const WSAEVENT FAR * lphEvents,

          BOOL fWaitAll,

          DWORD dwTimeout,

          BOOL fAlertable

     );

    其中,cEvents和lphEvents参数定义了由WSAEVENT对象构成的一个数组。在这个数组中,cEvents指定的是事件对象的数量,而lphEvents对应的是一个指针,用于直接引用该数组。要注意的是,WSAWaitForMultipleEvents只能支持由WS AMAXIMUMWAITEVENTS对象规定的一个最大值,在此定义成64个。因此,针对发出WSAWaitForMultipleEvents调用的每个线程,该I/O模型一次最多都只能支持64个套接字。假如想让这个模型同时管理不止64个套接字,必须创建额外的工作者线程,以便等待更多的事件对象。f WaitAll参数指定了WSAWaitForMultipleEvents如何等待在事件数组中的对象。若设为T RUE,那么只有等lphEvents数组内包含的所有事件对象都已进入“已传信”状态,函数才会返回;但若设为FALSE,任何一个事件对象进入“已传信”状态,函数就会返回。就后一种情况来说,返回值指出了到底是哪个事件对象造成了函数的返回。通常,应用程序应将该参数设为FALSE,一次只为一个套接字事件提供服务。dwTimeout参数规定了WSAWaitForMul tipleEvents最多可等待一个网络事件发生有多长时间,以毫秒为单位,这是一项“超时”设定。超过规定的时间,函数就会立即返回,即使由fWaitAll参数规定的条件尚未满足也如此。如超时值为0,函数会检测指定的事件对象的状态,并立即返回。这样一来,应用程序实际便可实现对事件对象的“轮询”。但考虑到它对性能造成的影响,还是应尽量避免将超时值设为0。假如没有等待处理的事件,WSAWaitForMultipleEvents便会返回WSAWAITTIMEOUT。如dwsTimeout设为WSA INFINITE(永远等待),那么只有在一个网络事件传信了一个事件对象后,函数才会返回。最后一个参数是fAlertable,在我们使用WSAEventSelect模型的时候,它是可以忽略的,且应设为FALSE。该参数主要用于在重叠式I/O模型中,在完成例程的处理过程中使用。

  若WSAWaitForMultipleEvents收到一个事件对象的网络事件通知,便会返回一个值,指出造成函数返回的事件对象。这样一来,我们的应用程序便可引用事件数组中已传信的事件,并检索与那个事件对应的套接字,判断到底是在哪个套接字上,发生了什么网络事件类型。对事件数组中的事件进行引用时,应该用WSAWaitForMultipleEvents的返回值,减去预定义值WSAWAITEVENT0,得到具体的引用值(即索引位置)。如下例所示:

   nIndex = WSAWaitForMultipleEvents(......);

   hEvent = hEventArray[nIndex - WSA_WAIT_EVENT_0];

   知道了造成网络事件发生的套接字后,调用WSAEnumNetworkEvents函数,调查发生了什么类型的网络事件,定义如下:

   int WSAEnumNetworkEvents(

       SOCKET s,  // 网络事件发生相关联的套接字

       WSAEVENT hEventObject, //重设事件对象,将处在“已传信”改为“未传信”,亦可使用WSAResetEvent替代

       LPWSAMETWORKEVENTS lpNetworkEvents // 接收套接字上发生的网络事件类型以及可能出现的错误代码

   );

   typedef struct _WSANETWORKEVENTS {

          long lNetworkEvent;

          int iErrorCode[FD_MAX_EVENTS];

    }WSANETWORKEVENTS, FAR * LPWSANETWORKEVENTS;

    lNetworkEvents参数指定了一个值,对应于套接字上发生的所有网络事件类型。注意一个事件进入传信状态时,可能会同时发生多个网络事件类型。例如,一个繁忙的服务器应用可能同时收到FD_READ和FD_WRITE通知。iErrorCode参数指定的是一个错误代码数组,同lNetworkEvents中的事件关联在一起。针对每个网络事件类型,都存在着一个特殊的事件索引,名字与事件类型的名字类似,只是要在事件名字后面添加一个“_BIT”后缀字串即可。例如,对FD_READ事件类型来说,iErrorCode数组的索引标识符便是FD_READ_BIT。下述代码片断对此进行了阐释(针对FDREAD事件):

    if (NetworkEvents.lNetworkEvents & FD_READ) {

        if (NetworkEvents.iErrorCode[FD_READ_BIT] != 0) {

        }

    }

    完成了对WSANETWORKEVENTS结构中的事件的处理之后,我们的应用程序应在所有可用的套接字上,继续等待更多的网络事件,以下是一段服务器端代码(来自与邮电出版社《Windows网络程序设计》):

int main()
{
    // 事件句柄和套接字句柄
    WSAEVENT eventArray[WSA_MAXIMUM_WAIT_EVENTS];
    SOCKET  sockArray[WSA_MAXIMUM_WAIT_EVENTS];
    int nEventTotal = 0;

    USHORT nPort = 4567; // 监听端口号

    // 创建监听套接字
    SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    sockaddr_in sin;
    sin.sin_family = AF_INET;
    sin.sin_port = htons(nPort);
    sin.sin_addr.S_un.S_addr = INADDR_ANY;
    if(::bind(sListen, (sockaddr*)&sin, sizeof(sin)) == SOCKET_ERROR)
    {
        printf(" Failed bind() /n");
        return -1;
    }
    ::listen(sListen, 200);

    // 创建事件对象,并关联到新的套接字
    WSAEVENT event = ::WSACreateEvent();
    ::WSAEventSelect(sListen, event, FD_ACCEPT|FD_CLOSE);
    // 添加到列表中
    eventArray[nEventTotal] = event;
    sockArray[nEventTotal] = sListen;
    nEventTotal++;

    char szText[512];

    // 处理网络事件
    while(TRUE)
    {
        // 在所有对象上等待
        int nIndex = ::WSAWaitForMultipleEvents(nEventTotal, eventArray, FALSE, WSA_INFINITE, FALSE);
        // 确定事件的状态
        nIndex = nIndex - WSA_WAIT_EVENT_0;

        // WSAWaitForMultipleEvents总是返回所有事件对象的最小值,为了确保所有的事件对象得到执行的机会,对 大于nIndex的事件对象进行轮询,使其得到执行的机会。
        for(int i=nIndex; i<nEventTotal; i++)
        {
            nIndex = ::WSAWaitForMultipleEvents(1, &eventArray[i], TRUE, 0, FALSE);
            if(nIndex == WSA_WAIT_FAILED || nIndex == WSA_WAIT_TIMEOUT)
            {
                continue;
            }
            else
            {
                // 获取到来的通知消息,WSAEnumNetworkEvents函数会自动重置受信事件
                WSANETWORKEVENTS event;
                ::WSAEnumNetworkEvents(sockArray[i], eventArray[i], &event);
                if(event.lNetworkEvents & FD_ACCEPT)    // 处理FD_ACCEPT事件
                {
                    if(event.iErrorCode[FD_ACCEPT_BIT] == 0)
                    {
                        if(nEventTotal > WSA_MAXIMUM_WAIT_EVENTS)
                        {
                           printf(" Too many connections! /n");
                           continue;
                        }
                        SOCKET sNew = ::accept(sockArray[i], NULL, NULL);
                        WSAEVENT event = ::WSACreateEvent();
                        ::WSAEventSelect(sNew, event, FD_READ|FD_CLOSE|FD_WRITE);
                        // 添加到列表中
                        eventArray[nEventTotal] = event;
                        sockArray[nEventTotal] = sNew;
                        nEventTotal++;
                    }
                }
                else if(event.lNetworkEvents & FD_READ)   // 处理FD_READ事件
                {
                    if(event.iErrorCode[FD_READ_BIT] == 0)
                    {
                        memset(szText, 0x01, sizeof(szText));
                        int nRecv = ::recv(sockArray[i], szText, strlen(szText), 0);
                        if(nRecv > 0)   
                        {
                            szText[nRecv] = '/0';
                            printf("%s /n", szText);
                        }
                    }
                }
                else if(event.lNetworkEvents & FD_CLOSE)  // 处理FD_CLOSE事件
                {
                    if(event.iErrorCode[FD_CLOSE_BIT] == 0)
                    {
                       ::closesocket(sockArray[i]);
                       for(int j=i; j<nEventTotal-1; j++)
                       {
                           sockArray[j] = sockArray[j+1];
                           sockArray[j] = sockArray[j+1];
                       }
                       EventTotal--;
                    }
               }
               else if(event.lNetworkEvents & FD_WRITE)  // FD_WRITE事件破难理解,下面将重点说明。
               {
               }
             }
          }
       }
       return 0;
}

    以上介绍了WSAEventSelect模型的流程和用法,大部分内容参考<<Windows网络编程>>。该模型也较易理解,使用比较普遍,但FD_WRITE事件的触发时机曾经非常使我困惑,书写了很多测试用例,总是不能得到满意的输出结果。一直认为,如果某一端(客户端或服务器端)调用recv阻塞接收,哪在另一段(服务器端或客户端)一定会触发FD_WRITE,进而用send来发送数据,后来查阅了MSDN和一些相关材料,才发现这一想法大错特错。FD_WRITE并非针对send的,一般是在连线成功后会触发一次或者缓冲区有多出的空位, 可以容纳需要发送的数据时才会触发。

   FD_READ 事件非常容易掌握. 当有数据发送过来时, WinSock会以FD_READ事件通知你, 对于每一个FD_READ事件,你需要像下面这样调用recv(): int nRecvData = recv(wParam, &data, sizeof(data), 0); 基本上就是这样, 别忘了修改上面的wParam。还有,不一定每一次调用recv()都会接收到一个完整的数据包, 因为数据可能不会一次性全部发送过来. 所以在开始处理接收到的数据之前, 最好对接收到的字节数(即recv()的返回值)进行判断,看看是否收到的是一个完整的数据包。

  FD_WRITE相对来说就麻烦一些。首先,当你建立了一个连接时,会产生一个FD_WRITE事件。但是如果你认为在收到 FD_WRITE时调用send()就万事大吉,那就错了。FD_WRITE事件只在发送缓冲区有多出的空位,可以容纳需要发送的数据时才会触发。

  上面所谓的发送缓冲区,是指系统底层提供的缓冲区。send()先将数据写入到发送缓冲区中,然后通过网络发送到接收端。你或许会想,只要不把发送缓冲区填满,让发送缓冲区保持足够多的空位容纳需要发送的数据,那么你就会源源不断地收到FD_WRITE事件了。嘿嘿,错了。上面只是说FD_WRITE事件在发送缓冲区有多出的空位时会触发,但不是在有足够的空位时触发,就是说你得先把发送缓冲区填满。

  通常的办法是在一个无限循环中不断的发送数据,直到把发送缓冲区填满。当发送缓冲区被填满后,send()将会返回 SOCKET_ERROR,WSAGetLastError()会返回WSAWOULDBLOCK。如果当前这个SOCKET处于阻塞(同步)模式,程序会一直等待直到发送缓冲区空出位置然后发送数据;如果SOCKET是非阻塞(异步)的,那么你就会得到WSAWOULDBLOCK错误。于是只要我们首先循环调用send()直到发送缓冲区被填满,然后当缓冲区空出位置来的时候,系统就会发出FD_WRITE事件。下面是一个处理FD_WRITE事件的例子。

   case FD_WRITE:  // 可以发送数据了
   {
      // 进入无限循环
      while(TRUE)
      {
          // 从文件中读取数据,保存到packet.data里面.
          in.read((char*)&packet.data,MAX_PACKET_SIZE);

          // 发送数据
          if (send(wparam, (char*)(&packet), sizeof(PACKET), 0) == SOCKET_ERROR)
          {
              if (WSAGetLastError() == WSAEWOULDBLOCK)
              {
                  // 发送缓冲区已经满了, 退出循环.
                  break;
              }
              else // 其他错误
              {
                 // 显示出错信息然后退出.
                 CleanUp();
                 return(0);
              }
           }
        }
     } break;

    看到了吧,实现其实一点也不困难。只是弄混了一些概念而已。使用这样的发送方式,在发送缓冲区变满的时候就可以退出循环。然后,当缓冲区空出位置来的时候,系统会触发另外一个FD_WRITE事件,于是你就可以继续发送数据了。

    在你开始使用新学到的知识之前,我还想说明一下FD_WRITE事件的使用时机。如果你不是一次性发送大批量的数据的话,就别想着使用FD_WRITE事件了,原因很简单-如果你寄期望于在收到FD_WRITE事件时发送数据,但是却又不能发送足够的数据填满发送缓冲区,那么你就只能收到连接刚刚建立时触发的那一次FD_WRITE-系统不会触发更多的FD_WRITE了。所以当你只是发送尽可能少的数据的时候,就忘掉 FD_WRITE 机制吧,在任何你想发送数据的时候直接调用send()。

    以上部分是我在CSDN上看到的一篇文章,文章写得很易懂。其实,如果你想收到FD_WRITE事件而你又无法先填满发送缓冲区,可以调用WSAAsyncSelect( ..., FD_WRITE)。如果当前发送缓冲区有空位,系统会马上给你发FD_WRITE 事件。