和WSAAsyncSelect类似,它也允许应用程序在一个或多个套接字上,接收以事件为基础的网络事件通知。
该模型最主要的区别是在于网络事件是由对象句柄完成的,而不是通过窗口例程完成。
事件通知
事件通知模型要求应用程序针对打算使用的每一个套接字,首先创建一个事件对象。创建方法是调用WSACreateEvent函数:
- WSAEVENT WSACreateEvent(void);
WSACreateEvent的返回值很简单,就是一个人工重设的事件对象句柄,一旦得到了事件对象句柄之后,必须将它与某个套接字关联起来,同时注册感兴趣的网络事件类型。要做到这一点,方法是调用WSAEventSelect函数:
- int WSAEventSelect(
- SOCKET s,
- WSAEVENT hEventObject,
- long lNetworkEvents
- );
为WSAEventSelect创建的事件有两种工作状态和两种工作模式,其中,两种工作状态是已传信(signaled)和为传信(non-signaled)。工作模式则包括人工重设和自动重设。WSAEventSelect最初是在一种为传信的工作状态,并用一种人工重设模式,来创建事件句柄。若网络事件触发了与一个套接字关联在一起的事件对象,工作状态便会从为传信变为已传信。由于事件对象是在一种人工重设模式下创建的,所有完成了一个I/O请求处理之后,应用程序需要负责将工作模式从已传信更改为未传信,要做到这一点,可调用WSAResetEvent函数:
- BOOL WSAResetEvent(WSAEVENT hEvent);
完成了对某个事件对象的处理之后,便应调用WSACloseEvent函数释放由事件句柄使用的系统资源。
- BOOL WSACloseEvent(WSAEVENT hEvent);
套接字同一个事件对象句柄关联在一起后,应用程序便可开始I/O处理,这就需要应用程序等待网络事件触发事件对象句柄的工作状态,WSAWaitForMultipleEvents函数的设计宗旨就是用来等待一个或多个事件对象句柄,并在事先指定的一个或所有句柄进入已传信状态后,或在超过了一个规定的时间周期后,立即返回
- DWORD WSAWaitForMultipleEvents(
- DWORD cEvents,
- const WSAEVENT FAR* lphEvents,
- BOOL fWaitAll,
- DWORD dwTimeout,
- 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等待一个网络事件发生时,最多可等待多长时间,以毫秒为单位,超过规定时间,函数就返回。如果超时值为0,函数会检测指定的事件对象状态,并立即返回。这样,应用程序可以实现对事件对象的轮询。如果没有可处理事件, WSAWaitForMultipleEvents便会返回WSA_WAIT_TIMEOUT,如果dwTimeout被设为WSA_INFINITE,那么只有在网络事件传信了一个事件对象后,函数才会返回。fAlertable可被忽略,设为FALSE。
应该注意到一次只服务一个已传信事件(fWaitAll设为FALSE),就可能让套接字一直“挨饿”,且可能持续到事件数组的末尾。
若WSAWaitForMultipleEvents收到一个事件对象的网络通知,便会返回一个值,指出造成函数返回的事件对象。这样,应用程序便可引用事件数组中已传信的事件,并检索与那个事件对应的套接字,并判断到底是那个套接字上,发生了什么样的网络事件。对事件数组中的事件进行引用时,应该用WSAWaitForMultipleEvents的返回值,减去预定义的WSA_WAIT_EVENT_0,从而得到具体的引用值。
- Index = WSAWaitForMultipleEvents(...);
- MyEvent = EventArray[Index - WSA_WAIT_0];
指定了造成网络事件的套接字后, 接下来可调用WSAEnumNetworkEvents函数,调查发生了那些网络事件,该函数定义如下:
- int WSAEnumNetworkEvents(
- SOCKET s,
- WSAEVENT hEventObject,
- LPWSANETWORKEVENTS lpNetworkEvents
- );
WSANETWORKEVENTS结构如下:
- typedef struct _WSANETWORKEVNETS
- {
- long lNetworkEvents;
- inbt iErrorCode[FD_MAX_EVENTS];
- }WSANETWORKEVENTS, FAR* LPWSANETWORKEVENTS;
lNetworkEvents指定一个值,对应于该套接字上发生的所有网络事件类型
iErrorCode指定了一个错误代码数组,这个数组同lNetworkEvents中的事件关联在一起,针对每个网络事件类型,都存在着一个特殊的事件索引,它与事件类型的名称类似,只是事件类型名称后面添加一个"_BIT"作为后缀字符串。例如,对FD_READ事件类型来说,iErrorCode数组的索引标识便是FD_READ_BIT。
-
- if(NetworkEvents.lNetworkEvents & FD_READ)
- {
- if(NetworkEvents.iErrorCode[FD_READ_BIT]!=0)
- {
- printf("FD_READ failed with error %d \n", NetworkEvents.iErrorCode[FD_READ_BIT]);
- }
- }
演示用WSAEventSelect模型的创建步骤:
- SOCKET SocketArray[WSA_MAXIMUM_WAIT_EVENTS];
- WSAEVENT EventArray[WSA_MAXIMUM_WAIT_EVENTS];
- WSAEVENT NetEvent;
- SOCKADDR_IN addr;
- SOCKET Accept,Listen;
- DWORD EventTotal = 0;
- DWORD Index;
- DOWRD i;
-
- Listen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
- addr.sin_family = AF_INET;
- addr.sin_port = htons(5050);
- addr.sin_addr.s_addr = htonl(INADDR_ANY);
- bind(Listen, (SOCKADDR*)&addr, sizeof(SOCKADDR_IN));
-
- NetEvent = WSACreateEvent();
-
- WSAEventSelect(Listen, NetEvent, FD_ACCEPT|FD_CLOSE);
-
- listen(Listen, 5);
-
- SocketArray[EventTotal] = Listen;
- EventArray[EventTotal] = NetEvent;
- EventTotal++;
-
- while(TRUE)
- {
-
- Index = WSAWaitForMultipleEvents(EventTotal, EventArray, FALSE, WSA_INFINITE, FALSE);
- Index = Index - WSA_WAIT_EVENT_0;
-
-
- for(i = Index; i<EventTotal; i++)
- {
- Index = WSAWaitForMultipleEvents(1, &EventArray[i], TRUE, 1000, FALSE);
- if((Index==WSA_WAIT_FAILED)||(Index==WSA_WAIT_TIMEOUT))
- {
- continue;
- }
- else
- {
- Index = i;
- WSAEnumNetworkEvents(SocketArray[Index], EventArray[Index], &NetworkEvents);
-
- if(NetworkEvents.lNetworkEvents&FD_ACCEPT)
- {
- if(NetworkEvents.iErrorCode[FD_ACCEPT_BIT]!=0)
- {
- printf("FD_ACCEPT failed with error %d\n", NetworkEvents.iErrorCode[FD_ACCEPT_BIT]);
- break;
- }
-
- Accept = accept(SocketArray[Index], NULL, NULL);
-
- if(EventTotal>WSA_MAXIMUM_WAIT_EVENTS)
- {
- printf("Too Many Connections");
- closesocket(Accept);
- break;
- }
- NetEvent = WSACreateEvent();
- WSAEventSelect(Accept, NetEvent, FD_READ|FD_WRITE|FD_CLOSE);
- EventArray[EventTotal] = NetEvent;
- SocketArray[EventTotal] = Accept;
- EventTotal++;
- printf("Socket %d connected \n", Accept);
- }
-
- if(NetworkEvents.lNetworkEvents&FD_READ)
- {
- if(NetworkEvents.iErrorCode[FD_READ_BIT]!=0)
- {
- printf("FD_READ failed with error %d\n", NetworkEvents.iErrorCode[FD_READ_BIT]);
- break;
- }
-
- recv(SocketArray[Index-WSA_WAIT_EVENT_0], buffer, sizeof(buffer), 0);
- }
-
- if(NetworkEvents.lNetworkEvents&FD_WRITE)
- {
- if(NetworkEvents.iErrorCode[FD_WRITE_BIT]!=0)
- {
- printf("FD_WRITE failed with error %d\n", NetworkEvents.iErrorCode[FD_WRITE_BIT]);
- break;
- }
- send(SocketArray[Index-WSA_WAIT_EVENT_0], buffer, sizeof(buffer), 0);
- }
-
- if(NetworkEvents.lNetworkEvents&FD_CLOSE)
- {
- if(NetworkEvents.iErrorCode[FD_CLOSE_BIT]!=0)
- {
- printf("FD_CLOSE failed with error %d\n", NetworkEvents.iErrorCode[FD_CLOSE_BIT]);
- break;
- }
- closesocket(SocketArray[Index]);
-
- CompressArrays(EventArray, SocketArray, &EventTotal);
- }
- }
- }
- }
优势,概念简单,不需要窗口环境。
缺点,它每次只等待64个事件,处理多个套接字时,有必要使用一个线程池
=======================================================================
- #include<stdio.h>
- #include<winsock2.h>
- #pragma comment(lib, "ws2_32.lib");
-
- #define PORT 5050
- #define MSGSIZE 1024
-
- int g_iTotalConn = 0;
- SOCKET g_CliSocketArr[MAXIMUM_WAIT_OBJECTS];
- WSAEVENT g_CliEventArr[MAXIMUM_WAIT_OBJECTS];
-
- DWORD WINAPI WorkerThread(LPVOID lpParam);
- void Cleanup(int index);
-
- int main()
- {
- WSADATA wsaData;
- SOCKET sListen, sClient;
- SOCKADDR_IN local, client;
- DWORD dwThreadId;
- int iAddrSize = sizeof(SOCKADDR_IN);
- WSAStartup(MAKEWORD(2,2), &wsaData);
- sListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
- memset(&local, 0, sizeof(SOCKADDR_IN));
- local.sin_family = AF_INET;
- local.sin_port = htons(PORT);
- local.sin_addr.s_addr = htonl(INADDR_ANY);
- bind(sListen, (SOCKADDR*)&local, sizeof(SOCKADDR_IN));
- listen(sListen, 3);
- CreateThread(NULL, 0, WorkerThread, NULL, 0, &dwThreadId);
- while(TRUE)
- {
-
- sClient = accept(sListen, (SOCKADDR*)&client, &iAddrSize);
- printf("Accepted client:%s:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
-
- g_CliSocketArr[g_iTotalConn] = sClient;
- g_CliEventArr[g_iTotalConn] = WSACreateEvent();
- WSAEventSelect(g_CliSocketArr[g_iTotalConn], g_CliEventArr[g_iTotalConn], FD_READ|FD_CLOSE);
- g_iTotalConn++;
- }
- return 0;
- }
-
- DWORD WINAPI WorkerThread(LPVOID lpParam)
- {
- int ret, index;
- WSANETWORKEVENTS NetworkEvents;
- char szMessage[MSGSIZE];
-
- while (TRUE)
- {
- ret = WSAWaitForMultipleEvents(g_iTotalConn, g_CliEventArr, FALSE, 1000, FALSE);
- if (ret == WSA_WAIT_FAILED || ret == WSA_WAIT_TIMEOUT)
- {
- continue;
- }
- index = ret - WSA_WAIT_EVENT_0;
- WSAEnumNetworkEvents(g_CliSocketArr[index], g_CliEventArr[index], &NetworkEvents);
- if (NetworkEvents.lNetworkEvents & FD_READ)
- {
-
- ret = recv(g_CliSocketArr[index], szMessage, MSGSIZE, 0);
- if (ret == 0 || (ret == SOCKET_ERROR && WSAGetLastError() == WSAECONNRESET))
- {
- Cleanup(index);
- }
- else
- {
- szMessage[ret] = '\0';
- send(g_CliSocketArr[index], szMessage, strlen(szMessage), 0);
- }
- }
- if (NetworkEvents.lNetworkEvents & FD_CLOSE)
- {
- Cleanup(index);
- }
- }
-
- return 0;
- }
-
- void Cleanup(int index)
- {
- closesocket(g_CliSocketArr[index]);
- WSACloseEvent(g_CliEventArr[index]);
- if (index < g_iTotalConn-1)
- {
- g_CliSocketArr[index] = g_CliSocketArr[g_iTotalConn-1];
- g_CliEventArr[index] = g_CliEventArr[g_iTotalConn-1];
- }
- g_iTotalConn--;
- }
事件选择模型也比较简单,实现起来也不是太复杂,它的基本思想是将每个套接字都和一个WSAEVENT对象对应起来,并且在关联的时候指定需要关注的哪些网络事件。一旦在某个套接字上发生了我们关注的事件(FD_READ和FD_CLOSE),与之相关联的WSAEVENT对象被Signaled。程序定义了两个全局数组,一个套接字数组,一个WSAEVENT对象数组,其大小都是MAXIMUM_WAIT_OBJECTS(64),两个数组中的元素一一对应。
同样的,这里的程序没有考虑两个问题,一是不能无条件的调用accept,因为我们支持的并发连接数有限。解决方法是将套接字按 MAXIMUM_WAIT_OBJECTS分组,每MAXIMUM_WAIT_OBJECTS个套接字一组,每一组分配一个工作者线程;或者采用 WSAAccept代替accept,并回调自己定义的Condition Function。第二个问题是没有对连接数为0的情形做特殊处理,程序在连接数为0的时候CPU占用率为100%。