目錄
引言
一、WSAEventSelect模型概述?
二、 WSAEventSelect模型的實現流程
2.1 創建一個事件對象,注冊網絡事件
2.2 等待網絡事件發生
2.3 獲取網絡事件
2.4 手動設置信號量和釋放資源
三、 WSAEventSelect模型偽代碼示例
四、完整實踐示例代碼
引言
????????在網絡編程的復雜世界里,如何高效捕捉和響應網絡事件是構建穩健應用的關鍵。WSAEventSelect模型作為一種強大的異步I/O模型,為開發者開辟了一條獨特路徑。它借助事件驅動機制,讓應用程序能夠敏銳感知套接字上的網絡動態。與WSAAsyncSelect模型不同,它以事件而非消息形式傳遞通知,為網絡編程帶來別樣靈活性。接下來,讓我們深入探索WSAEventSelect模型的工作原理、實現流程以及實際應用示例。
一、WSAEventSelect模型概述?
????????Windows Sockets異步事件選擇模型,也就是WSAEventSelect模型,屬于另一種異步I/O模型。利用這個模型,應用程序能夠在單個或多個套接字上,基于事件接收網絡方面的通知。
????????WSAEventSelect模型和WSAAsyncSelect模型有所不同,主要區別就在于應用程序接收網絡事件通知的方式。WSAEventSelect模型通過事件來告訴應用程序網絡事件發生了,而WSAAsyncSelect模型是依靠消息來通知。但從根本上來說,在應用程序接收網絡事件通知這件事上,這兩個模型都是被動的。意思就是,只有網絡事件真正發生的時候,系統才會向應用程序發出通知 。?
二、 WSAEventSelect模型的實現流程
初始化套接字、綁定端口及IP、監聽這前三步在此略過。
2.1 創建一個事件對象,注冊網絡事件
????????在應用程序調用WSAEventSelect函數之前,必須先創建一個事件對象。當獲取到一個socket后,需使用WSACreateEvent函數來創建事件對象,之后再使用WSAEventSelect函數進行相關操作。
WSAEVENT WSACreateEvent(void);
該函數的返回值為事件對象句柄。
int WSAEventSelect(SOCKET s, //當前服務端的SOCK句柄WSAEVENT hEventObject, //事件對象句柄long lNetworkEvents //網絡事件
);
?????????注:當調用WSAEventSelect函數后,套接字會被自動設置為非阻塞模式。若要將套接字設置為阻塞模式,則必須把參數lNetworkEvents設置為0。
示例:
//創建一個事件對象
WSAEVENT wsaEvent = WSACreateEvent();
if (WSA_INVALID_EVENT == wsaEvent) {printf("創建一個事件對象失敗!");closesocket(sSocket);WSACleanup();
}
//注冊網絡事件
if (WSAEventSelect(sSocket, wsaEvent, //當前服務端的SOCK句柄//事件對象句柄FD_ACCEPT | FD_CLOSE)) { //網絡事件printf("注冊網絡事件失敗!");closesocket(sSocket); //關閉套接字WSACleanup();//釋放套接字資源return FALSE;
}
2.2 等待網絡事件發生
????????在WinSockets應用程序里,先用WSAEventSelect函數給套接字把網絡事件注冊好,緊接著就得調用WSAWaitForMultipleEvents函數,目的是等著網絡事件發生。這個WSAWaitForMultipleEvents函數的作用就是,一直等到有一個事件對象或者所有事件對象進入“有信號量”狀態,又或者是函數調用時間到了(超時),它才會返回結果 。?
DWORD WSAWaitForMultipleEvents(DWORD cEvents, //事件對象句柄數量WSAEVENT FAR*lphEvents, //指向事件對象句柄的指針BOOL fWaitAll, //等待事件句柄的數量DWORD dwTimeout, //調用該函數的阻塞時間BOOL fAlertable //完成例程后是否繼續等待
);
????????WSAWaitForMultipleEvents這個函數,最多能夠處理64個對象。所以呢,基于這個情況,使用這個I/O模型的時候,在一個線程里,同一時刻最多也就只能支持64個套接字。要是你想用這個模型去管理超過64個套接字,那就得再創建一些額外的工作線程才行。?
????????WSAWaitForMultipleEvents函數的作用就是等著網絡事件發生。只要在規定的時間里,有網絡事件出現了,那這個函數返回的值,就能告訴你是哪個事件對象導致函數返回的 。?
????????注:要是fWaitAll這個參數設成true,那所有的事件對象都會被置為有信號量狀態。要是fWaitAll被設成FALSE,那么只要眾多事件句柄當中有一個變為有信號量狀態就可以了。這個函數運行結束后會給出一個返回值,這個返回值其實是個索引。用這個索引減去WSA_WAIT_EVENT_0這個宏的值,就能夠知道在事件數組里,哪個事件被觸發了,也就是能找到被觸發事件在數組中的位置 。?
示例:
//定義事件對象數組
EventArray[WSA_MAXIMUMWAIT_EVENTS] = {};
//等待網絡事件的發生
DWORD dwIndex =
WSAWaitForMultipleEvents(uEventCount, EventArray, FALSE,
WSA_INFINITE,
FALSE);
//完成例程后是否繼續等待
//返回該事件在EventArray數組中的位置(下標從0開始)
dwIndex = dwIndex - WSA_WAIT_EVENT_0;
2.3 獲取網絡事件
????????利用WSAWaitForMultipleEvents函數的返回值,我們能知道哪個套接字發生了網絡事件。不過,僅僅知道是哪個套接字還不夠,應用程序還得弄清楚在這個套接字上具體發生了哪種網絡事件。WSAEnumNetworkEvents函數就派上用場了,它能找出套接字上發生的網絡事件,同時把系統里關于這個網絡事件的記錄清除掉,還會把事件對象重新設置回初始狀態 。
int WSAEnumNetworkEvents(SOCKET s, //發生網絡事件的套接字句柄WSAEVENT hEventObject, //被重置的事件對象句柄LPWSANETWORKEVENTS lpNetworkEvents //網絡事件的記錄和相應錯誤碼
);
typedef struct _WSANETWORKEVENTS {long lNetworkEvents; //網絡事件int iErrorCode[FD_MAX_EVENTS]; //錯誤碼
}
WSAEnumNetworkEvents參數
?_WSANETWORKEVENTS 參數
?????????使用方式:當lNetworkEvents&FD_XX為TRUE時,即表示發生了此網絡事件。
示例:
Socket SocketArray[WSA_MAXIMUM_WAIT_EVENTS] = {};
int uEventCount = 0; //記錄當前事件和套接字的個數
WSAEnumNetworkEvents(SocketArray[dwIndex],//發生網絡事件的套接字句柄
EventArray[dwIndex],//被重置的事件對象句柄
&NetworkEvents)) //網絡事件的記錄和相應錯誤碼
//響應網絡事件
if ((NetworkEvents.lNetworkEvents & FD_ACCEPT) &&0 == NetworkEvents.iErrorCode[FD_ACCEPT_BIT]) {// 處理邏輯
}
????????這個函數創建的事件對象,有“手動重設”和“自動重設”這兩種工作模式。咱們這里創建的事件對象,是按手動方式工作的,一開始它處于無信號狀態。一旦網絡事件發生,跟套接字相關聯的這個事件對象,就會從無信號量的狀態變成有信號量狀態。因為是“手動重設”模式,所以應用程序把相關事件處理完之后,得把這個有信號量的事件對象,再變回無信號量狀態。 有個MAX_NUM_SOCKET宏,它的值是64 ,一般來說,這代表一個線程最多能同時等待處理64個事件,也就是說一個線程最多只能同時盯著64個socket。要是超過了這個數量,就必須再開啟新的線程來處理。 調用這個函數的時候,如果hEventObject參數不是NULL,那么這個事件對象就會被自動重置為“無信號”狀態;要是hEventObject參數是NULL,那就得調用WSAResetEvent函數,把事件設置成“無信號”狀態 。?
2.4 手動設置信號量和釋放資源
BOOL WSAResetEvent(WSAEVENT hEvent //要設置為無信號量的事件對象句柄
);
WSAResetEvent函數用于將事件對象從“有信號量”設置為“無信號量”。
BOOL WSACloseEvent(WSAEVENT hEvent //要釋放資源的事件對象句柄
);
????????應用程序完成網絡事件的處理后,需要使用WSACloseEvent函數釋放事件對象所占用的系統資源。
三、 WSAEventSelect模型偽代碼示例
#include <Winsock2.h>
#pragma comment(lib,"Ws2_32.lib")
typedef struct _EVENT_SOCKET_INFO {WSAEVENT EventArray[WSA_MAXIMUM_WAIT_EVENTS];SOCKET SocketArray[WSA_MAXIMUM_WAIT_EVENTS];
}EVENT_SOCKET_INFO, *PEVENT_SOCKET_INFO;
BOOL SetSocket() {//1.初始化套接字WSADATA stcData;int nResult;nResult = WSAStartup(MAKEWORD(2, 2), &stcData);if (nResult == SOCKET_ERROR)return FALSE;//2.創建套接字// 此處代碼省略//3.初始化地址定址sockaddr_in sAddr = {0};sAddr.sin_family = AF_INET;sAddr.sin_port = htons(1234);sAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");int nSaddrLen = sizeof(sockaddr_in);//4.綁定int nRet = 0;nRet = bind(sSocket,(sockaddr*)&sAddr,sizeof (sockaddr_in));if (SOCKET_ERROR == nRet) {printf("綁定IP定址失敗");closesocket(sSocket);WSACleanup();}//接收返回信息//當前客戶端SOCK句柄//IP定址//IP定址結構體大小//關閉套接字//釋放套接字資源//WSANETWORKEVENTS NetworkEvents = {0};//網絡事件的記錄和相應錯誤碼SOCKET sClientSocket = 0; //當前發送事件客戶端的SOCK句柄UINT uEventCount = 0; //事件對象句柄數量CLIENTINFO ClientInfo = {0};//當前發送事件的客戶端EVENT_SOCKET_INFO EventSocketInfo = {0};//保存事件和Sock信息//5.監聽if (listen(sSocket, SOMAXCONN)) {printf(" 監聽失敗!");closesocket(sSocket);WSACleanup();}//6.創建一個事件對象WSAEVENT wsaEvent = WSACreateEvent();if (WSA_INVALID_EVENT == wsaEvent) {printf("創建一個事件對象失敗!");closesocket(sSocket);WSACleanup();}//關閉套接字//釋放套接字資源//7.注冊網絡事件if (WSAEventSelect(sSocket,wsaEvent,//當前服務端的SOCK句柄//事件對象句柄FD_ACCEPT | FD_CLOSE))//網絡事件{printf("注冊網絡事件失敗!");closesocket(sSocket);WSACleanup();//關閉套接字//釋放套接字資源return FALSE;}//保存事件對象和套接字EventSocketInfo.EventArray[uEventCount] = wsaEvent;EventSocketInfo.SocketArray[uEventCount++] = sSocket;while (TRUE) {WSAEVENT EventArray[WSA_MAXIMUM_WAIT_EVENTS];//8.等待網絡事件的發生DWORD dwIndex = WSAWaitForMultipleEvents(uEventCount,EventSocketInfo.EventArray,FALSE,WSA_INFINITE,FALSE);if (WSA_WAIT_FAILED == dwIndex)continue;//手動設置無信號//WSAResetEvent(EventSocketInfo.EventArray);//9.找到有信號的對象的標號//WSAWaitForMultipleEvent 函數的返回值-WSA_WAIT_EVENT_0dwIndex = dwIndex - WSA_WAIT_EVENT_0;if (WSAEnumNetworkEvents(EventSocketInfo.SocketArray[dwIndex],// 發生網絡事件的套接字句柄EventSocketInfo.EventArray [dwIndex],//被重置的事件對象句柄&NetworkEvents)) //網絡事件的記錄和相應錯誤碼{printf(" 調用WSAEnumNetworkEvents失敗!");closesocket(sSocket); //關閉套接字WSACleanup(); //釋放套接字資源}//10.響應網絡事件//10.1連接if ((NetworkEvents.lNetworkEvents & FD_ACCEPT) &&0 == NetworkEvents.iErrorCode[FD_ACCEPT_BIT]) {sClientSocket = accept(sSocket, (sockaddr*)&sAddr, &nSaddrLen);if (INVALID_SOCKET == sClientSocket) {printf(" 連接客戶端失敗!");continue;}//為新客戶端創建網絡事件//1.為剛連接進來的客戶端創建事件對象EventSocketInfo.EventArray[uEventCount] = WSACreateEvent();//2.保存當前連接進來的客戶端Socket 套接字EventSocketInfo.SocketArray[uEventCount] = sClientSocket;//3.為該客戶端注冊網絡事件WSAEventSelect(sClientSocket,EventSocketInfo.EventArray[uEventCount],FD_READ | FD_WRITE | FD_CLOSE);uEventCount++;ClientInfo.ClientSock = sClientSocket;g_ClientInfo.push_back(ClientInfo);continue;}//10.2接收消息if ((NetworkEvents.lNetworkEvents & FD_READ) &&0 == NetworkEvents.iErrorCode[FD_READ_BIT]) {// 接收數據處理邏輯continue;}//10.3關閉事件if ((NetworkEvents.lNetworkEvents & FD_CLOSE) &&(0 == NetworkEvents.iErrorCode[FD CLOSE_BIT])) {//關閉Socket套接字和釋放事件對象占有的資源closesocket(EventSocketInfo.SocketArray[dwIndex]);WSACloseEvent(EventSocketInfo.EventArray[dwIndex]);//將退出的客戶端從事件數組中刪除,并將之后的數據向前移動for (int i = dwIndex; i < uEventCount; i++) { //線性表EventSocketInfo.EventArray[dwIndex]= EventSocketInfo.EventArray[dwIndex + 1];EventSocketInfo.SocketArray[dwIndex]= EventSocketInfo.SocketArray[dwIndex + 1];}uEventCount--;continue;}}return TRUE;
}
????????WSAEventSelect模型作為一種異步I/O模型,通過事件機制實現網絡事件的通知與處理,在網絡編程中為應用程序提供了一種有效的處理網絡操作的方式,了解其原理和實現流程有助于開發者編寫出更高效、穩定的網絡應用程序。
四、完整實踐示例代碼
頭文件initsock.h:
#include <winsock2.h>
#pragma comment(lib, "WS2_32") // 鏈接到 WS2_32.libclass CInitSock
{
public:/*CInitSock 的構造器*/CInitSock(BYTE minorVer = 2, BYTE majorVer = 2){// 初始化WS2_32.dllWSADATA wsaData;WORD sockVersion = MAKEWORD(minorVer, majorVer);if (::WSAStartup(sockVersion, &wsaData) != 0){exit(0);}}/*CInitSock 的析構器*/~CInitSock(){::WSACleanup();}
};
?服務端代碼:
#include "initsock.h"
#include <iostream>
using namespace std;// 初始化Winsock庫
CInitSock theSock;int main()
{// 事件句柄和套節字句柄表WSAEVENT eventArray[WSA_MAXIMUM_WAIT_EVENTS];SOCKET sockArray[WSA_MAXIMUM_WAIT_EVENTS];int nEventTotal = 0;// 創建監聽套節字SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);sockaddr_in sin;sin.sin_family = AF_INET;sin.sin_port = htons(4567); // 此服務器監聽的端口號sin.sin_addr.S_un.S_addr = INADDR_ANY;if (::bind(sListen, (sockaddr*)&sin, sizeof(sin)) == SOCKET_ERROR){cout << " Failed bind()" << endl;return -1;}// 進入監聽模式if (::listen(sListen, 5) == SOCKET_ERROR){cout << " Failed listen()" << endl;return 0;}cout << "服務器已啟動監聽,可以接收連接!" << endl;// 創建事件對象,并關聯到新的套節字WSAEVENT event = ::WSACreateEvent();::WSAEventSelect(sListen, event, FD_ACCEPT | FD_CLOSE);// 添加到表中eventArray[nEventTotal] = event;sockArray[nEventTotal] = sListen;nEventTotal++;// 處理網絡事件while (TRUE){// 在所有事件對象上等待int nIndex = ::WSAWaitForMultipleEvents(nEventTotal, eventArray, FALSE, WSA_INFINITE, FALSE);// 對每個事件調用WSAWaitForMultipleEvents函數,以便確定它的狀態nIndex = nIndex - WSA_WAIT_EVENT_0;for (int i = nIndex; i < nEventTotal; i++){nIndex = ::WSAWaitForMultipleEvents(1, &eventArray[i], TRUE, 1000, 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){cout << " Too many connections!" << endl;continue;}sockaddr_in addrRemote;int nAddrLen = sizeof(addrRemote);SOCKET sNew = ::accept(sockArray[i], (SOCKADDR*)&addrRemote, &nAddrLen);cout << "\n與主機" << ::inet_ntoa(addrRemote.sin_addr) << "建立連接" << endl;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){char szText[256];int nRecv = ::recv(sockArray[i], szText, strlen(szText), 0);if (nRecv > 0){szText[nRecv] = '\0';cout << " 接收到數據:" << szText << endl;}// 向客戶端發送數據char sendText[] = "你好,客戶端!";if (::send(sockArray[i], sendText, strlen(sendText), 0) > 0){cout << " 向客戶端發送數據:" << sendText << endl;}}}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++){eventArray[j] = eventArray[j + 1];sockArray[j] = sockArray[j + 1];}nEventTotal--;}}}}}return 0;
}
客戶端代碼:
#include "InitSock.h"
#include <iostream>
using namespace std;CInitSock initSock; // 初始化Winsock庫int main()
{// 創建套節字SOCKET s = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);if (s == INVALID_SOCKET){cout << " Failed socket()" << endl;return 0;}// 也可以在這里調用bind函數綁定一個本地地址// 否則系統將會自動安排char address[20] = "127.0.0.1";// 填寫遠程地址信息sockaddr_in servAddr;servAddr.sin_family = AF_INET;servAddr.sin_port = htons(4567);// 注意,這里要填寫服務器程序(TCPServer程序)所在機器的IP地址// 如果你的計算機沒有聯網,直接使用127.0.0.1即可servAddr.sin_addr.S_un.S_addr = inet_addr(address);if (::connect(s, (sockaddr*)&servAddr, sizeof(servAddr)) == -1){cout << " Failed connect() " << endl;return 0;}else {cout << "與服務器 " << address << "建立連接" << endl;}char szText[] = "你好,服務器!";if (::send(s, szText, strlen(szText), 0) > 0){cout << " 發送數據:" << szText << endl;}// 接收數據char buff[256];int nRecv = ::recv(s, buff, 256, 0);if (nRecv > 0){buff[nRecv] = '\0';cout << " 接收到數據:" << buff << endl;}// 關閉套節字::closesocket(s);return 0;
}