转载自:http://blog.csdn.net/ithzhang/article/details/8476556
WSAEventSelect模型
WSAEventSelect 模型是 Windows socekts 提供的另一个有用异步 IO 模型。该模型允许在一个或多个套接字上接收以事件为基础的网络事件通知。Windows sockets 应用程序可以通过调用 WSAEventSelect 函数,将一个事件与网络事件集合关联起来。当网络事件发生时,应用程序以事件的形式接收网络事件通知。
WSAEventSelect 模型与 WSAAsyncSelect 模型很相似。它们最主要的差别就是当网络事件发生时通知应用程序的形式不同。虽然它们都是异步的,但 WSAAsyncSelect 以消息的形式通知,而 WSAEventSelect 以事件的形式通知。
与 select 模型相比较,WSAAsyncSelect 与 WSAEventSelect 模型都是被动接受的。网络事件发生时,系统通知应用程序。而 select 模型是主动的,应用程序主动调用 select 函数来检查是否发生了网络事件。
WSAEventSelect函数
该函数功能是为套接字注册网络事件。该函数将事件对象与网络事件关联起来。当在该套接字上发生一个或多个网络事件时,应用程序便以事件的形式接收这些网络事件通知。
int WSAEventSelect(SOCKET s, WSAEVENT hEvent, Long lNetworkEvents);
// s 为套接字句柄。
// hEvent 为事件对象句柄。
// lNetworkEvents 为应用程序感兴趣的网络事件集合。
如果应用程序为套接字注册网络事件成功,函数返回 0。否则返回 SOCKET_ERROR。可以调用WSAGetLastError 来获取具体的错误代码。
调用该函数后,套接字自动被设为非阻塞的工作模式。如果应用程序要将套接字设置为阻塞模式,必须将lNetwork 参数设为 0,再次调用 WSAEventSelect 函数。
Windows sockets 声明的网络事件与前面介绍的 WSAAsyncSelect 介绍的是一样的,此处不再介绍。
WSACreateEvent()函数
应用程序在调用 WSAEventSelect 函数之前,必须先创建一个事件对象。创建的方法是调用WSACreateEvent 函数:
WSAEVENT WSACreateEvent(void);
调用成功则返回事件对象句柄。否则返回 WSA_INVALID_EVENT。返回的事件对象的初始态为未触发态手工重置的对象。
WSAResetEvent()函数
当网络事件到来时,与套接字关联的事件对象由未触发变为触发态。由于它是手工重置事件,应用程序需要手动将事件的状态设置为未触发态。这可以调用 WSAResetEvent 函数:
bool WSAResetEvent(WSAEVENT hEvent);
该函数的参数为事件对象。调用成功则返回TRUE,否则false。
WSACloseEvent()函数
不再使用事件对象时要将其关闭。这可以调用WSACloseEvent函数:
bool WSACloseEvent(WSAEVENT hEvent);
调用成功返回TRUE,否则false。
WSAWaitForMultipleEvents()函数
该函数可以等待网络事件的发生。它的目的是等待一个或是所有的事件对象变为已触发状态:
DWORD WSAWaitForMultipleEvents(
DWORD cEvents,
const WSAEVENT *plhEvent,
BOOL fWaitAll,
DWORD dwTimeOUT,
BOOL fAlertable);
- cEvents:为事件对象句柄个数。至少为1,最多为WSA_MAXIMUM_WAIT_EVENTS,64个。
- lphEvents为指向对象句柄数组指针。
- fWaitAll:如果为TRUE,则该函数在所有事件对象都转变为已触发时才返回。如为false,只要有一个对象被触发,函数即返回。
- dwTimeOUT:函数阻塞事件。单位为毫秒。在时间用完后函数会返回。如果为 WSA_INFINITE 则函数会一直等待下去。如果超时返回,函数返回 WSA_WAIT_TIMEOUT。
- fAlterable:该参数说明当完成例程在系统队列中排队等待执行时,该函数是否返回。这主要应用于重叠IO模型,以后还会介绍。此处将其设置为 false 即可。
WSAWaitForMultipleEvents返回时,返回值会指出它返回的原因。
当 bWaitAll 为 TRUE 时:
- 如果返回值为WSA_TIMEOUT则表明等待超时。
- WSA_EVENT_0表明所有对象都已变成触发态。等待成功。
- WAIT_IO_COMPLETION说明一个或多个完成例程已经排队等待执行。
如果 bWaitAll 为 false 时:
- WSA_WAIT_EVENT_0 到 WSA_WAIT_EVENT_0+cEvent-1 范围内的值,说明有一个对象变为触发态。它在数组中的下标为:返回值-WSA_EVENT_0。
如果函数调用失败,则返回 WSA_WAIT_FAILED。
下面的程序代码,演示了当 WSAWaitForMultipleEvents 返回值,如何确定事件对象和发生网络事件的套接字:
SOCKET socketArray[WSA_MAXIMUM_WAIT_EVENTS];
WSAEVENT eventArray[WSA_MAXIMUM_WAIT_EVENTS];
int dwIndex = WSAWaitForMultipleEvents(num, eventArray, false, false);
//已触发的网络事件对象为:
WSAEVENT cur = eventArray[dwIndex-WSA_WAIT_EVENT_0];
//当前套接字为:
SOCKET curSocket = socektArray[dwIndex-WSA_WAIT_EVENT_0];
WSAEnumNetworkEvents()函数
通过 WSAWaitForMultipleEvents 的返回值可以获得发生网络事件的套接字。但是,应用程序需要判断在该套接字上究竟发生了什么网络事件。这可以通过调用 WSAEnumNetworkEvents 来实现:
int WSAEnumNetworkEvents(SOCKET s, WSAEVENT hEvent, LPWSANETWORKEVENTS lpNetworkEvents);
该函数可以查找发生在套接字上的网络事件,并清除系统内部的网络事件记录,重置事件对象。
- s 为发生网络事件的套接字句柄。
- hEvent 为被重置的事件对象句柄(可选)。
- lpNetworkEvents 为指向 WSANETWORKEVENTS 结构指针。
如果 hEvent 不为 NULL,则该事件被重置。如果为 NULL,需要调用 WSAResetEvent 函数设置事件为非触发状态。
该结构中包含发生网络事件的记录和相关错误代码。
调用成功返回 0,否则为 SOCKETS_ERROR。
WSANETWORKEVENTS结构如下:
typedef struct _WSANETWORKEVENTS
{
long lNetworkEvents,
int iErrorCode[FD_MAX_EVENTS];
} WSANETWORKEVENTS,*LPWSANETWORKEVENTS;
- lNetworkEvents 指示发生的网络事件。一个对象再变为触发态时,可能在套接字上发生了多个网络事件。
- iErrorCode 为包含网络事件错误代码的数组。错误代码与 lNetworkEvents 字段中的网络事件对应。
在应用程序中,使用网络事件错误标识符对 iErrorCode 数组进行索引,检查是否发生了网络错误。这些标识符的命名规则是对应的网络事件后面添加 _BIT。例如,对于 FD_READ 事件的网络事件错误标识符为 FD_READ_BIT。
下面的代码演示了,如何判断FD_READ网络事件的发生:
SOCKET s;
WSAEVENT hNetworkEvent;
WSANETWORKEVENT networkEvents;
if(0 == WSAEnumNetworkEvents(h, hNetworkEvent, &networkEvents)
{
//发生FD_WRITE网络事件。
if(networkEvents.lNetworkEvents & FD_READ)
{
if(0 == networkEvent.iErrorCode[FD_READ_BIT])
{
//接收数据。
}
else
{
//获取错误代码。
int nErrorCode = networkEvents.iErrorCode[FD_READ_BIT];
//处理错误。
}
}
}
示例
本例演示利用 WSAEventSelect 模型开发一个服务器应用程序的步骤。
主要步骤:
程序开始时会创建监听套接字,利用 WSAEventSelect 函数为套接字注册 FD_ACCEPT 和 FD_CLOSE 事件,然后套接字进入监听状态。在 while 循环内,循环调用 WSAWaitForMultipleEvents 函数等待网络事件的发生,当网络事件发生时函数返回,并通过该函数的返回值得到发生网络事件的套接字。调用 WSAEnumNetworkEvents 函数检查在该套接字上到底发生什么网络事件。
如果发生 FD_ACCEPT 网络事件,则调用 accept 函数接受客户端连接。将该套接字加入套接字数组。创建事件对象并加入事件数组。事件对象数量加一。然后调用 WSAEventSelect 函数为该套接字关联事件对象,注册 FD_READ,FD_WRITE 和 FD_CLOSE 网络事件。
如果发生 FD_READ 网络事件,则调用 recv 函数接收数据。
如果发生 FD_WRITE 网络事件,则调用 send 函数发送数据。
如果发生 FD_CLOSE 网络事件,将该套接字从套接字数组清除,同时将对应事件从事件数组删除。事件对象数量减一,并关闭该套接字。
在应用程序中,对发生的每种网络事件,都首先判断是否发生了网络错误。如果发生错误,则服务器退出。
步骤一:定义事件对象数组和套接字数组
这两个数组的最大长度为 WSA_MAXIMUM_WAIT_EVENTS。这两个数组的成员存在一一对应关系。
DWORD totalEvent; //事件对象数量。
WSAEVENT eventArray[WSA_MAXIMUM_WAIT_EVENTS];
SOCKETS socketArray[WSA_MAXIMUM_WAIT_EVENTS];
步骤二:创建套接字
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
if((sListen == socket(AF_INET, SOCK_STREAM, 0))
{
//创建失败。
}
步骤三:为监听套接字注册网络事件
if(eventArray[totalEvent] = WSACreateEvent() == WSA_INVALID_EVENT)
{
//调用失败。
}
//为监听套接字注册 FD_READ,和 FD_CLOSE 网络事件。
if(WSAEventSelect(sListen,eventArray[totalEvent], FD_ACCEPT | FD_CLOSE) == SOCKETS_ERROR)
{
//调用失败。
}
步骤四:开始监听
sockaddr_in addr;
addr.sin_family=AF_INET;
addr.sin_addr.S_addr=htons(INADDR_ANY);
addr.sin_port=htons(4000);
if(bind(sListen,(SOCKADDR*)&addr,sizeof(addr))==SOCKETS_ERROR)
{
//绑定失败。
}
if(!listen(sListen,10))
{
//监听失败。
}
步骤五:等待网络事件
while(true)
{
if(dwIndex = WSAWaitForMultipleEvents(totalEvent,eventArray,false,WSA_INFINITE,false) == WSA_WAIT_FAILED)
{
//等待失败。
}
}
步骤六:获取发生的网络事件
当网络事件发生时 WSAWaitForMultipleEvents 函数返回。调用 WSAEnumNetworkEvents 函数获取发生在套接字上的网络事件。
socketArray[dwIndex-WSA_WAIT_EVENT_0] //为当前发生网络事件的套接字。
eventArray[dwIndex-WSA_WAIT_EVENT_0] //为当前被投递网络事件的事件对象。
当函数返回时networkEvents变量中保存了网络事件的记录。同时事件对象的工作状态,由未触发态变为触发态。
WSANETWORKEVENTS networkEvents;
if(WSAEnumNetworkEvents(socketArray[dwIndex-WSA_WAIT_EVENT_0],eventArray[dwIndex-WSA_WAIT_EVENT_0],&networkEvents)==SOCKETS_ERROR)
{
//调用失败。
}
步骤七:判断是否是各网络事件发生
WSAEnumNetworkEvents 函数返回时,首先检查是否发生了 FD_ACCEPT 网络事件。如果该网络事件发生,则说明此时客户端的连接请求被等待。检查是否发生了网络错误,如果没有错误发生,则执行下面的步骤:
1:调用 accept 接受客户端请求。
2:判断当前套接字数量是否超过了最大值。如果超过则关闭该套接字。
3:将客户端套接字介入套接字数组。
4:创建套接字事件对象,并将该事件对象加入事件对象数组。
5:为该套接字注册 FD_READ, FD_WRITE 和 FD_CLOSE 网络事件。
6:事件对象数量加一,将该套接字加入管理客户端套接字链表中。
使用WSAEventSelect应该注意的问题
如果在一个套接字上多次调用WSAEventSelect函数,那么最后一次函数调用将会取消前一次的调用效果。
一个套接字不要关联多个事件对象 。在一个套接字上为不同网络事件注册不同的事件对象是不可能的。一个套接字关联一个对象,当该对象被触发时,获得对应的套接字,然后调用WSAEnumNetworkEvents来获得发生在此套节字上的事件。
如果要取消事件对象与网络事件的关联,以及为套接字注册的网络事件。应用程序可以在调用WSAEventSelect时将lNetworkEvent设置为0。另外,调用closesocket关闭套接字时,也会取消这种关联和为套接字注册的网络事件。
调用accept接受的套接字与监听套接字具有同样的属性。
如:在创建监听套接字时为其设置感兴趣的网络事件为FD_ACCEPT和FD_CLOSE。那么accept返回的套接字同样具有这些属性。它与监听套接字感兴趣的网络事件相同且使用同一个事件对象。一般情况下,我们都会为新套接字重新调用WSAEventSelect。后面的代码中在accept后会新套接字调用WSAEventSelect函数就不足为奇了!!接收FD_CLOSE网络事件时,错误代码指出套接字是从容关闭还是硬关闭。如果错误代码为0,则为从容关闭;若错误代码为WSAECONNRESET错误,则是硬关闭。当应用程序接收到该网络事件时,说明对方在该套接字上执行了shutdown或者是closesocket函数调用。
WSAEventSelect模型的优势和不足。
优势: WSAEventSelect模型的优势是可以应用在一个非窗口的Windows sockets程序中,实现对多个套接字的管理。
不足: 每个WSAEventSelect模型最多只能管理64个套接字。当应用程序需要管理多于64个套接字时,就需要额外创建线程。由于该模型需要调用多个函数,这增加了开发的难度。
以下为详细代码:
#include<iostream>
#include<windows.h>
#include"winsock2.h"
#pragma comment(lib,"WS2_32.lib")
#define MAX_NUM_SOCKET 20
SOCKET sListen;
u_int totalEvent=0;
//构造事件对象数组和套接字数组。
WSAEVENT eventArray[MAX_NUM_SOCKET];
SOCKET socketArray[MAX_NUM_SOCKET];
bool InitSocket()
{
WSAData wsa;
WSAStartup(MAKEWORD(2,2),&wsa);
sListen=socket(AF_INET,SOCK_STREAM,0);
if(sListen==INVALID_SOCKET)
{
return false;
}
WSAEVENT hEvent=WSACreateEvent();
eventArray[totalEvent]=hEvent;
totalEvent++; //可用事件加一
int ret=WSAEventSelect(sListen,hEvent,FD_CLOSE|FD_ACCEPT);//监听套接字只能收到这两种消息。
if(!ret)
{
return false;
}
sockaddr_in addr;
addr.sin_addr.S_un=inet_addr("192.168.1.100");
addr.sin_family=AF_INET;
addr.sin_port=htons(4000);
ret=bind(sListen,(SOCKADDR*)&addr,sizeof(addr));
if(ret==SOCKET_ERROR)
{
return false;
}
ret=listen(sListen,10);
if(SOCKET_ERROR==ret)
{
return false;
}
return true;
}
int main(int argc,char**argv)
{
InitSocket();
while(true)
{
//有一个事件被触发等待函数即返回。
int dwIndex=WSAWaitForMultipleEvents(totalEvent,eventArray,false,WSA_INFINITE,false);
if(dwIndex==WSA_WAIT_FAILED)
{
break;
}
else
{
//有网络事件发生。
WSANETWORKEVENTS wsanetwork;
SOCKET s=socketArray[dwIndex-WSA_WAIT_EVENT_0];
//传hEventObject为被触发的套接字,WSAEnumNetworkEvents函数,会将其设置为非触发态。无需手工设置。
int ret=WSAEnumNetworkEvents(s,eventArray[dwIndex-WSA_WAIT_EVENT_0],&wsanetwork);
if(ret==SOCKET_ERROR)//函数调用失败。
{
break;
}
//发生FD_ACCEPT网络事件。
else if(wsanetwork.lNetworkEvents&FD_ACCEPT)
{
if(wsanetwork.iErrorCode[FD_ACCEPT_BIT]!=0)//发生网络错误。
{
break;
}
else //接受连接请求。
{
SOCKET sAccept;
if((sAccept=accept(socketArray[dwIndex-WSA_WAIT_EVENT_0],NULL,NULL))==INVALID_SOCKET)
{
break;
}
//超过最大值。
if(totalEvent>WSA_MAXIMUM_WAIT_EVENTS)
{
closesocket(sAccept);
break;
}
//将新接受的套接字加入套接字数组。
socketArray[totalEvent]=sAccept;
//创建套接字事件对象。
if((eventArray[totalEvent]=WSACreateEvent())==WSA_INVALID_EVENT)
{
break;
}
//为新接受的套接字重新注册网络事件,重新关联事件对象。
//不使用与监听套接字同样的属性,这点要注意!。
//接受的套接字,用于收发数据。
if(WSAEventSelect(sAccept,eventArray[totalEvent], FD_READ|FD_WRITE|FD_CLOSE) == SOCKET_ERROR)
{
break;
}
totalEvent++;//总数加一。
//将套接字加入链表。
}
}
//发生FD_CLOSE网络事件。
else if(wsanetwork.lNetworkEvents&FD_CLOSE)
{
if(wsanetwork.iErrorCode[FD_CLOSE_BIT]!=0)//发生网络错误。
{
break;
}
else //连接关闭。
{
//删除链表中的该套接字。
//关闭网络事件对象。
WSACloseEvent(eventArray[dwIndex-WSA_WAIT_EVENT_0]);
//将此套节字和事件对象从数组中清除。
for(int i=dwIndex-WSA_WAIT_EVENT_0;i<totalEvent-1;i++)
{
eventArray[i]=eventArray[i+1];
socketArray[i]=socketArray[i+1];
}
totalEvent--;//总数减一。
}
}
//发生FD_READ网络事件。
else if(wsanetwork.lNetworkEvents&FD_READ)
{
if(wsanetwork.iErrorCode[FD_READ_BIT]!=0)//发生网络错误。
{
break;
}
else //套接字可读。
{
//接收数据。
}
}
//发生FD_WRITE网络事件。
else if(wsanetwork.lNetworkEvents&FD_WRITE)
{
if(wsanetwork.iErrorCode[FD_WRITE_BIT]!=0)//发生网络错误。
{
break;
}
else //套接字可写。
{
//发送数据。
}
}
}
}
return 0;
}
以上参考自《精通Windows sockets网络开发-基于Visual C++实现》如有纰漏,请不吝指教!
2013.1.7于山西大同