WSAEventSelect模型
WSAEventSelect模型也是I/O模型中较为常用的一个异步模型,它也允许应用程序在一个或多个套接字上,接收以事件为基础的网络事件通知。该模型最主要是将网络事件投递至一个事件对象句柄。 事件通知
事件通知模型要求我们的应用程序针对打算使用的每一个套接字,首先创建一个事件对象。创建方法是调用WSACreateEvent函数,它的定义如下: WSAEVENT WSACreateEvent(void);
函数的返回值很简单,就是一个创建好的事件对象句柄。事件对象句柄到手后,接下来必须将其与某个套接字关联在一起,同时注册自己感兴趣的网络事件类型。调用WSAEventSelect来做到这一点,定义如下:
int WSAEventSelect(
SOCKET s, // 定义的套接字
WSAEVENT hEventObject, // 与套接字关联在一起的事件对象句柄
long lNetworkEvent // 应用程序感兴趣的各种网络事件类型的一个组合(FD_READ|FD_WRITE|FD_ClOSE)
);
WSAEventSelect创建的事件拥有两种工作状态,以及两种工作模式。其中,两种工作状态分别是“已传信”(signaled)和“未传信”(non signaled)。工作模式则包括“人工重设”(manual reset)和“自动重设”(auto reset)。WSAEventSelect最开始在一种未传信的工作状态中,并用一种人工重设模式,来创建事件句柄。随着网络事件触发了与一个套接字关联在一起的事件对象,工作状态便会从“未传信”转变成“已传信”。由于事件对象是在一种人工重设模式中创建的,所以在完成了一个I/O请求的处理之后,我们的应用程序需要负责将工作状态从已传信更改为未传信。要做到这一点,可调用WSAResetEvent函数,对它的定义如下:BOOL WSAResetEvent(WSAEVENT hEvent);唯一的参数是前面用WSACreateEvent函数创建的事件对象句柄,成功返回TRUE,失败返回FALSE。当应用程序完成了对一个事件对象的处理后,应调用BOOL WSACloseEvent(WSAEVENT hEvent);函数释放由hEvent句柄占用的系统资源。成功返回TRUE,失败返回FALSE。
一个套接字同一个事件对象句柄关联在一起后,应用程序便可开始I/O处理;方法是等待网络事件触发事件对象句柄的工作状态。WSAWaitForMultipleEvents函数的设计宗旨便是用来等待一个或多个事件对象句柄,并在事先指定的一个或所有句柄进入“已传信”状态后,或在超过了一个规定的时间周期后,立即返回。定义如下:
DWORD WSAWaitForMultipleEvents(
DWORD cEvents,
const WSAEVENT FAR * lphEvents,
BOOL fWaitAll,
DWORD dwTimeout,
BOOL fAlertable
);
其中,cEvents和lphEvents参数定义了由WSAEVENT对象构成的一个数组。在这个数组中,cEvents指定的是事件对象的数量,而lphEvents对应的是一个指针,用于直接引用该数组。要注意的是,WSAWaitForMultipleEvents只能支持由WS AMAXIMUMWAITEVENTS对象规定的一个最大值,在此定义成64个。因此,针对发出WSAWaitForMultipleEvents调用的每个线程,该I/O模型一次最多都只能支持64个套接字。假如想让这个模型同时管理不止64个套接字,必须创建额外的工作者线程,以便等待更多的事件对象。f WaitAll参数指定了WSAWaitForMultipleEvents如何等待在事件数组中的对象。若设为T RUE,那么只有等lphEvents数组内包含的所有事件对象都已进入“已传信”状态,函数才会返回;但若设为FALSE,任何一个事件对象进入“已传信”状态,函数就会返回。就后一种情况来说,返回值指出了到底是哪个事件对象造成了函数的返回。通常,应用程序应将该参数设为FALSE,一次只为一个套接字事件提供服务。dwTimeout参数规定了WSAWaitForMul tipleEvents最多可等待一个网络事件发生有多长时间,以毫秒为单位,这是一项“超时”设定。超过规定的时间,函数就会立即返回,即使由fWaitAll参数规定的条件尚未满足也如此。如超时值为0,函数会检测指定的事件对象的状态,并立即返回。这样一来,应用程序实际便可实现对事件对象的“轮询”。但考虑到它对性能造成的影响,还是应尽量避免将超时值设为0。假如没有等待处理的事件,WSAWaitForMultipleEvents便会返回WSAWAITTIMEOUT。如dwsTimeout设为WSA INFINITE(永远等待),那么只有在一个网络事件传信了一个事件对象后,函数才会返回。最后一个参数是fAlertable,在我们使用WSAEventSelect模型的时候,它是可以忽略的,且应设为FALSE。该参数主要用于在重叠式I/O模型中,在完成例程的处理过程中使用。
若WSAWaitForMultipleEvents收到一个事件对象的网络事件通知,便会返回一个值,指出造成函数返回的事件对象。这样一来,我们的应用程序便可引用事件数组中已传信的事件,并检索与那个事件对应的套接字,判断到底是在哪个套接字上,发生了什么网络事件类型。对事件数组中的事件进行引用时,应该用WSAWaitForMultipleEvents的返回值,减去预定义值WSAWAITEVENT0,得到具体的引用值(即索引位置)。如下例所示:
nIndex = WSAWaitForMultipleEvents(......);
hEvent = hEventArray[nIndex - WSA_WAIT_EVENT_0];
知道了造成网络事件发生的套接字后,调用WSAEnumNetworkEvents函数,调查发生了什么类型的网络事件,定义如下:
int WSAEnumNetworkEvents(
SOCKET s, // 网络事件发生相关联的套接字
WSAEVENT hEventObject, //重设事件对象,将处在“已传信”改为“未传信”,亦可使用WSAResetEvent替代
LPWSAMETWORKEVENTS lpNetworkEvents // 接收套接字上发生的网络事件类型以及可能出现的错误代码
);
typedef struct _WSANETWORKEVENTS {
long lNetworkEvent;
int iErrorCode[FD_MAX_EVENTS];
}WSANETWORKEVENTS, FAR * LPWSANETWORKEVENTS;
lNetworkEvents参数指定了一个值,对应于套接字上发生的所有网络事件类型。注意一个事件进入传信状态时,可能会同时发生多个网络事件类型。例如,一个繁忙的服务器应用可能同时收到FD_READ和FD_WRITE通知。iErrorCode参数指定的是一个错误代码数组,同lNetworkEvents中的事件关联在一起。针对每个网络事件类型,都存在着一个特殊的事件索引,名字与事件类型的名字类似,只是要在事件名字后面添加一个“_BIT”后缀字串即可。例如,对FD_READ事件类型来说,iErrorCode数组的索引标识符便是FD_READ_BIT。下述代码片断对此进行了阐释(针对FDREAD事件):
if (NetworkEvents.lNetworkEvents & FD_READ) {
if (NetworkEvents.iErrorCode[FD_READ_BIT] != 0) {
}
}
完成了对WSANETWORKEVENTS结构中的事件的处理之后,我们的应用程序应在所有可用的套接字上,继续等待更多的网络事件,以下是一段服务器端代码(来自与邮电出版社《Windows网络程序设计》):
int main()
{
// 事件句柄和套接字句柄
WSAEVENT eventArray[WSA_MAXIMUM_WAIT_EVENTS];
SOCKET sockArray[WSA_MAXIMUM_WAIT_EVENTS];
int nEventTotal = 0;
USHORT nPort = 4567; // 监听端口号
// 创建监听套接字
SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(nPort);
sin.sin_addr.S_un.S_addr = INADDR_ANY;
if(::bind(sListen, (sockaddr*)&sin, sizeof(sin)) == SOCKET_ERROR)
{
printf(" Failed bind() /n");
return -1;
}
::listen(sListen, 200);
// 创建事件对象,并关联到新的套接字
WSAEVENT event = ::WSACreateEvent();
::WSAEventSelect(sListen, event, FD_ACCEPT|FD_CLOSE);
// 添加到列表中
eventArray[nEventTotal] = event;
sockArray[nEventTotal] = sListen;
nEventTotal++;
char szText[512];
// 处理网络事件
while(TRUE)
{
// 在所有对象上等待
int nIndex = ::WSAWaitForMultipleEvents(nEventTotal, eventArray, FALSE, WSA_INFINITE, FALSE);
// 确定事件的状态
nIndex = nIndex - WSA_WAIT_EVENT_0;
// WSAWaitForMultipleEvents总是返回所有事件对象的最小值,为了确保所有的事件对象得到执行的机会,对 大于nIndex的事件对象进行轮询,使其得到执行的机会。
for(int i=nIndex; i<nEventTotal; i++)
{
nIndex = ::WSAWaitForMultipleEvents(1, &eventArray[i], TRUE, 0, 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)
{
printf(" Too many connections! /n");
continue;
}
SOCKET sNew = ::accept(sockArray[i], NULL, NULL);
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)
{
memset(szText, 0x01, sizeof(szText));
int nRecv = ::recv(sockArray[i], szText, strlen(szText), 0);
if(nRecv > 0)
{
szText[nRecv] = '/0';
printf("%s /n", szText);
}
}
}
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++)
{
sockArray[j] = sockArray[j+1];
sockArray[j] = sockArray[j+1];
}
EventTotal--;
}
}
else if(event.lNetworkEvents & FD_WRITE) // FD_WRITE事件破难理解,下面将重点说明。
{
}
}
}
}
return 0;
}
以上介绍了WSAEventSelect模型的流程和用法,大部分内容参考<<Windows网络编程>>。该模型也较易理解,使用比较普遍,但FD_WRITE事件的触发时机曾经非常使我困惑,书写了很多测试用例,总是不能得到满意的输出结果。一直认为,如果某一端(客户端或服务器端)调用recv阻塞接收,哪在另一段(服务器端或客户端)一定会触发FD_WRITE,进而用send来发送数据,后来查阅了MSDN和一些相关材料,才发现这一想法大错特错。FD_WRITE并非针对send的,一般是在连线成功后会触发一次或者缓冲区有多出的空位, 可以容纳需要发送的数据时才会触发。
FD_READ 事件非常容易掌握. 当有数据发送过来时, WinSock会以FD_READ事件通知你, 对于每一个FD_READ事件,你需要像下面这样调用recv(): int nRecvData = recv(wParam, &data, sizeof(data), 0); 基本上就是这样, 别忘了修改上面的wParam。还有,不一定每一次调用recv()都会接收到一个完整的数据包, 因为数据可能不会一次性全部发送过来. 所以在开始处理接收到的数据之前, 最好对接收到的字节数(即recv()的返回值)进行判断,看看是否收到的是一个完整的数据包。
FD_WRITE相对来说就麻烦一些。首先,当你建立了一个连接时,会产生一个FD_WRITE事件。但是如果你认为在收到 FD_WRITE时调用send()就万事大吉,那就错了。FD_WRITE事件只在发送缓冲区有多出的空位,可以容纳需要发送的数据时才会触发。
上面所谓的发送缓冲区,是指系统底层提供的缓冲区。send()先将数据写入到发送缓冲区中,然后通过网络发送到接收端。你或许会想,只要不把发送缓冲区填满,让发送缓冲区保持足够多的空位容纳需要发送的数据,那么你就会源源不断地收到FD_WRITE事件了。嘿嘿,错了。上面只是说FD_WRITE事件在发送缓冲区有多出的空位时会触发,但不是在有足够的空位时触发,就是说你得先把发送缓冲区填满。
通常的办法是在一个无限循环中不断的发送数据,直到把发送缓冲区填满。当发送缓冲区被填满后,send()将会返回 SOCKET_ERROR,WSAGetLastError()会返回WSAWOULDBLOCK。如果当前这个SOCKET处于阻塞(同步)模式,程序会一直等待直到发送缓冲区空出位置然后发送数据;如果SOCKET是非阻塞(异步)的,那么你就会得到WSAWOULDBLOCK错误。于是只要我们首先循环调用send()直到发送缓冲区被填满,然后当缓冲区空出位置来的时候,系统就会发出FD_WRITE事件。下面是一个处理FD_WRITE事件的例子。
case FD_WRITE: // 可以发送数据了
{
// 进入无限循环
while(TRUE)
{
// 从文件中读取数据,保存到packet.data里面.
in.read((char*)&packet.data,MAX_PACKET_SIZE);
// 发送数据
if (send(wparam, (char*)(&packet), sizeof(PACKET), 0) == SOCKET_ERROR)
{
if (WSAGetLastError() == WSAEWOULDBLOCK)
{
// 发送缓冲区已经满了, 退出循环.
break;
}
else // 其他错误
{
// 显示出错信息然后退出.
CleanUp();
return(0);
}
}
}
} break;
看到了吧,实现其实一点也不困难。只是弄混了一些概念而已。使用这样的发送方式,在发送缓冲区变满的时候就可以退出循环。然后,当缓冲区空出位置来的时候,系统会触发另外一个FD_WRITE事件,于是你就可以继续发送数据了。
在你开始使用新学到的知识之前,我还想说明一下FD_WRITE事件的使用时机。如果你不是一次性发送大批量的数据的话,就别想着使用FD_WRITE事件了,原因很简单-如果你寄期望于在收到FD_WRITE事件时发送数据,但是却又不能发送足够的数据填满发送缓冲区,那么你就只能收到连接刚刚建立时触发的那一次FD_WRITE-系统不会触发更多的FD_WRITE了。所以当你只是发送尽可能少的数据的时候,就忘掉 FD_WRITE 机制吧,在任何你想发送数据的时候直接调用send()。
以上部分是我在CSDN上看到的一篇文章,文章写得很易懂。其实,如果你想收到FD_WRITE事件而你又无法先填满发送缓冲区,可以调用WSAAsyncSelect( ..., FD_WRITE)。如果当前发送缓冲区有空位,系统会马上给你发FD_WRITE 事件。