【第23节】windows网络编程模型(WSAEventSelect模型)
目录
引言
一、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_0
dwIndex = 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.lib
class CInitSock
{
public:
/*CInitSock 的构造器*/
CInitSock(BYTE minorVer = 2, BYTE majorVer = 2)
{
// 初始化WS2_32.dll
WSADATA 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;
}