事件选择模型

来源:互联网 发布:淘宝页头图片素材 编辑:程序博客网 时间:2024/05/19 23:03
WSAEventSelect 是 WinSock 提供的一种异步事件通知I/O模型,与 WSAAsyncSelect模型有些类似。该模型同样是接收 FD_XXX 之类的网络事件,但是是通过事件对象句柄通知,而非像 WSAAsyncSelect一样依靠Windows的消息驱动机制。

与WSAAsyncSelect模型相同,WSAEventSelect将所有的SOCKET事件分为如下类型:(共十种)
                FD_READ , FD_WRITE , FD_OOB , FD_ACCEPT, FD_CONNECT , FD_CLOSE,
                FD_QOS , FD_GROUP_QOS , FD_ROUTING_INTERFACE_CHANGE , FD_ADDRESS_LIST_CHANGE
还有一个 FD_ALL_EVENTS  代表所有的事件
其中 FD_READ 的定义如下:
 #define FD_READ_BIT      0
 #define FD_READ          (1 << FD_READ_BIT)   // = 1 
其他的定义也都类似,比如: FD_ACCEPT_BIT = 3
但是并不是每一种SOCKET都能发生所有的事件,比如监听SOCKET只能发生 FD_ACCEPT 和 FD_CLOSE 事件。

在WSAEventSelect模型中,基本流程如下:
 1. 创建一个事件对象数组,用于存放所有的事件对象;
 2. 创建一个事件对象(WSACreateEvent);
 3. 将一组你感兴趣的SOCKET事件与事件对象关联(WSAEventSelect),然后加入事件对象数组;
 4. 等待事件对象数组上发生一个你感兴趣的网络事件(WSAWaitForMultipleEvents);
 5. 对发生事件的事件对象查询具体发生的事件类型(WSAEnumNetworkEvents);
 6. 针对不同的事件类型进行不同的处理;
 7. 循环进行 .4

对于TCP服务端程序而言,在创建一个监听SOCKET,绑定至某个端口然后监听后,可以创建一个事件对象然后与 FD_ACCEPT 和 FD_CLOSE 事件关联。在第6步时对于 FD_ACCEPT 事件可以将accept得到的SOCKET关联 FD_WRITE,FD_READ,FD_CLOSE事件后加入事件对象数组。

WSAEVENT WSACreateEvent(void);// 创建一个事件对象,实际上 WSAEVENT就是一个 HANDLE

int WSAEventSelect(
  _In_  SOCKET s,            // 需要关联的SOCKET
  _In_  WSAEVENT hEventObject,    // 需要关联的事件对象
  _In_  long lNetworkEvents     // 感兴趣的网络事件,不同的事件可以用 | 合并, FD_ALL_EVENTS 代表所有的事件
);
函数执行成功将返回 0,否则返回 SOCKET_ERROR, 可调用WSAGetLastError() 查看具体的错误代码

DWORD WSAWaitForMultipleEvents(
  _In_  DWORD cEvents,    // 事件对象数组的数量
  _In_  const WSAEVENT *lphEvents,   // 事件对象数组
  _In_  BOOL fWaitAll,    // 是否等待所有的事件对象受信,显然一般情况下是false
  _In_  DWORD dwTimeout,   // 超时时限,单位是毫秒,WSA_INFINITE 为无穷大
  _In_  BOOL fAlertable  // 该模型下忽略,应该设置为false
);
该函数最多只能等待64个事件,详情请参见MSDN
如果执行失败返回 WSA_WAIT_IO_COMPLETION ; 如果是超时,则返回 WSA_WAIT_TIMEOUT
如果 函数执行成功将会返回一个值,分布在 区间 [ WSA_WAIT_EVENT_0 ,(WSA_WAIT_EVENT_0+cEvents-1) ] 内
也就是说返回值 nRet-WSA_WAIT_EVENT_0 将是发生事件的对象在事件对象数组中的下标。

int WSAEnumNetworkEvents(
  _In_   SOCKET s,     //    发生事件的SOCKET
  _In_   WSAEVENT hEventObject,   //    发生事件的事件对象
  _Out_  LPWSANETWORKEVENTS lpNetworkEvents //     发生的网络事件
);
如果该函数执行成功将会返回0,然后可以通过查询网络事件判断到底发生了什么事件。

WSANETWORKEVENTS的定义如下:
typedef struct _WSANETWORKEVENTS {
       long lNetworkEvents;   // 发生的网络事件类型
       int iErrorCode[FD_MAX_EVENTS]; // 网络事件对应的错误代码
} WSANETWORKEVENTS, FAR * LPWSANETWORKEVENTS;
比如当发生 FD_READ 事件时, 那么 networkEvent.lNetworkEvents&FD_READ 将为真,同时 networkEvent.iErrorCode[FD_READ_BIT]

标明了此时的错误代码。

//服务器端#include <winsock2.h>#include <stdio.h>#pragma comment(lib, "ws2_32.lib")#define PORT5000#define MSGSIZE 1024int       g_iTotalConn = 0;SOCKET    g_CliSocketArr[MAXIMUM_WAIT_OBJECTS];WSAEVENT g_CliEventArr[MAXIMUM_WAIT_OBJECTS];DWORD WINAPI WorkerThread(LPVOID);void Cleanup(int index);int main() {// Initialize Windows Socket libraryWORD wVersionRequested;WSADATA wsaData;wVersionRequested = MAKEWORD(2, 2);WSAStartup(wVersionRequested, &wsaData);SOCKET       sListen, sClient;SOCKADDR_IN  local, client;DWORD        dwThreadId;int          iaddrSize = sizeof(SOCKADDR_IN);// Create listening socketsListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);// Bindlocal.sin_addr.S_un.S_addr = htonl(INADDR_ANY);local.sin_family = AF_INET;local.sin_port = htons(PORT);bind(sListen, (struct sockaddr *)&local, sizeof(SOCKADDR_IN));// Listenlisten(sListen, 3);// Create worker threadCreateThread(NULL, 0, WorkerThread, NULL, 0, &dwThreadId);while (TRUE) {// Accept a connectionsClient = accept(sListen, (struct sockaddr *)&client, &iaddrSize);printf("Accepted client:%s:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));// Associate socket with network eventg_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) {// Receive message from clientret = recv(g_CliSocketArr[index], szMessage, MSGSIZE, 0);if (ret == 0 || (ret == SOCKET_ERROR && WSAGetLastError() == WSAECONNRESET)) {Cleanup(index);} else {//send(g_CliSocketArr[index], szMessage, strlen(szMessage), 0);printf("recv data:%s\n", szMessage);}}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--;}

函数最多可以支持WSA_MAXIMUM_WAIT_EVENTS个对象,他的大小是64.该函数会等待网络事件的发生,如果过了指定了时间(dwTimeOut)则返回WSA_WAIT_TIMEOUT;如果在规定的时间内有事件发生,则返回该事件对象的索引(注意:在程序中要想得到发生的事件的真正索引需得用返回值减去WSA_WAIT_EVENT_0),调用失败返回WSA_WAIT_FAILED.如果将参数fWaitAll设置成FALSE,如果有多个网络事件发生,该函数也只返回一个事件对象索引,并且该事件是在事件句柄数组中最前面的一个。解决方法是循环调用该函数处理后面的受信事件. 
该函数的第一个参数是后面事件对象句柄数组的大小,第二个是个事件对象句柄数组,最后一个设置成FALSE即可. 
一旦事件对象受信那么找到与之对应的套接字,然后调用 WSAEnumNetWorkEvent 可以查看发生的网络事件,第一个参数和相应的网络事件标识做"与"运算就可.第二参数是返回的错误信息。
   
事件选择模型也比较简单,实现起来也不是太复杂,它的基本思想是将每个套接字都和一个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%。

0 0