探究WSAEventSelect模型

探究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 事件。

發佈了76 篇原創文章 · 獲贊 166 · 訪問量 19萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章