事件选择(WSAEventSelect)模型是另一个有用的异步 I/O 模型。和 WSAAsyncSelect 模型类似的是,它也允许应用程序在一个或多个套接字上,接收以事件为基
础的网络事件通知,最主要的差别在于网络事件会投递至一个事件对象句柄,而非投递到一个窗口例程。
事件通知模型要求我们的应用程序针对使用的每一个套接字,首先创建一个事件对象。创建方法是调用 WSACreateEvent 函数,它的定义如下:
WSAEVENT WSACreateEvent(void);
WSACreateEvent 函数的返回值很简单,就是一个创建好的事件对象句柄,接下来必须将其与某个套接字关联在一起,同时注册自己感兴趣的网络事件类型
(FD_READ、FD_WRITE、FD_ACCEPT、FD_CONNECT、FD_CLOSE等),方法是调用 WSAEventSelect 函数,其定义如下:
int WSAEventSelect( __in SOCKET s, //代表感兴趣的套接字 __in WSAEVENT hEventObject, //指定要与套接字关联在一起的事件对象,即用 WSACreateEvent 创建的那一个 __in long lNetworkEvents //对应一个“位掩码”,用于指定应用程序感兴趣的各种网络事件类型的一个组合。 );
其中参数 lNetworkEvents可以用以下数值进行OR操作
FD_READ 应用程序想要接收有关是否可读的通知,以便读入数据
FD_WRITE 应用程序想要接收有关是否可写的通知,以便写入数据
FD_ACCEPT 应用程序想接收与进入连接有关的通知
FD_CONNECT 应用程序想接收与一次连接完成的通知
FD_CLOSE 应用程序想接收与套接字关闭的通知
WSACreateEvent 创建的事件有两种工作状态,以及两种工作模式。工作状态分别是“已传信”(signaled)和“未传信”(nonsignaled)。工作模式则包括“人工重
设”(manual reset)和“自动重设”(auto reset)。WSACreateEvent 开始是在一种未传信的工作状态,并用一种人工重设模式,来创建事件句柄。随着网络事件触
发了与一个套接字关联在一起的事件对象,工作状态便会从“未传信”转变成“已传信”。由于事件对象是在一种人工重设模式中创建的,所以在完成了一个 I/O 请求的处理
之后,我们的应用程序需要负责将工作状态从已传信更改为未传信。要做到这一点,可调用 WSAResetEvent 函数,对它的定义如下:
BOOL WSAResetEvent( __in WSAEVENT hEvent //事件句柄; );
该函数调用是成功还是失败,会分别返回TRUE或FALSE。
应用程序完成了对一个事件对象的处理后,便应调用WSACloseEvent函数,释放由事件句柄使用的系统资源。对 WSACloseEvent 函数的定义如下:
BOOL WSACloseEvent( __in WSAEVENT hEvent //事件句柄; );
该函数调用是成功还是失败,会分别返回TRUE或FALSE。
一个套接字同一个事件对象句柄关联在一起后,应用程序便可开始I/O处理;方法是等待网络事件触发事件对象句柄的工作状态。WSAWaitForMultipleEvents 函数的设
计宗旨便是用来等待一个或多个事件对象句柄,并在事先指定的一个或所有句柄进入“已传信”状态后,或在超过了一个规定的时间周期后,立即返回。下面是
WSAWaitForMultipleEvents 函数的定义:
DWORD WSAWaitForMultipleEvents( __in DWORD cEvents, __in const WSAEVENT* lphEvents, __in BOOL fWaitAll, __in DWORD dwTimeout, __in BOOL fAlertable );
cEvents 和 lphEvents 参数定义了由 WSAEVENT 对象构成的一个数组。在这个数组中,cEvents指定的是事件对象的数量,而lphEvents对应的是一个指针,用于
直接引用该数组。要注意的是,WSAWaitForMultipleEvents 只能支持由 WSA_MAXIMUM_WAIT_EVENTS 对象规定的一个最大值,在此定义成64个。因此,针对
发出 WSAWaitForMultipleEvents 调用的每个线程,该 I/O 模型一次最多都只能支持64个套接字。假如想让这个模型同时管理不止64个套接字,必须创建额外的工作
者线程,以便等待更多的事件对象。
fWaitAll 参数指定了 WSAWaitForMultipleEvents 如何等待在事件数组中的对象。若设为TRUE,那么只有等 lphEvents 数组内包含的所有事件对象都已进入“已
传信”状态,函数才会返回;但若设为FALSE,任何一个事件对象进入“已传信”状态,函数就会返回。就后一种情况来说,返回值指出了到底是哪个事件对象造成了函数的
返回。通常,应用程序应将该参数设为 FALSE,一次只为一个套接字事件提供服务。
dwTimeout参数规定了 WSAWaitForMultipleEvents 最多可等待一个网络事件发生有多长时间,以毫秒为单位,这是一项“超时”设定。超过规定的时间,函数就会
立即返回,即使由 fWaitAll 参数规定的条件尚未满足也如此。考虑到它对性能造成的影响,应尽量避免将超时值设为0。假如没有等待处理的事件,
WSAWaitForMultipleEvents 便会返回 WSA_WAIT_TIMEOUT。如 dwTimeout 设为 WSAINFINITE(永远等待),那么只有在一个网络事件传信了一个事件对象
后,函数才会返回。
fAlertable 参数,在我们使用 WSAEventSelect 模型的时候,它是可以忽略的,且应设为 FALSE。该参数主要用于在重叠式 I/O 模型中,在完成例程的处理过程中
使用。
若 WSAWaitForMultipleEvents 收到一个事件对象的网络事件通知,便会返回一个值,指出造成函数返回的事件对象。这样一来,我们的应用程序便可引用事件数组
中已传信的事件,并检索与那个事件对应的套接字,判断到底是在哪个套接字上,发生了什么网络事件类型。对事件数组中的事件进行引用时,应该用
WSAWaitForMultipleEvents 的返回值,减去预定义的值 WSA_WAIT_EVENT_0,得到具体的引用值(即索引位置)。如下例所示:
Index = WSAWaitForMultipleEvents(...);
MyEvent = EventArray[Index - WSA_WAIT_EVENT_0];
知道了造成网络事件的套接字后,接下来可调用 WSAEnumNetworkEvents 函数,调查发生了什么类型的网络事件。该函数定义如下:
int WSAEnumNetworkEvents( __in SOCKET s, __in WSAEVENT hEventObject, __out LPWSANETWORKEVENTS lpNetworkEvents );
s 参数对应于造成了网络事件的套接字。
hEventObject 参数则是可选的;它指定了一个事件句柄,对应于打算重设的那个事件对象。由于我们的事件对象处在一个“已传信”状态,所以可将它传入,令其自动
成为“未传信”状态。如果不想用 hEventObject 参数来重设事件,那么可使用 WSAResetEvent 函数,该函数之前已经讨论过了。
lpNetworkEvents参数,代表一个指针,指向 WSANETWORKEVENTS 结构,用于接收套接字上发生的网络事件类型以及可能出现的任何错误代码。
WSANETWORKEVENTS 结构的定义如下:
typedef struct _WSANETWORKEVENTS { long lNetworkEvents; int iErrorCode[FD_MAX_EVENTS]; } WSANETWORKEVENTS, *LPWSANETWORKEVENTS;
lNetworkEvents 参数指定了一个值,对应于套接字上发生的所有网络事件类型(FD_READ、FD_WRITE 等)。注意:一个事件进入传信状态时,可能会同时发生
多个网络事件类型。例如,一个繁忙的服务器应用可能同时收到 FD_READ 和 FD_WRITE 通知。
iErrorCode 参数指定的是一个错误代码数组,同 lNetworkEvents 中的事件关联在一起。针对每个网络事件类型,都存在着一个特殊的事件索引,名字与事件类型的
名字类似,只是要在事件名字后面添加一个“_BIT”后缀字串即可。例如,对 FD_READ 事件类型来说,iErrorCode 数组的索引标识符便是 FD_READ_BIT。下述代码
片断对此进行了阐释(针对FD_READ事件):
if (NetwordEvents.lNetworkEvents & FD_READ) { if (NetworkEvents.iErrorCode[FD_READ_BIT] != 0) { printf("FD_READ failed with error %d\n", NetworkEvents.iErrorCode[FD_READ_BIT]); } }
理论知识讲完了,让我们来看看演示和代码的实现
服务器端界面
客户端界面:
服务器和客户端交互的界面截图:
下面我们来看看代码的实现:
1、初始化socket,固定套路
BOOL CWSAEventSelectDlg::InitSocket() { WSAData data; int error; error = WSAStartup(MAKEWORD(2, 2), &data); if (0 != error) { return FALSE; } if(HIBYTE(data.wVersion) != 2 && LOBYTE(data.wVersion))
{
WSACleanup();
return FALSE;
}
return TRUE;
}
2、这里为了简便,定义一个全局socket数组和event数组,一个socket操作关联一个event对象
SOCKET socketArray[WSA_MAXIMUM_WAIT_EVENTS]; WSAEVENT eventArray[WSA_MAXIMUM_WAIT_EVENTS]; DWORD dwTotal = 0; //记录接入socket的数量
3、启动一个工作线程,核心代码就在这里,该线程一直循环等待客户端的连接或者发过来的数据显示,主线程闲的蛋疼。。。
UINT ThreadProc(LPVOID lpParameter) { CWSAEventSelectDlg *pDlg = (CWSAEventSelectDlg*)lpParameter; ASSERT(pDlg != NULL); SOCKET acceptSocket = INVALID_SOCKET; SOCKET listenSocket = INVALID_SOCKET; WSAEVENT newEvent; WSANETWORKEVENTS NetworkEvents; DWORD Index; TCHAR buf[1024] = {0}; CString cstrMsg; listenSocket = socket(AF_INET, SOCK_STREAM, 0); if (INVALID_SOCKET == listenSocket) { return FALSE; } char ipbuf[1024] = {0}; wcstombs(ipbuf, pDlg->GetIpAddress(), pDlg->GetIpAddress().GetLength()); const char *p = ipbuf; sockaddr_in serverAddress; serverAddress.sin_addr.S_un.S_addr = inet_addr(p); serverAddress.sin_family = AF_INET; serverAddress.sin_port = htons(pDlg->m_iPort); if (SOCKET_ERROR == bind(listenSocket, (sockaddr*)&serverAddress, sizeof(sockaddr_in))) { return FALSE; } newEvent = WSACreateEvent(); WSAEventSelect(listenSocket, newEvent, FD_ACCEPT | FD_CLOSE); socketArray[dwTotal] = listenSocket; eventArray[dwTotal] = newEvent; dwTotal++; listen(listenSocket, SOMAXCONN); pDlg->ShowText(_T("系统消息:服务器开始监听。。。")); while (TRUE) { Index = WSAWaitForMultipleEvents(dwTotal, eventArray, FALSE, 100, FALSE); if (WSA_WAIT_TIMEOUT == Index) { continue; } WSAEnumNetworkEvents(socketArray[Index - WSA_WAIT_EVENT_0], eventArray[Index - WSA_WAIT_EVENT_0], &NetworkEvents); WSAResetEvent(eventArray[Index - WSA_WAIT_EVENT_0]); if (NetworkEvents.lNetworkEvents & FD_ACCEPT) { if (NetworkEvents.iErrorCode[FD_ACCEPT_BIT] != 0) { continue; } if (dwTotal > WSA_MAXIMUM_WAIT_EVENTS) { pDlg->ShowText(_T("系统消息: 客户端超过最大连接数。。。")); continue; } acceptSocket = accept(socketArray[Index - WSA_WAIT_EVENT_0], NULL, 0); WSAResetEvent(eventArray[Index - WSA_WAIT_EVENT_0]); newEvent = WSACreateEvent(); WSAEventSelect(acceptSocket, newEvent, FD_READ | FD_WRITE | FD_CLOSE); socketArray[dwTotal] = acceptSocket; eventArray[dwTotal] = newEvent; dwTotal++; pDlg->ShowText(_T("系统消息: 客户端已经连接成功")); } if (NetworkEvents.lNetworkEvents & FD_READ) { if (NetworkEvents.iErrorCode[FD_READ_BIT] != 0) { pDlg->ShowText(_T("接受客户端数据失败。。。")); continue; } recv(socketArray[Index - WSA_WAIT_EVENT_0], (char *)buf, 1024, 0); WSAResetEvent(eventArray[Index - WSA_WAIT_EVENT_0]); cstrMsg = buf; pDlg->ShowText(_T("Client: >") + cstrMsg); } if (NetworkEvents.lNetworkEvents & FD_WRITE) { if (NetworkEvents.iErrorCode[FD_WRITE_BIT] != 0) { continue; } } if (NetworkEvents.lNetworkEvents & FD_CLOSE) { if (NetworkEvents.iErrorCode[FD_CLOSE_BIT] != 0) { continue; } pDlg->ShowText(_T("系统消息: 客户端退出。。。")); closesocket(socketArray[Index - WSA_WAIT_EVENT_0]); WSACloseEvent(eventArray[Index - WSA_WAIT_EVENT_0]); for (int i = Index - WSA_WAIT_EVENT_0; i < dwTotal; i++) { socketArray[i] = socketArray[i + 1]; eventArray[i] = eventArray[i + 1]; }
dwTotal--; } } return TRUE; }
流程大致是这样:
1、定义一个socket数组和event数组
2、每一个socket操作关联一个event对象
3、调用WSAWaitForMultipleEvents函数等待事件的触发
4、调用WSAEnumNetworkEvents函数查看是哪个一个事件,根据事件找到相应的socket,然后进行相应的处理:比如数据显示等,同时,记得要将那个event
重置为无信号状态。
5、循环步骤3和4,直到服务器退出。