目录
引言
一、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;
}