事件選擇(WSAEventSelect)模型是另一個有用的異步 I/O 模型。和 WSAAsyncSelect 模型類似的是,它也允許應用程序在一個或多個套接字上,接收以事件為基
礎的網絡事件通知,最主要的差別在於網絡事件會投遞至一個事件對象句柄,而非投遞到一個窗口例程。
事件通知模型要求我們的應用程序針對使用的每一個套接字,首先創建一個事件對象。創建方法是調用 WSACreateEvent 函數,它的定義如下:
WSAEVENT WSACreateEvent(void);
WSACreateEvent 函數的返回值很簡單,就是一個創建好的事件對象句柄,接下來必須將其與某個套接字關聯在一起,同時注冊自己感興趣的網絡事件類型
(FD_READ、FD_WRITE、FD_ACCEPT、FD_CONNECT、FD_CLOSE等),方法是調用 WSAEventSelect 函數,其定義如下:
int WSAEventSelect( __in SOCKET s, //代表感興趣的套接字 __in WSAEVENT hEventObject, //指定要與套接字關聯在一起的事件對象,即用 WSACreateEvent 創建的那一個 __in long lNetworkEvents //對應一個“位掩碼”,用於指定應用程序感興趣的各種網絡事件類型的一個組合。 );
其中參數 lNetworkEvents可以用以下數值進行OR操作
FD_READ 應用程序想要接收有關是否可讀的通知,以便讀入數據
FD_WRITE 應用程序想要接收有關是否可寫的通知,以便寫入數據
FD_ACCEPT 應用程序想接收與進入連接有關的通知
FD_CONNECT 應用程序想接收與一次連接完成的通知
FD_CLOSE 應用程序想接收與套接字關閉的通知
WSACreateEvent 創建的事件有兩種工作狀態,以及兩種工作模式。工作狀態分別是“已傳信”(signaled)和“未傳信”(nonsignaled)。工作模式則包括“人工重
設”(manual reset)和“自動重設”(auto reset)。WSACreateEvent 開始是在一種未傳信的工作狀態,並用一種人工重設模式,來創建事件句柄。隨着網絡事件觸
發了與一個套接字關聯在一起的事件對象,工作狀態便會從“未傳信”轉變成“已傳信”。由於事件對象是在一種人工重設模式中創建的,所以在完成了一個 I/O 請求的處理
之后,我們的應用程序需要負責將工作狀態從已傳信更改為未傳信。要做到這一點,可調用 WSAResetEvent 函數,對它的定義如下:
BOOL WSAResetEvent( __in WSAEVENT hEvent //事件句柄; );
該函數調用是成功還是失敗,會分別返回TRUE或FALSE。
應用程序完成了對一個事件對象的處理后,便應調用WSACloseEvent函數,釋放由事件句柄使用的系統資源。對 WSACloseEvent 函數的定義如下:
BOOL WSACloseEvent( __in WSAEVENT hEvent //事件句柄; );
該函數調用是成功還是失敗,會分別返回TRUE或FALSE。
一個套接字同一個事件對象句柄關聯在一起后,應用程序便可開始I/O處理;方法是等待網絡事件觸發事件對象句柄的工作狀態。WSAWaitForMultipleEvents 函數的設
計宗旨便是用來等待一個或多個事件對象句柄,並在事先指定的一個或所有句柄進入“已傳信”狀態后,或在超過了一個規定的時間周期后,立即返回。下面是
WSAWaitForMultipleEvents 函數的定義:
DWORD WSAWaitForMultipleEvents( __in DWORD cEvents, __in const WSAEVENT* lphEvents, __in BOOL fWaitAll, __in DWORD dwTimeout, __in 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 最多可等待一個網絡事件發生有多長時間,以毫秒為單位,這是一項“超時”設定。超過規定的時間,函數就會
立即返回,即使由 fWaitAll 參數規定的條件尚未滿足也如此。考慮到它對性能造成的影響,應盡量避免將超時值設為0。假如沒有等待處理的事件,
WSAWaitForMultipleEvents 便會返回 WSA_WAIT_TIMEOUT。如 dwTimeout 設為 WSAINFINITE(永遠等待),那么只有在一個網絡事件傳信了一個事件對象
后,函數才會返回。
fAlertable 參數,在我們使用 WSAEventSelect 模型的時候,它是可以忽略的,且應設為 FALSE。該參數主要用於在重疊式 I/O 模型中,在完成例程的處理過程中
使用。
若 WSAWaitForMultipleEvents 收到一個事件對象的網絡事件通知,便會返回一個值,指出造成函數返回的事件對象。這樣一來,我們的應用程序便可引用事件數組
中已傳信的事件,並檢索與那個事件對應的套接字,判斷到底是在哪個套接字上,發生了什么網絡事件類型。對事件數組中的事件進行引用時,應該用
WSAWaitForMultipleEvents 的返回值,減去預定義的值 WSA_WAIT_EVENT_0,得到具體的引用值(即索引位置)。如下例所示:
Index = WSAWaitForMultipleEvents(...);
MyEvent = EventArray[Index - WSA_WAIT_EVENT_0];
知道了造成網絡事件的套接字后,接下來可調用 WSAEnumNetworkEvents 函數,調查發生了什么類型的網絡事件。該函數定義如下:
int WSAEnumNetworkEvents( __in SOCKET s, __in WSAEVENT hEventObject, __out LPWSANETWORKEVENTS lpNetworkEvents );
s 參數對應於造成了網絡事件的套接字。
hEventObject 參數則是可選的;它指定了一個事件句柄,對應於打算重設的那個事件對象。由於我們的事件對象處在一個“已傳信”狀態,所以可將它傳入,令其自動
成為“未傳信”狀態。如果不想用 hEventObject 參數來重設事件,那么可使用 WSAResetEvent 函數,該函數之前已經討論過了。
lpNetworkEvents參數,代表一個指針,指向 WSANETWORKEVENTS 結構,用於接收套接字上發生的網絡事件類型以及可能出現的任何錯誤代碼。
WSANETWORKEVENTS 結構的定義如下:
typedef struct _WSANETWORKEVENTS { long lNetworkEvents; int iErrorCode[FD_MAX_EVENTS]; } WSANETWORKEVENTS, *LPWSANETWORKEVENTS;
lNetworkEvents 參數指定了一個值,對應於套接字上發生的所有網絡事件類型(FD_READ、FD_WRITE 等)。注意:一個事件進入傳信狀態時,可能會同時發生
多個網絡事件類型。例如,一個繁忙的服務器應用可能同時收到 FD_READ 和 FD_WRITE 通知。
iErrorCode 參數指定的是一個錯誤代碼數組,同 lNetworkEvents 中的事件關聯在一起。針對每個網絡事件類型,都存在着一個特殊的事件索引,名字與事件類型的
名字類似,只是要在事件名字后面添加一個“_BIT”后綴字串即可。例如,對 FD_READ 事件類型來說,iErrorCode 數組的索引標識符便是 FD_READ_BIT。下述代碼
片斷對此進行了闡釋(針對FD_READ事件):
if (NetwordEvents.lNetworkEvents & FD_READ) { if (NetworkEvents.iErrorCode[FD_READ_BIT] != 0) { printf("FD_READ failed with error %d\n", NetworkEvents.iErrorCode[FD_READ_BIT]); } }
理論知識講完了,讓我們來看看演示和代碼的實現
服務器端界面
客戶端界面:
服務器和客戶端交互的界面截圖:
下面我們來看看代碼的實現:
1、初始化socket,固定套路
BOOL CWSAEventSelectDlg::InitSocket() { WSAData data; int error; error = WSAStartup(MAKEWORD(2, 2), &data); if (0 != error) { return FALSE; } if(HIBYTE(data.wVersion) != 2 && LOBYTE(data.wVersion))
{
WSACleanup();
return FALSE;
}
return TRUE;
}
2、這里為了簡便,定義一個全局socket數組和event數組,一個socket操作關聯一個event對象
SOCKET socketArray[WSA_MAXIMUM_WAIT_EVENTS]; WSAEVENT eventArray[WSA_MAXIMUM_WAIT_EVENTS]; DWORD dwTotal = 0; //記錄接入socket的數量
3、啟動一個工作線程,核心代碼就在這里,該線程一直循環等待客戶端的連接或者發過來的數據顯示,主線程閑的蛋疼。。。
UINT ThreadProc(LPVOID lpParameter) { CWSAEventSelectDlg *pDlg = (CWSAEventSelectDlg*)lpParameter; ASSERT(pDlg != NULL); SOCKET acceptSocket = INVALID_SOCKET; SOCKET listenSocket = INVALID_SOCKET; WSAEVENT newEvent; WSANETWORKEVENTS NetworkEvents; DWORD Index; TCHAR buf[1024] = {0}; CString cstrMsg; listenSocket = socket(AF_INET, SOCK_STREAM, 0); if (INVALID_SOCKET == listenSocket) { return FALSE; } char ipbuf[1024] = {0}; wcstombs(ipbuf, pDlg->GetIpAddress(), pDlg->GetIpAddress().GetLength()); const char *p = ipbuf; sockaddr_in serverAddress; serverAddress.sin_addr.S_un.S_addr = inet_addr(p); serverAddress.sin_family = AF_INET; serverAddress.sin_port = htons(pDlg->m_iPort); if (SOCKET_ERROR == bind(listenSocket, (sockaddr*)&serverAddress, sizeof(sockaddr_in))) { return FALSE; } newEvent = WSACreateEvent(); WSAEventSelect(listenSocket, newEvent, FD_ACCEPT | FD_CLOSE); socketArray[dwTotal] = listenSocket; eventArray[dwTotal] = newEvent; dwTotal++; listen(listenSocket, SOMAXCONN); pDlg->ShowText(_T("系統消息:服務器開始監聽。。。")); while (TRUE) { Index = WSAWaitForMultipleEvents(dwTotal, eventArray, FALSE, 100, FALSE); if (WSA_WAIT_TIMEOUT == Index) { continue; } WSAEnumNetworkEvents(socketArray[Index - WSA_WAIT_EVENT_0], eventArray[Index - WSA_WAIT_EVENT_0], &NetworkEvents); WSAResetEvent(eventArray[Index - WSA_WAIT_EVENT_0]); if (NetworkEvents.lNetworkEvents & FD_ACCEPT) { if (NetworkEvents.iErrorCode[FD_ACCEPT_BIT] != 0) { continue; } if (dwTotal > WSA_MAXIMUM_WAIT_EVENTS) { pDlg->ShowText(_T("系統消息: 客戶端超過最大連接數。。。")); continue; } acceptSocket = accept(socketArray[Index - WSA_WAIT_EVENT_0], NULL, 0); WSAResetEvent(eventArray[Index - WSA_WAIT_EVENT_0]); newEvent = WSACreateEvent(); WSAEventSelect(acceptSocket, newEvent, FD_READ | FD_WRITE | FD_CLOSE); socketArray[dwTotal] = acceptSocket; eventArray[dwTotal] = newEvent; dwTotal++; pDlg->ShowText(_T("系統消息: 客戶端已經連接成功")); } if (NetworkEvents.lNetworkEvents & FD_READ) { if (NetworkEvents.iErrorCode[FD_READ_BIT] != 0) { pDlg->ShowText(_T("接受客戶端數據失敗。。。")); continue; } recv(socketArray[Index - WSA_WAIT_EVENT_0], (char *)buf, 1024, 0); WSAResetEvent(eventArray[Index - WSA_WAIT_EVENT_0]); cstrMsg = buf; pDlg->ShowText(_T("Client: >") + cstrMsg); } if (NetworkEvents.lNetworkEvents & FD_WRITE) { if (NetworkEvents.iErrorCode[FD_WRITE_BIT] != 0) { continue; } } if (NetworkEvents.lNetworkEvents & FD_CLOSE) { if (NetworkEvents.iErrorCode[FD_CLOSE_BIT] != 0) { continue; } pDlg->ShowText(_T("系統消息: 客戶端退出。。。")); closesocket(socketArray[Index - WSA_WAIT_EVENT_0]); WSACloseEvent(eventArray[Index - WSA_WAIT_EVENT_0]); for (int i = Index - WSA_WAIT_EVENT_0; i < dwTotal; i++) { socketArray[i] = socketArray[i + 1]; eventArray[i] = eventArray[i + 1]; }
dwTotal--; } } return TRUE; }
流程大致是這樣:
1、定義一個socket數組和event數組
2、每一個socket操作關聯一個event對象
3、調用WSAWaitForMultipleEvents函數等待事件的觸發
4、調用WSAEnumNetworkEvents函數查看是哪個一個事件,根據事件找到相應的socket,然后進行相應的處理:比如數據顯示等,同時,記得要將那個event
重置為無信號狀態。
5、循環步驟3和4,直到服務器退出。