WSAEventSelect模型
WSAEventSelect模型是Windowssocekts提供的另一个有用异步IO模型。该模型允许在一个或多个套接字上接收以事件为基础的网络事件通知。Windowssockets应用程序可以通过调用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函数。
Windowssockets声明的网络事件与前面介绍的WSAAsyncSelect介绍的是一样的,此处不再介绍。
WSACreateEvent()函数。
应用程序在调用WSAEventSelect函数之前,必须先创建一个事件对象。创建的方法是调用WSACreateEvent函数。
WSAEVENT WSACreateEvent(void);
调用成功则返回事件对象句柄。否则返回WSA_INVALID_EVENT。返回的事件对象的初始态为未触发态手工重置的对象。
当网络事件到来时,与套接字关联的事件对象由未触发变为触发态。由于它是手工重置事件,应用程序需要手动将事件的状态设置为未触发态。这可以调用WSAResetEvent函数:
bool WSAResetEvent(WSAEVENT hEvent);
该函数的参数为事件对象。调用成功则返回TRUE,否则false。
不再使用事件对象时要将其关闭。这可以调用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。这两个数组的成员存在一一对应关系。
DWORDtotalEvent;//事件对象数量。
WSAEVENTeventArray[WSA_MAXIMUM_WAIT_EVENTS];
SOCKETSsocketArray[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变量中保存了网络事件的记录。同时事件对象的工作状态,由未触发态变为触发态。
WSANETWORKEVENTSnetworkEvents;
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应该注意的问题。
1:如果在一个套接字上多次调用WSAEventSelect函数,那么最后一次函数调用将会取消前一次的调用效果。
2:一个套接字不要关联多个事件对象。在一个套接字上为不同网络事件注册不同的事件对象是不可能的。一个套接字关联一个对象,当该对象被触发时,获得对应的套接字,然后调用WSAEnumNetworkEvents来获得发生在此套节字上的事件。
3:如果要取消事件对象与网络事件的关联,以及为套接字注册的网络事件。应用程序可以在调用WSAEventSelect时将lNetworkEvent设置为0。另外,调用closesocket关闭套接字时,也会取消这种关联和为套接字注册的网络事件。
4:调用accept接受的套接字与监听套接字具有同样的属性。
如:在创建监听套接字时为其设置感兴趣的网络事件为FD_ACCEPT和FD_CLOSE。那么accept返回的套接字同样具有这些属性。它与监听套接字感兴趣的网络事件相同且使用同一个事件对象。一般情况下,我们都会为新套接字重新调用WSAEventSelect。后面的代码中在accept后会新套接字调用WSAEventSelect函数就不足为奇了!!
5:接收FD_CLOSE网络事件时,错误代码指出套接字是从容关闭还是硬关闭。如果错误代码为0,则为从容关闭;若错误代码为WSAECONNRESET错误,则是硬关闭。当应用程序接收到该网络事件时,说明对方在该套接字上执行了shutdown或者是closesocket函数调用。
WSAEventSelect模型的优势和不足。
WSAEventSelect模型的优势是可以应用在一个非窗口的Windowssockets程序中,实现对多个套接字的管理。
不足是:每个WSAEventSelect模型最多只能管理64个套接字。当应用程序需要管理多于64个套接字时,就需要额外创建线程。由于该模型需要调用多个函数,这增加了开发的难度。
以下为详细代码:
#include
#include
#include"winsock2.h"
SOCKET sListen;
#pragma comment(lib,"WS2_32.lib")
#define MAX_NUM_SOCKET 20
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
以上参考自《精通Windows sockets网络开发-基于Visual C++实现》如有纰漏,请不吝指教!
2013.1.7于山西大同