当前位置: 首页 > news >正文

【第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;
}

相关文章:

  • A2 最佳学习方法
  • SpringBoot事务原理剖析
  • 力扣刷题-热题100题-第23题(c++、python)
  • 股指期权最后交易日是哪一天?
  • tortoiseSVN、source insignt、J-flash使用
  • 算法 | 蜣螂优化算法原理,引言,公式,算法改进综述,应用场景及matlab完整代码
  • C语言笔记(鹏哥)上课板书+课件汇总(动态内存管理)--数据结构常用
  • 在fedora41中使用最新版本firefox和腾讯翻译插件让英文网页显示中文翻译
  • package-lock.json能否直接删除?
  • Java 集合 List、Set、Map 区别与应用
  • vue 一个组件的开发,引出组件开发流程和知识点
  • 职坐标:互联网行业职业发展路径解析
  • CSS 相对复杂但实用的margin
  • 手动创建Electron+React项目框架(建议直接看最后)
  • vue3里面使用Socketjs之后打包完访问的时候报socketStore-BmspPEpN.js:1 WebSocket connection to
  • HarmonyOS Next应用架构设计与模块化开发详解
  • 数据:$UPC 上涨突破 5.8 USDT,近7日总涨幅达 73.13%
  • 常见中间件漏洞攻略-Tomcat篇
  • Spring Boot定时任务设置与实现
  • 5.3 位运算专题:LeetCode 371. 两整数之和
  • 全球前瞻|王毅赴巴西出席金砖外长会,加拿大迎来“几十年来最重要大选”
  • 经济日报:多平台告别“仅退款”,规则调整有何影响
  • 这场迪图瓦纪念拉威尔的音乐会,必将成为乐迷反复品味的回忆
  • 【社论】用生态环境法典守护生态文明
  • 最高法报告重申保护创新主体权益:加大侵权损害赔偿力度
  • 政治局会议:要提高中低收入群体收入,设立服务消费与养老再贷款