WSAEventSelect模型
事件通知模型要求我们的应用程序针对打算使用的每一个套接字,首先创建一个事件对象。创建方法是调用WSACreateEvent函数,它的定义如下 : WSAEVENT WSACreateEvent(void);
函数的返回值很简单,就是一个创建好的事件对象句柄。 事件对象句柄到手后,接下来必须将其与某个套接字关联在一起 ,同时注册自己感兴 趣的网络事件类型。调用WSAEventSelect来做到这一点,定义如下:
DWORD WSAWaitForMultipleEvents(
DWORD cEvents, //cEvents指定的是事件对象的数量
const WSAEVENT FAR * lphEvents, //lphEvents对应的是一个指针,用于直接引用该事件对象数组
BOOL fWaitAll,
DWORD dwTimeout,
BOOL fAlertable
);
若WSAWaitForMultipleEvents收到一个事件对象的网络事件通知,便会返回一个值,指出造成函数返回的事件对象。这样一来,我们的应用程序便可引用事件数组中已传信的事件,并检索与那个事件对应的套接字,判断到底是在哪个套接字上,发生了什么网络事件类型。对事件数组中的事件进行引用时,应该用WSAWaitForMultipleEvents的返回值,减去预定义值WSAWAITEVENT0,得到具体的引用值(即索引位置)。如下例所示:
nIndex = WSAWaitForMultipleEvents(......); //返回的事件对象
hEvent = hEventArray[nIndex - WSA_WAIT_EVENT_0];
int WSAEnumNetworkEvents(
SOCKET s, // 网络事件发生相关联的套接字
WSAEVENT hEventObject, //重设事件对象,将处在“已传信”改为“未传信”,亦可使用WSAResetEvent替代
LPWSAMETWORKEVENTS lpNetworkEvents // 接收套接字上发生的网络事件类型以及可能出现的错误代码
);
typedef struct _WSANETWORKEVENTS {
long lNetworkEvent;
int iErrorCode[FD_MAX_EVENTS];
}WSANETWORKEVENTS, FAR * LPWSANETWORKEVENTS;
if(net_event.lNetworkEvents & FD_WRITE) { // 套节字可写
if(net_event.iErrorCode[FD_WRITE_BIT] == 0) {
...........
}
}
if (net_event.lNetworkEvents & FD_READ) {
if (net_event.iErrorCode[FD_READ_BIT] != 0) {
...........
}
}
if(net_event.lNetworkEvents & FD_CLOSE) { /* 套节字关闭*/
...........
}
完成了对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;
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()的返回值)进行判断,看看是否收到的是一个完整的数据包。
上面所谓的发送缓冲区,是指系统底层提供的缓冲区。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 事件。