一、什么是事件選擇模型
事件選擇(WSAEventSelect)模型是另一個有用的異步 I/O 模型。和 WSAAsyncSelect 模型類似的是,它也允許應用程序在一個或多個套接字上,接收以事件為基礎的網絡事件通知,最主要的差別在於網絡事件會投遞至一個事件對象句柄,而非投遞到一個窗口例程。
每一個socket都配備一個event,開發者可以為event注冊對應的網絡事件,當
有事件來的時候,對應socket的event就會變成有信號狀態。
二、事件選擇模型API函數
事件通知模型要求我們的應用程序針對使用的每一個套接字,首先創建一個事件對象。創建方法是調用 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(永遠等待),那么只有在一個網絡事件傳信了一個事件對象
后,函數才會返回。
三、代碼示例
客戶端服務器收發數據進行通信:
server:
#include <Winsock2.h> #include <windows.h> #include "TcpSocket.h" #include "CLock.h" #include <vector> using namespace std; CLock g_lock; bool HandleData(SOCKET sockClient) { // 5) 收發數據 char aryBuff[MAXWORD] = { 0 }; int nRet = recv(sockClient, aryBuff, sizeof(aryBuff), 0); if (nRet == 0 || nRet == SOCKET_ERROR) { printf("接受數據失敗 \n"); return false; } printf("收到數據: %s \n", aryBuff); char szBuff[] = { "recv OK \r\n" }; nRet = send(sockClient, szBuff, sizeof(szBuff), 0); if (nRet == SOCKET_ERROR) { printf("數據發送失敗 \n"); return false; } return true; } //線程, 用來處理客戶端, 和客戶端進行數據的收發 DWORD WINAPI HandleClientsThread(LPVOID pParam) { //vector<SOCKET>& vctClients =*(vector<SOCKET>*)pParam; vector<pair<SOCKET, WSAEVENT>>& vctClients = *(vector<pair<SOCKET, WSAEVENT>>*)pParam; while (TRUE) { /* fd_set fdRead; FD_ZERO(&fdRead); //初始化 */ WSAEVENT aryEvents[WSA_MAXIMUM_WAIT_EVENTS]; int nCount = 0; //把所有客戶端加入數組 g_lock.Lock(); for (auto& pairSockEvent : vctClients) { //FD_SET(sock, &fdRead); aryEvents[nCount++] = pairSockEvent.second; } g_lock.UnLock(); /* timeval tv = { 1, }; int nRet = select(fdRead.fd_count, &fdRead, NULL, NULL, &tv); if (nRet == 0 || nRet==SOCKET_ERROR) { continue; } */ //檢測指定的socket int nRet = WSAWaitForMultipleEvents(nCount, aryEvents, FALSE, 1000, FALSE); if (nRet == WSA_WAIT_TIMEOUT) //超時繼續等待 { continue; } //處理數據 g_lock.Lock(); for (auto itr = vctClients.begin(); itr != vctClients.end(); itr++) { //判斷sock是否可以讀數據 //if (FD_ISSET(*itr, &fdRead)) //判斷socket是否是可以讀數據了 if (itr->second == aryEvents[nRet]) { WSANETWORKEVENTS workevent; WSAEnumNetworkEvents(itr->first, itr->second, &workevent); if (workevent.lNetworkEvents & FD_READ) { //if (itr->second == aryEvents[nRet]); if (!HandleData(itr->first)) { //連接斷開 vctClients.erase(itr); break; } } else if (workevent.lNetworkEvents & FD_CLOSE) { vctClients.erase(itr); } } } g_lock.UnLock(); } return 0; }
client
int main() { //1.創建socket SOCKET sockServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP ); //2.綁定端口 sockaddr_in siServer; siServer.sin_family = AF_INET; siServer.sin_port = htons(9527); siServer.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); int nRet = bind(sockServer, (sockaddr*)&siServer, sizeof(siServer)); if (nRet == SOCKET_ERROR) { printf("端口綁定失敗\n"); return 0; } //3.監聽 nRet = listen(sockServer, SOMAXCONN);//監聽最大值 if (nRet == SOCKET_ERROR) { printf("監聽失敗\n"); return 0; } //創建線程,檢測socket是否有數據可讀並處理 //vector<SOCKET> vctClients; vector<pair<SOCKET, WSAEVENT>>vctClients; HANDLE hTread = CreateThread(NULL, 0, HandleClientsThread, (LPVOID)&vctClients, 0, NULL); CloseHandle(hTread); while (true) { // 4) 接受連接 sockaddr_in siClient; int nSize = sizeof(siClient); printf("客戶端已就緒,等待連接"); SOCKET sockClient = accept(sockServer, (sockaddr*)&siClient, &nSize); if (sockClient == SOCKET_ERROR) { printf("接受連接失敗 \r\n"); return 0; } printf("IP:%s port:%d 連接到服務器. \r\n", inet_ntoa(siClient.sin_addr), ntohs(siClient.sin_port)); g_lock.Lock(); WSAEVENT hEvent = WSACreateEvent(); WSAEventSelect(sockClient, hEvent, FD_READ | FD_CLOSE); vctClients.push_back(make_pair(sockClient, hEvent)); g_lock.UnLock(); } return 0; }