WinSock WSAEventSelect 模型

在前面我们说了WSAAsyncSelect 模型,它相比于select模型来说提供了这样一种机制:当发生对应的IO通知时会立即通知操作系统,并调用对应的处理函数,它解决了调用send和 recv的时机问题,但是它有一个明显的缺点,就是它必须依赖窗口。对此WinSock 提供了另一种模型 WSAEventSelect

模型简介

该模型主要特色在于它使用事件句柄来完成SOCKET事件的通知。与WSAAsyncSelect 模型类似,它也允许使用事件对象来完成多个socket的完成通知。 该模型首先在每个socket句柄上调用WSACreateEvent来创建一个WSAEvent对象句柄(早期的WSAEvent与传统的Event句柄有一定的区别,但是从WinSock2.0 以后二者是同一个东西)。接着调用WSAEventSelect将SOCKET句柄和WSAEvent对象绑定,最终通过WSAWaitForMultiEvents来等待WSAEvent变为有信号,然后再来处理对应的socket

WSAEvent有两种工作模式和工作状态 工作状态有有信号和无信号两种 工作模式有手工重置和人工重置,手工重置指的是每当WSAWaitForMultiEvents或者WSAWaitForSingleEvents 返回之后,WSAEvent不会自动变为无信号,需要手工调用WSAResetEvent来将WSAEvent对象设置为无信号,而自动重置表示每次等待函数返回后会自动重置为无信号;调用WSACreateEvent创建的WSAEvent对象是需要手工重置的,如果想创建自动重置的WSAEvent对象可以调用CreateEvent函数来创建(由于WinSock2.0 之后二者没有任何区别,所以只需要调用CreateEvent并将返回值强转为WSAEvent即可)

WSAEventSelect函数的原型如下:

int WSAEventSelect(  SOCKET s,  WSAEVENT hEventObject,  long lNetworkEvents);

其中s表示对应的SOCKET,hEventObject表示对应的WSAEvent对象,lNetworkEvents 表示我们需要处理哪些事件,它有一些对应的宏定义

网络事件

对应的含义

FD_READ

当前可以进行数据接收操作,此时可以调用像 recv, recvfrom, WSARecv, 或者 WSARecvFrom 这样的函数

FD_WRITE

此时可以发送数据,可以调用 send, sendto, WSASend, or WSASendTo

FD_ACCEPT

可以调用accept (Windows Sockets) 或者 WSAAccept 除非返回的错误代码是WSATRY_AGAIN.

FD_CONNECT

表示当前可以连接远程服务器

FD_CLOSE

当前收到关闭的消息

当WSAWaitForMultipleEvents返回时同时会返回一个序号,用于标识是数组中的哪个WSAEvent有信号,我们使用 index - WSA_WAIT_EVENT_0 来获取对应WSAEvent在数组中的下标,然后根据这个事件对象找到对应的SOCKET即可 获得了对应的SOCKET以后,还需要获取到当前是哪个事件发生导致它变为有信号,我们可以调用WSAEnumNetworkEvents函数来获取对应发生的网络事件

int WSAEnumNetworkEvents(
    SOCKET s,
    WSAEVENT hEventObject,
    LPWSANETWORKEVENTS lpNetworkEvents
);

s就是要获取其具体事件通知的SOCKET句柄 hEventObject就是对应的WSAEvent句柄,可以不传入,因为SOCKET句柄已经说明了要获取那个句柄上的通知,当然如果传入了,那么这个函数会对这个WSAEvent做一次重置,置为无信号的状态,相当于WSAResetEvent调用。此时我们就不需要调用WSAResetEvent函数了

最后一个参数是一个结构,结构的定义如下:

typedef struct _WSANETWORKEVENTS {  
    long lNetworkEvents;  
    int iErrorCode[FD_MAX_EVENTS];
} WSANETWORKEVENTS,  *LPWSANETWORKEVENTS;

第一个数据是当前产生的网络事件。 iErrorCode数组是对应每个网络事件可能发生的错误代码,对于每个事件错误代码其具体数组下标是预定义的一组FD_开头的串再加上一个_BIT结尾的宏,比如FD_READ事件对应的错误码下标是FD_READ_BIT

下面的代码演示了处理接收(读取)数据的事件错误的例子代码

if (NetworkEvents.lNetworkEvents & FD_READ)
{
    if (NetworkEvents.iErrorCode[FD_READ_BIT] != 0)
    {
       printf("FD_READ failed with error %d\n",
           NetworkEvents.iErrorCode[FD_READ_BIT]);
    }
}

到目前为止,我们可以总结一下使用WSAEventSelect模型的步骤

  1. 调用WSACreateEvent为每一个SOCKET创建一个等待对象,并与对应的SOCKET形成映射关系
  2. 调用WSAEventSelect函数将SOCKET于WSAEvent对象进行绑定
  3. 调用WSAWaitForMultipleEvents 函数对所有SOCKET句柄进行等待
  4. 当WSAWaitForMultipleEvents 函数返回时利用返回的索引找到对应的WSAEvent对象和SOCKET对象
  5. 调用WSAEnumNetworkEvents来获取对应的网络事件,根据网络事件来进行对应的收发操作
  6. 重复3~5的步骤

示例

下面是一个简单的例子

int _tmain(int argc, TCHAR *argv[])
{
    WSADATA wd = {0};
    WSAStartup(MAKEWORD(2, 2), &wd);

    SOCKET skServer = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
    SOCKADDR_IN AddrServer = {AF_INET};
    AddrServer.sin_port = htons(SERVER_PORT);
    AddrServer.sin_addr.s_addr = htonl(INADDR_ANY);
    bind(skServer, (SOCKADDR*)&AddrServer, sizeof(SOCKADDR));
    listen(skServer, 5);
    printf("服务端正在监听...........\n");

    CWSAEvent WSAEvent;
    WSAEvent.InsertClient(skServer, FD_ACCEPT | FD_CLOSE);
    WSAEvent.EventLoop();

    WSACleanup();
    return 0;
}

在代码中定义了一个类CWSAEvent,该类封装了关于该模型的相关操作和对应事件对象和SOCKET对象的操作,在主函数中首先创建监听的SOCKET,然后绑定、监听,并提交监听SOCKET到类中,以便对它进行管理,函数InsertClient的定义如下:

void CWSAEvent::InsertClient(SOCKET skClient, long lNetworkEvents)
{
    m_socketArray[m_nTotalItem] = skClient;
    m_EventArray[m_nTotalItem] = WSACreateEvent();
    WSAEventSelect(skClient, m_EventArray[m_nTotalItem++], lNetworkEvents);
}

这个函数中主要向事件数组和SOCKET数组的对应位置添加了相应的成员,然后调用WSAEventSelect。

而类的EventLoop函数定义了一个循环来重复前面的3~5步,函数的部分代码如下:

int CWSAEvent::WaitForAllClient()
{
    DWORD dwRet = WSAWaitForMultipleEvents(m_nTotalItem, m_EventArray, FALSE, WSA_INFINITE, FALSE);
    WSAResetEvent(m_EventArray[dwRet - WSA_WAIT_EVENT_0]);
    return dwRet - WSA_WAIT_EVENT_0;
}


int CWSAEvent::EventLoop()
{
    WSANETWORKEVENTS wne = {0};
    while (TRUE)
    {
        int nRet = WaitForAllClient();
        WSAEnumNetworkEvents(m_socketArray[nRet], m_EventArray[nRet], &wne);
        if (wne.lNetworkEvents & FD_ACCEPT)
        {
            if (0 != wne.iErrorCode[FD_ACCEPT_BIT])
            {
                OnAcceptError(nRet, m_socketArray[nRet], wne.iErrorCode[FD_ACCEPT_BIT]);
            }else
            {
                OnAcccept(nRet, m_socketArray[nRet]);
            }
        }else if (wne.lNetworkEvents & FD_CLOSE)
        {
            if (0 != wne.iErrorCode[FD_CLOSE_BIT])
            {
                OnCloseError(nRet, m_socketArray[nRet], wne.iErrorCode[FD_CLOSE_BIT]);
            }else
            {
                OnClose(nRet, m_socketArray[nRet]);
            }
        }else if (wne.lNetworkEvents & FD_READ)
        {
            if (0 != wne.iErrorCode[FD_READ_BIT])
            {
                OnReadError(nRet, m_socketArray[nRet], wne.iErrorCode[FD_READ_BIT]);
            }else
            {
                OnRead(nRet, m_socketArray[nRet]);
            }
        }else if (wne.lNetworkEvents & FD_WRITE)
        {
            if (0 != wne.iErrorCode[FD_WRITE_BIT])
            {
                OnWriteError(nRet, m_socketArray[nRet], wne.iErrorCode[FD_WRITE_BIT]);
            }else
            {
                OnWrite(nRet, m_socketArray[nRet]);
            }
        }
    }
}

函数首先进行了等待,当等待函数返回时,获取对应的下标,以此来获取到socket和事件对象,然后调用WSAEnumNetworkEvents来获取对应的网络事件,最后根据事件调用不同的处理函数来处理 在上面的代码中,这个循环有一个潜在的问题,我们来设想这么一个场景,当有多个客户端同时连接服务器,在第一次等待返回时,我们主要精力在进行该IO事件的处理,也就是响应这个客户端A的请求,而此时客户端A又发送了一个请求,而另外几个客户端B随后也发送了一个请求,在第一次处理完成后,等待得到的将又是客户端A,而后续客户端B的请求又被排到了后面,如果这个客户端A一直不停的发送请求,可能造成的问题是服务器一直响应A的请求,而对于B来说,它的请求迟迟得不到响应。为了避免这个问题,我们可以在函数WSAWaitForMultipleEvents 返回后,针对数组中的每个SOCKET循环调用WSAWaitForMultipleEvents将等待的数量设置为1,并将超时值设置为0,这个时候这个函数的作用就相当于查看数组中的每个SOCKET,看看是不是有待决的,当所有遍历完成后依次处理这些请求或者专门创建对应的线程来处理请求

最后,整个示例代码


本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏蜉蝣禅修之道

android之编辑框限定范围

23930
来自专栏有趣的django

37.Django1.11.6文档

第一步 入门 检查版本 python -m django --version 创建第一个项目 django-admin startproject mysite ...

50580
来自专栏青玉伏案

ReactiveSwift源码解析(七) Signal的CombineLatest的代码实现

本篇博客我们就来聊一下combineLatest()的使用以及具体的实现方式。在之前的《iOS开发之ReactiveCocoa下的MVVM》的博客中我们已经聊过...

24780
来自专栏信安之路

PE 病毒与 msf 奇遇记

通俗的讲,PE 病毒就是感染 PE 文件的病毒,通过修改可执行文件的代码中程序入口地址,变为恶意代码的的入口,导致程序运行时执行恶意代码。

11500
来自专栏码农分享

对LinqtoExcel的扩展 【数据有限性,逻辑有效性】

接着上文的内容继续讲,上文中我提到了对Excel操作帮助类库LinqToExcel类库的优缺点和使用方法。我也讲到了自己在使用中碰到的问题,我也开发了一个简单的...

19180
来自专栏Java技术栈

非常有用的并发控制-循环栅栏CyclicBarrier

昨天我讲了倒计时器CountDownLatch的应用,它是阻塞线程直到计时器归0的一种等待方式。今天讲的这个循环栅栏CyclicBarrier与倒计时器非常类似...

403120
来自专栏jeremy的技术点滴

现代Web开发系列教程_06

32570
来自专栏Spark学习技巧

Kafka源码系列之分组消费的再平衡策略

一,Kafka消费模式 从kafka消费消息,kafka客户端提供两种模式: 分区消费,分组消费。 分区消费对应的就是我们的DirectKafkaInputDS...

1.1K60
来自专栏技术博客

Asp.Net Web API 2第十五课——Model Validation(模型验证)

阅读本文之前,您也可以到Asp.Net Web API 2 系列导航进行查看 http://www.cnblogs.com/aehyok/p/3446289.h...

10320
来自专栏柠檬先生

vuex 使用文档

安装 直接下载CDN 引用   <script src="/path/to/vue.js"></script>   <script src="/path/to/...

613100

扫码关注云+社区

领取腾讯云代金券