确定与所提供的FD_XXX网络事件集合相关的一个事件对象。
#include
int WSAAPI WSAEventSelect ( SOCKET s, WSAEVENT
hEventObject, long lNetworkEvents );
s:一个标识套接口的描述字。
hEventObject:一个句柄,用于标识与所提供的FD_XXX网络事件集合相关的一个事件对象。
lNetworkEvents:一个屏蔽位,用于指定感兴趣的FD_XXX网络事件组合。
返回值:
如果应用程序指定的网络事件及其相应的事件对象成功设置,则返回0。否则的话,将返回INVALID_SOCKET错误,应用程序可通过WSAGetLastError()来获取相应的错误代码。
在使用select()和WSAAsyncSelect()函数时,WSAEventSelect()常用来决定何时进行数据传送操作(如send()或recv()),并期望能立即成功。但是一个稳定的应用程序应该做好这样的准备,即事件对象被设置,并且一个WinSock调用以WSAEWOULDBLOCK立即返回 。举例来说,有可能发生下述操作序列:
(i) 套接口s上到达数据;WinSock设置了WSAEventSelect事件对象。
(ii) 应用程序进行其他操作。
(iii) 在进行操作时,应用程序调用了ioctlsocket(s, FIONREAD...)并发现有数据可读。
(iv) 应用程序调用一个recv(s,...)来读取数据。
(v) 最后应用程序等待WSAEventSelect()所指定的数据对象,该数据对象指出数据可读。
(vi) 应用程序调用recv(s,...),但以WSAEWOULDBLOCK错误失败。
其他的操作序列也是可能的。
成功地记录了网络事件的发生(通过设置内部网络事件记录的相应位),并且将相应的事件对象设置了信号后,不会对该网络事件作进一步的操作,直到应用程序调用了相应的函数显式地重新允许该网络事件及相应事件对象的信号。
网络事件 重新允许函数
FD_READ recv() 或 recvfrom()
FD_WRITE send() 或 sendto()
FD_OOB recv()
FD_ACCEPT accept() 或WSAAccept(),直到返回的错误代码为 WSATRY_AGAIN,指明条件函数返回CF_DEFER。
FD_CONNECT NONE
FD_CLOSE NONE
FD_QOS 用SIO_GET_QOS 命令调用WSAIoctl()。
FD_GROUP_QOS 用SIO_GET_GROUP_QOS命令调用WSAIoctl()。
错误代码:
WSANOTINITIALISED 在调用本API之前应成功调用WSAStartup()。
WSAENETDOWN 网络子系统失效。
WSAEINVAL 参数中有非法值,或者指定的套接口处于非法状态。
WSAEINPROGRESS 一个阻塞的WinSock调用正在进行中,或者服务提供者仍在处理一个回调函数
WSAENOTSOCK 描述字不是一个套接口。
另请参阅:WSACloseEvent() ,WSACreateEvent(),WSAEnumNetworkEvents(),WSAGetOverlappedResult(),WSAWaitForMultipleEvents().
它允许程序在一个套接字上接收以事件为基础的网络事件通知。
事件对象
该模型要求程序对打算使用的每个套接字首先创建一个事件对象,方法是调用WSACreateEvent函数
WSAEVENT WSACreateEvent( void );
该函数返回创建好的事件对象。
事件对象创建好后,必须将其与某个套接字关联在一起,同时注册感兴趣的网络事件类型。需调用WSAEventSelect函数。
int WSAEventSelect(
SOCKET sock, /* 套接字 */
WSAEVENT hEvent, /* 事件对象 */
long lNetworkEvents /* 对应一个"位掩码",指定感兴趣的网络事件类型 */
);
创建的事件拥有两种工作状态和两种模式。
工作状态 : 已传信和未传信
工作模式 : 人工重设和自动重设
由WSACreateEvent创建的事件对象,默认工作状态为"未传信",工作模式为"人工重设",当网络事件触发了与套接字关联在一起的事件对象时,工作状态会转变为"已传信",而在完成了一个I/O请求的处理之后,因为工作模式为"人工重设",因此,程序需要将工作状态从"已传信"改变为"未传信",使用WSAResetEvent函数
BOOL WSAResetEvent( WSAEVENT hEvent );
当程序不在对事件对象进行处理时,应调用WSACloseEvent函数将其关闭
BOOL WSACloseEvent( WSAEVENT hEvent );
套接字与事件对象关联在一起后,便可开始进行I/O操作,方法是等待一个或多个事件对象,并在事先指定的一个或所有句柄进入"已传信"状态,或在超过了一个规定的时间间隔后,立即返回。使用WSAWaitForMultipleEvents函数
DWORD WSAWaitForMultipleEvents(
DWORD cEvents, /* 指定lphEvents的数量,最大为WSA_MAXIMUM_WAIT_EVENTS(64) */
const WSAEVENT *lphEvents, /* 事件对象数组 */
BOOL fWaitAll,
DWORD dwTimeout, /* 等待时间,为0,立即返回,为WSA_INFINITE,永不返回 */
BOOL fAlertable /* 可设为FALSE */
);
fWaitAll : 指定如何等待lphEvents数组内的事件对象,为TRUE时,只有当所有事件对象都进入"已传信"时,才会返回,若为FALSE时,只要一个事件进入"已传信"状态,就返回,通常设为FALSE
返回值:返回发生网络事件的事件对象索引
当发生网络事件后,接下来可调用WSAEnumNetworkEvents函数来检查发生了什么类型的网络事件
int WSAEnumNetworkEvents(
SOCKET sock,
WSAEVENT hEvent,
LPWSANETWORKEVENTS lpNetworkEvents
);
lpNetworkEvents : 为一个结构,内包含发生的事件及可能的错误代码
typedef struct _WSANETWORKEVENTS
{
long lNetworkEvents; /* 发生的网络事件 */
int iErrorCode[FD_MAX_EVENTS]; /* 可能的错误代码 */
}WSANETWORKEVENTS,*LPWSANETWORKEVENTS;
iErrorCode : 指定错误代码数组,其索引名为事件类型名+'_BIT'(如FD_READ_BIT)
近来在Windows下用WSAEventSelect时,碰到一个棘手的问题,当然现在已经解决了。
问题描述:
一个Server,一个ClientA,一个ClientB,Server用WSAEventSelect模型监听(只有监听,没有读写),ClientA在连接Server后,ClientA对应的EventA被触发,Server的WSAWaitForMultipleEvents等待到EventA,ClientB连接Server时,TCP三次握手成功,ClientB与Server的TCP状态被置为ESTABLISHED,然而Server的WSAWaitForMultipleEvents没有等待到EventB被触发。
用netstat看了一下,ClientB与Server的状态是ESTABLISHED,此时如果ClientB退出,由于Server无法正常Close该连接,因此Server的状态不是TIME_WAIT而是CLOSE_WAIT(持续2小时),Client的状态是FIN_WAIT_2(持续10分钟)。
我尝试将ClientA主动关闭后再次连接Server,Server的WSAWaitForMultipleEvents在wait到EventA之后,EventB此时也被触发。
开始一直以为问题的根源在于WSAEventSelect的使用上,毕竟,之前没有系统写过类似的代码,难免怀疑到事件模型的使用上。多方查阅资料,最后还是没有发现类似问题的解决方案。
又跟了一上午之后,Kevin开始怀疑是多线程使用的问题,我看了一下,的确没有对event的多线程操作进行处理,但因为在另一个应用中,使用了同样的模块,却没有该问题。最后考虑必要性时还是放弃了加临界资源,无视多线程同步问题。Kevin本来劝我换个模型,但我固执的认为要做就把这事儿做好。因为下午还要回学校一趟,就想尽快搞定,毕竟因为这一块已经把Kevin的进度拖了一周了,心下还是过意不去,而且隐约感觉到离问题的解决越来越近了。
问题分析:
在对着WSAWaitForMultipleEvents思考了半天之后,忽然开窍了,如果ThreadA在WSAWaitForMultipleEvents时,只有一个EventA被WSAEventSelect并set到signaled状态,则该EventA会被wait成功,ThreadA处理EventA之后继续阻塞在WSAWaitForMultipleEvents。此时,ThreadB通过WSAEventSelect将EventB初始化为nonsignaled状态,之后即使EventB被set为signaled状态,但ThreadA的WSAWaitForMultipleEvents因为处于阻塞状态,不可能刷新事件集,也就不可能wait到EventB,最终导致了ClientB的请求无法被响应。如果EventA被触发则会被ThreadA等待到,WSAWaitForMultipleEvents返回后再次进入时事件集已经被刷新,EventB被wait到也就不难理解了。
问题解决:
说到底是因为当ThreadA阻塞在WSAWaitForMultipleEvents处之时,事件集的变更无法立即得到体现。如果允许上层应用随时create或close一些event,则WSAWaitForMultipleEvents就不应该无限阻塞下去。
因此最后的一个解决方法就是让WSAWaitForMultipleEvents超时返回并Sleep一段时间,当WSAWaitForMultipleEvents再次进入时事件集得以更新。
想了一下,另一个应用中之所以没出现该问题也只是个巧合,因为该应用中ThreadB的两次WSAEventSelect间隔很短,在ThreadA获得时间片之前已经确定了事件集。
说白了这也不是一个什么大问题,甚至谈不上任何难度,但是因为之前对WSAEventSelect没有一个清晰的概念,因此在发现和分析问题上花费了大量时间,加上在VS2005调试过程中,有个别文件更新时没有被重新编译,也耗费了很多无谓的时间,以至于我们都在考虑是不是要放弃IDE,因为我们确实太依赖IDE了,有些TX为了稳妥,每次都是“重新生成整个解决方案”,如果一个解决方案有几千个文件、几十万行的代码,估计重编一次也要花个几分钟吧。
总结:
netstat观察的网络连接处于ESTABLISHED状态并不意味着逻辑连接被accept,只是表明客户端connect的TCP物理连接(三次握手)被服务器端ack,如果服务器没有accept到该连接,证明网络模块代码有问题;
多线程怎么都是个问题,线程同步尽量避免,毕竟,用Kevin的话来说,加锁是丑陋的。但在涉及到同步问题时,还是权衡一下,我这儿之所以最后没有加临界区,是因为事件主要是在ThreadA中处理,ThreadB中只有create操作,而且ThreadA对事件集的刷新要求不是那么严格,也就不考虑加临界区了;
如果能力和条件允许的话,放弃IDE吧,IDE的确不是个好东西,我主要是指在编译链接的时候,如果作为编辑器说不定还会好用:)。