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函数的原型如下:

1
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函数来获取对应发生的网络事件

1
2
3
4
5
int WSAEnumNetworkEvents(
SOCKET s,
WSAEVENT hEventObject,
LPWSANETWORKEVENTS lpNetworkEvents
);

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

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

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

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

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

1
2
3
4
5
6
7
8
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的步骤

示例

下面是一个简单的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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的定义如下:

1
2
3
4
5
6
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步,函数的部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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,看看是不是有待决的,当所有遍历完成后依次处理这些请求或者专门创建对应的线程来处理请求

最后,整个示例代码