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,看看是不是有待决的,当所有遍历完成后依次处理这些请求或者专门创建对应的线程来处理请求

最后,整个示例代码
<hr />

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,099评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,828评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,540评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,848评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,971评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,132评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,193评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,934评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,376评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,687评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,846评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,537评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,175评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,887评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,134评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,674评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,741评论 2 351

推荐阅读更多精彩内容