和WSAAsyncSelect類似,它也允許應用程序在一個或多個套接字上,接收以事件爲基礎的網絡事件通知。
該模型最主要的區別是在於網絡事件是由對象句柄完成的,而不是通過窗口例程完成。
事件通知
事件通知模型要求應用程序針對打算使用的每一個套接字,首先創建一個事件對象。創建方法是調用WSACreateEvent函數:
- WSAEVENT WSACreateEvent(void);
WSACreateEvent的返回值很簡單,就是一個人工重設的事件對象句柄,一旦得到了事件對象句柄之後,必須將它與某個套接字關聯起來,同時註冊感興趣的網絡事件類型。要做到這一點,方法是調用WSAEventSelect函數:
- int WSAEventSelect(
- SOCKET s,//程序感興趣的套接字
- WSAEVENT hEventObject,//指定要與套接字關聯在一起的事件對象
- long lNetworkEvents//位掩碼,用於指定應用程序感興趣的各種網絡事件類型組合
- );
爲WSAEventSelect創建的事件有兩種工作狀態和兩種工作模式,其中,兩種工作狀態是已傳信(signaled)和爲傳信(non-signaled)。工作模式則包括人工重設和自動重設。WSAEventSelect最初是在一種爲傳信的工作狀態,並用一種人工重設模式,來創建事件句柄。若網絡事件觸發了與一個套接字關聯在一起的事件對象,工作狀態便會從爲傳信變爲已傳信。由於事件對象是在一種人工重設模式下創建的,所有完成了一個I/O請求處理之後,應用程序需要負責將工作模式從已傳信更改爲未傳信,要做到這一點,可調用WSAResetEvent函數:
- BOOL WSAResetEvent(WSAEVENT hEvent);
完成了對某個事件對象的處理之後,便應調用WSACloseEvent函數釋放由事件句柄使用的系統資源。
- BOOL WSACloseEvent(WSAEVENT hEvent);
套接字同一個事件對象句柄關聯在一起後,應用程序便可開始I/O處理,這就需要應用程序等待網絡事件觸發事件對象句柄的工作狀態,WSAWaitForMultipleEvents函數的設計宗旨就是用來等待一個或多個事件對象句柄,並在事先指定的一個或所有句柄進入已傳信狀態後,或在超過了一個規定的時間週期後,立即返回
- DWORD WSAWaitForMultipleEvents(
- DWORD cEvents,
- const WSAEVENT FAR* lphEvents,
- BOOL fWaitAll,
- DWORD dwTimeout,
- BOOL fAlertable
- );
cEvents和lphEvents定義了由WSAEVENT對象構成的一個數組,cEvents指定的是這個數組中事件對象的數量,而lphEvents是一個指針,用於直接引用該數組。要注意的是WSAWaitForMultipleEvents只能支持由WSA_MAXIMUM_WAIT_EVENTS對象規定的一個最大值,在此這個值爲64。因此對於發出 WSAWaitForMultipleEvents調用的每一個線程,該I/O模型一次最多接收64個套接字。假如想讓這個套接字一次管理多於64個套接字,必須創建額外的工作線程,以便等待更多的事件對象。fWaitAll指定 WSAWaitForMultipleEvents如何等待在事件數組中的對象。若將該參數設置爲TRUE,那麼只有等lphEvents數組內包含的所有事件對象都已進入已傳信狀態,函數纔會返回,若設爲FALSE,則任何一個事件對象進入已傳信狀態時,函數就返回。通常應用程序會將該參數設爲FALSE,一次只爲一個套接字事件提供服務。dwTimeout規定了 WSAWaitForMultipleEvents等待一個網絡事件發生時,最多可等待多長時間,以毫秒爲單位,超過規定時間,函數就返回。如果超時值爲0,函數會檢測指定的事件對象狀態,並立即返回。這樣,應用程序可以實現對事件對象的輪詢。如果沒有可處理事件, WSAWaitForMultipleEvents便會返回WSA_WAIT_TIMEOUT,如果dwTimeout被設爲WSA_INFINITE,那麼只有在網絡事件傳信了一個事件對象後,函數纔會返回。fAlertable可被忽略,設爲FALSE。
應該注意到一次只服務一個已傳信事件(fWaitAll設爲FALSE),就可能讓套接字一直“捱餓”,且可能持續到事件數組的末尾。
若WSAWaitForMultipleEvents收到一個事件對象的網絡通知,便會返回一個值,指出造成函數返回的事件對象。這樣,應用程序便可引用事件數組中已傳信的事件,並檢索與那個事件對應的套接字,並判斷到底是那個套接字上,發生了什麼樣的網絡事件。對事件數組中的事件進行引用時,應該用WSAWaitForMultipleEvents的返回值,減去預定義的WSA_WAIT_EVENT_0,從而得到具體的引用值。
- Index = WSAWaitForMultipleEvents(...);
- MyEvent = EventArray[Index - WSA_WAIT_0];
指定了造成網絡事件的套接字後, 接下來可調用WSAEnumNetworkEvents函數,調查發生了那些網絡事件,該函數定義如下:
- int WSAEnumNetworkEvents(
- SOCKET s,//造成網絡事件的套接字
- WSAEVENT hEventObject,//可選參數,指定了一個事件句柄,對應於打算重設的那個事件對象
- LPWSANETWORKEVENTS lpNetworkEvents//指向WSANETWORKEVENTS的指針,用於檢測套接字上發生的網絡事件類型以及可能出現的任何錯誤代碼
- );
WSANETWORKEVENTS結構如下:
- typedef struct _WSANETWORKEVNETS
- {
- long lNetworkEvents;
- inbt iErrorCode[FD_MAX_EVENTS];
- }WSANETWORKEVENTS, FAR* LPWSANETWORKEVENTS;
lNetworkEvents指定一個值,對應於該套接字上發生的所有網絡事件類型
iErrorCode指定了一個錯誤代碼數組,這個數組同lNetworkEvents中的事件關聯在一起,針對每個網絡事件類型,都存在着一個特殊的事件索引,它與事件類型的名稱類似,只是事件類型名稱後面添加一個"_BIT"作爲後綴字符串。例如,對FD_READ事件類型來說,iErrorCode數組的索引標識便是FD_READ_BIT。
- //處理FD_READ通知
- if(NetworkEvents.lNetworkEvents & FD_READ)
- {
- if(NetworkEvents.iErrorCode[FD_READ_BIT]!=0)
- {
- printf("FD_READ failed with error %d \n", NetworkEvents.iErrorCode[FD_READ_BIT]);
- }
- }
演示用WSAEventSelect模型的創建步驟:
- SOCKET SocketArray[WSA_MAXIMUM_WAIT_EVENTS];
- WSAEVENT EventArray[WSA_MAXIMUM_WAIT_EVENTS];
- WSAEVENT NetEvent;
- SOCKADDR_IN addr;
- SOCKET Accept,Listen;
- DWORD EventTotal = 0;
- DWORD Index;
- DOWRD i;
- //創建一個TCP套接字在5050端口上的監聽
- Listen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
- addr.sin_family = AF_INET;
- addr.sin_port = htons(5050);
- addr.sin_addr.s_addr = htonl(INADDR_ANY);
- bind(Listen, (SOCKADDR*)&addr, sizeof(SOCKADDR_IN));
- NetEvent = WSACreateEvent();
- WSAEventSelect(Listen, NetEvent, FD_ACCEPT|FD_CLOSE);
- listen(Listen, 5);
- SocketArray[EventTotal] = Listen;
- EventArray[EventTotal] = NetEvent;
- EventTotal++;
- while(TRUE)
- {
- //等候所有套接字上的網絡事件
- Index = WSAWaitForMultipleEvents(EventTotal, EventArray, FALSE, WSA_INFINITE, FALSE);
- Index = Index - WSA_WAIT_EVENT_0;
- //遍歷所有事件,查看被傳信的事件是否多於一個
- for(i = Index; i<EventTotal; i++)
- {
- Index = WSAWaitForMultipleEvents(1, &EventArray[i], TRUE, 1000, FALSE);
- if((Index==WSA_WAIT_FAILED)||(Index==WSA_WAIT_TIMEOUT))
- {
- continue;
- }
- else
- {
- Index = i;
- WSAEnumNetworkEvents(SocketArray[Index], EventArray[Index], &NetworkEvents);
- //檢測FD_ACCEPT消息
- if(NetworkEvents.lNetworkEvents&FD_ACCEPT)
- {
- if(NetworkEvents.iErrorCode[FD_ACCEPT_BIT]!=0)
- {
- printf("FD_ACCEPT failed with error %d\n", NetworkEvents.iErrorCode[FD_ACCEPT_BIT]);
- break;
- }
- //接收一個新連接,並將它添加到套接字及事件列表中
- Accept = accept(SocketArray[Index], NULL, NULL);
- //無法處理多於WSA_MAXIMUM_WAIT_EVENTS數量套接字,故關閉接收套接字
- if(EventTotal>WSA_MAXIMUM_WAIT_EVENTS)
- {
- printf("Too Many Connections");
- closesocket(Accept);
- break;
- }
- NetEvent = WSACreateEvent();
- WSAEventSelect(Accept, NetEvent, FD_READ|FD_WRITE|FD_CLOSE);
- EventArray[EventTotal] = NetEvent;
- SocketArray[EventTotal] = Accept;
- EventTotal++;
- printf("Socket %d connected \n", Accept);
- }
- //處理FD_READ通知
- if(NetworkEvents.lNetworkEvents&FD_READ)
- {
- if(NetworkEvents.iErrorCode[FD_READ_BIT]!=0)
- {
- printf("FD_READ failed with error %d\n", NetworkEvents.iErrorCode[FD_READ_BIT]);
- break;
- }
- //從套接字讀取數據
- recv(SocketArray[Index-WSA_WAIT_EVENT_0], buffer, sizeof(buffer), 0);
- }
- //處理FD_WRITE通知
- if(NetworkEvents.lNetworkEvents&FD_WRITE)
- {
- if(NetworkEvents.iErrorCode[FD_WRITE_BIT]!=0)
- {
- printf("FD_WRITE failed with error %d\n", NetworkEvents.iErrorCode[FD_WRITE_BIT]);
- break;
- }
- send(SocketArray[Index-WSA_WAIT_EVENT_0], buffer, sizeof(buffer), 0);
- }
- //處理FD_CLOSE通知
- if(NetworkEvents.lNetworkEvents&FD_CLOSE)
- {
- if(NetworkEvents.iErrorCode[FD_CLOSE_BIT]!=0)
- {
- printf("FD_CLOSE failed with error %d\n", NetworkEvents.iErrorCode[FD_CLOSE_BIT]);
- break;
- }
- closesocket(SocketArray[Index]);
- //從Socket和Event數組中刪除套接字及與其關聯的事件,並遞減EventTotal
- CompressArrays(EventArray, SocketArray, &EventTotal);
- }
- }
- }
- }
優勢,概念簡單,不需要窗口環境。
缺點,它每次只等待64個事件,處理多個套接字時,有必要使用一個線程池
=======================================================================
- #include<stdio.h>
- #include<winsock2.h>
- #pragma comment(lib, "ws2_32.lib");
- #define PORT 5050
- #define MSGSIZE 1024
- int g_iTotalConn = 0;
- SOCKET g_CliSocketArr[MAXIMUM_WAIT_OBJECTS];
- WSAEVENT g_CliEventArr[MAXIMUM_WAIT_OBJECTS];
- DWORD WINAPI WorkerThread(LPVOID lpParam);
- void Cleanup(int index);
- int main()
- {
- WSADATA wsaData;
- SOCKET sListen, sClient;
- SOCKADDR_IN local, client;
- DWORD dwThreadId;
- int iAddrSize = sizeof(SOCKADDR_IN);
- WSAStartup(MAKEWORD(2,2), &wsaData);
- sListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
- memset(&local, 0, sizeof(SOCKADDR_IN));
- local.sin_family = AF_INET;
- local.sin_port = htons(PORT);
- local.sin_addr.s_addr = htonl(INADDR_ANY);
- bind(sListen, (SOCKADDR*)&local, sizeof(SOCKADDR_IN));
- listen(sListen, 3);
- CreateThread(NULL, 0, WorkerThread, NULL, 0, &dwThreadId);
- while(TRUE)
- {
- // Accept a connection
- sClient = accept(sListen, (SOCKADDR*)&client, &iAddrSize);
- printf("Accepted client:%s:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
- // Associate socket with network event
- g_CliSocketArr[g_iTotalConn] = sClient;
- g_CliEventArr[g_iTotalConn] = WSACreateEvent();
- WSAEventSelect(g_CliSocketArr[g_iTotalConn], g_CliEventArr[g_iTotalConn], FD_READ|FD_CLOSE);
- g_iTotalConn++;
- }
- return 0;
- }
- DWORD WINAPI WorkerThread(LPVOID lpParam)
- {
- int ret, index;
- WSANETWORKEVENTS NetworkEvents;
- char szMessage[MSGSIZE];
- while (TRUE)
- {
- ret = WSAWaitForMultipleEvents(g_iTotalConn, g_CliEventArr, FALSE, 1000, FALSE);
- if (ret == WSA_WAIT_FAILED || ret == WSA_WAIT_TIMEOUT)
- {
- continue;
- }
- index = ret - WSA_WAIT_EVENT_0;
- WSAEnumNetworkEvents(g_CliSocketArr[index], g_CliEventArr[index], &NetworkEvents);
- if (NetworkEvents.lNetworkEvents & FD_READ)
- {
- // Receive message from client
- ret = recv(g_CliSocketArr[index], szMessage, MSGSIZE, 0);
- if (ret == 0 || (ret == SOCKET_ERROR && WSAGetLastError() == WSAECONNRESET))
- {
- Cleanup(index);
- }
- else
- {
- szMessage[ret] = '\0';
- send(g_CliSocketArr[index], szMessage, strlen(szMessage), 0);
- }
- }
- if (NetworkEvents.lNetworkEvents & FD_CLOSE)
- {
- Cleanup(index);
- }
- }
- return 0;
- }
- void Cleanup(int index)
- {
- closesocket(g_CliSocketArr[index]);
- WSACloseEvent(g_CliEventArr[index]);
- if (index < g_iTotalConn-1)
- {
- g_CliSocketArr[index] = g_CliSocketArr[g_iTotalConn-1];
- g_CliEventArr[index] = g_CliEventArr[g_iTotalConn-1];
- }
- g_iTotalConn--;
- }
事件選擇模型也比較簡單,實現起來也不是太複雜,它的基本思想是將每個套接字都和一個WSAEVENT對象對應起來,並且在關聯的時候指定需要關注的哪些網絡事件。一旦在某個套接字上發生了我們關注的事件(FD_READ和FD_CLOSE),與之相關聯的WSAEVENT對象被Signaled。程序定義了兩個全局數組,一個套接字數組,一個WSAEVENT對象數組,其大小都是MAXIMUM_WAIT_OBJECTS(64),兩個數組中的元素一一對應。
同樣的,這裏的程序沒有考慮兩個問題,一是不能無條件的調用accept,因爲我們支持的併發連接數有限。解決方法是將套接字按 MAXIMUM_WAIT_OBJECTS分組,每MAXIMUM_WAIT_OBJECTS個套接字一組,每一組分配一個工作者線程;或者採用 WSAAccept代替accept,並回調自己定義的Condition Function。第二個問題是沒有對連接數爲0的情形做特殊處理,程序在連接數爲0的時候CPU佔用率爲100%。