WSAEventSelect模型编程
这个模型是一个简单的异步事件模型,使用起来比较方便,现在说一下其的具体的用法和需要注意的地方。
一,模型的例程(服务端):
先举一个王艳平网络通信上的例子:
////////////////////////////////////////////////// // WSAEventSelect文件 #include "initsock.h" #include <stdio.h> #include <iostream.h> #include <windows.h> // 初始化Winsock库 CInitSock theSock; int main() { // 事件句柄和套节字句柄表 WSAEVENT eventArray[WSA_MAXIMUM_WAIT_EVENTS]; SOCKET sockArray[WSA_MAXIMUM_WAIT_EVENTS]; int nEventTotal = 0; USHORT nPort = 4567; // 此服务器监听的端口号 // 创建监听套节字 SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); sockaddr_in sin; sin.sin_family = AF_INET; sin.sin_port = htons(nPort); sin.sin_addr.S_un.S_addr = INADDR_ANY; if(::bind(sListen, (sockaddr*)&sin, sizeof(sin)) == SOCKET_ERROR) { printf(" Failed bind() \n"); return -1; } ::listen(sListen, 5); // 创建事件对象,并关联到新的套节字 WSAEVENT event = ::WSACreateEvent(); ::WSAEventSelect(sListen, event, FD_ACCEPT|FD_CLOSE); // 添加到表中 eventArray[nEventTotal] = event; sockArray[nEventTotal] = sListen; nEventTotal++; // 处理网络事件 while(TRUE) { // 在所有事件对象上等待 int nIndex = ::WSAWaitForMultipleEvents(nEventTotal, eventArray, FALSE, WSA_INFINITE, FALSE); // 对每个事件调用WSAWaitForMultipleEvents函数,以便确定它的状态 nIndex = nIndex - WSA_WAIT_EVENT_0; for(int i=nIndex; i<nEventTotal; i++) { nIndex = ::WSAWaitForMultipleEvents(1, &eventArray[i], TRUE, 1000, FALSE); if(nIndex == WSA_WAIT_FAILED || nIndex == WSA_WAIT_TIMEOUT) { continue; } else { // 获取到来的通知消息,WSAEnumNetworkEvents函数会自动重置受信事件 WSANETWORKEVENTS event; ::WSAEnumNetworkEvents(sockArray[i], eventArray[i], &event); if(event.lNetworkEvents & FD_ACCEPT) // 处理FD_ACCEPT通知消息 { if(event.iErrorCode[FD_ACCEPT_BIT] == 0) { if(nEventTotal > WSA_MAXIMUM_WAIT_EVENTS) { printf(" Too many connections! \n"); continue; } SOCKET sNew = ::accept(sockArray[i], NULL, NULL); WSAEVENT event = ::WSACreateEvent(); ::WSAEventSelect(sNew, event, FD_READ|FD_CLOSE|FD_WRITE); // 添加到表中 eventArray[nEventTotal] = event; sockArray[nEventTotal] = sNew; nEventTotal++; } } else if(event.lNetworkEvents & FD_READ) // 处理FD_READ通知消息 { if(event.iErrorCode[FD_READ_BIT] == 0) { char szText[256]; int nRecv = ::recv(sockArray[i], szText, strlen(szText), 0); if(nRecv > 0) { szText[nRecv] = '\0'; printf("接收到数据:%s \n", szText); } } } else if(event.lNetworkEvents & FD_CLOSE) // 处理FD_CLOSE通知消息 { if(event.iErrorCode[FD_CLOSE_BIT] == 0) { ::closesocket(sockArray[i]); for(int j=i; j<nEventTotal-1; j++) { sockArray[j] = sockArray[j+1]; sockArray[j] = sockArray[j+1]; //这个是个BUG,应为: eventArray[j] = eventArray[j+1];还真没注意,直到同事提 起才注意到。 } nEventTotal--; } } else if(event.lNetworkEvents & FD_WRITE) // 处理FD_WRITE通知消息 { } } } } return 0; }
二、例程的分析
1、事件的创建和绑定
前面的一些设置我们略过,从WSAEVENT 开始说起,跟踪发现在winsock2.h中有如下定义:
#define WSAEVENT HANDLE
这个事件说明是一个句柄,我们知道在事件中有两种状态,一种是手动处理事件,一种是自动的,这里使用WSACreateEvent()这个函数创建返回的事件句柄,正常的返回的情况下,其创建的是一个手工处理的句柄,否则,其返回WSA_INVALID_EVENT,表明创建未成功,如果需要知道更多的信息WSAGetLastError()这个函数来得到具体的信息出错代码。这里埋伏下了一个雷,为什么创建的是手工处理的事件(manually reset ),那后面为什么没有WSAResetEvent()这个函数来处理事件,先记下。
然后接着讲,
::WSAEventSelect(sListen, event, FD_ACCEPT|FD_CLOSE); // 添加到表中 eventArray[nEventTotal] = event; sockArray[nEventTotal] = sListen; nEventTotal++;
int nIndex = ::WSAWaitForMultipleEvents(nEventTotal, eventArray, FALSE, WSA_INFINITE, FALSE); nIndex = nIndex - WSA_WAIT_EVENT_0;
这个函数用来监听多个事件(就是上面我们绑定的事件)的状态,有状态或者是事件被触发,就会返回,否则会按照你设置的参数进行操作。
前面两个参数,第一个是监听的数量,最小是一,MSDN上有,第二是一个事件的数组,第三个是精彩的去处,如果设置成TRUE,那么只有这第二个事件数组中的所有的事件都受信或者说触发,才会动作,如果是FALSE呢,则只要有一个就可以动作。第五个是超时设置,可以是0,是WSA_INFINITE,也可以是其它的数值,这里有一个问题,如果设置为0会造成程序的CPU占用率过高,WSA_INFINITE则可能会出现在等待数量为一个字时,且第三个参数设置为TRUE,产生死套接字的长期阻塞。所以还是设置成一个经验值为好,至于这个经验值是多少,看你的程序的具体的应用了。
其实这个函数本质还是调用WaitForMulipleObjectsEx这个函数,MSDN上讲WSAEventSelect模型在等待时不占用CPU时间,就是这个原因,所以其比阻塞的SOCKET通信要效率高很多,其实那个消息的模型WSAAsycSelect和这个事件的模型也差不多,异曲同工之妙吧。不过适用范围是有区别的,这个可以用在WINCE上。消息则不行。
这里就又引出一个注意点,在这个模型里,如果同时有几个事件受信,或者说触发,那么nIndex = ::WSAWaitForMultipleEvents()只返回最前面的一个事件,那么怎么解决其后面的呢,书上有曰:多次循环调用这个就可以了,所以才会引出下面的再次在for循环里调用
nIndex = ::WSAWaitForMultipleEvents(1, &eventArray[i], TRUE, 1000, FALSE);
注意这里参数的变化,数量为1,事件为[i],但事件会不断的增长,全面受信改成了TRUE,超时为1000,最后的这个参数在这里只能设置成FALSE,具体为什么查MSDN去。
如果这里我们处理的不好,如果把1000改成无限等待的话,就可以出现上面说的死套接字的无限阻塞,也就是说如果一个套接字死掉了,你没有在事件队伍里删除他,那么他就会一直在这儿阻塞,即使后面有事件也无法得到响应,但是,如果你的套接字只有一个连接的话,就没有什么了,可以改成无限等待。不过,最好还是别这样,因为如果你处理一个失误,就会产生死的套接字(比如重连,但你没有删除先前无用的套接字)。
用两个::WSAWaitForMultipleEvents函数,
一个用来处理监听多个事件数组,一个用来遍历每个数组事件,
防止出现丢失响应的现象,所以其参数的设置是不同的,一定要引起注意。
2.2事件的处理
然后戏又来了,上面说的读写监听呢,就在这里出现了,包括上面埋伏下的一个雷,也在这里处理了:
首先调用::WSAEnumNetworkEvents(sockArray[i], eventArray[i], &event),把上面的雷给拆了,
::WSAEnumNetworkEvents会自动重置事件,
然后得到事件的索引或者说ID,
if(event.lNetworkEvents & FD_ACCEPT) // 处理FD_ACCEPT通知消息 { if(event.iErrorCode[FD_ACCEPT_BIT] == 0) { if(nEventTotal > WSA_MAXIMUM_WAIT_EVENTS) { printf(" Too many connections! \n"); continue; } SOCKET sNew = ::accept(sockArray[i], NULL, NULL); WSAEVENT event = ::WSACreateEvent(); ::WSAEventSelect(sNew, event, FD_READ|FD_CLOSE|FD_WRITE); // 添加到表中 eventArray[nEventTotal] = event; sockArray[nEventTotal] = sNew; nEventTotal++; } }
代码里重新调用了事件创建和事件绑定函数,并且将两个数组自动增大,最最重要的是我们终于看到了,FD_READ|FD_CLOSE|FD_WRITE,
明白了吧,这个简单的程序的本质其实是将 读 写 和 接收关闭 的套接字混合到了一起,
而在后面的服务器例程里,我们发现,这个已经拆开,并且重新手动设置受信的事件,调用了::ResetEvent(event)。这样不就完美的拆除了上面的雷么。
2.3 其它处理方法
当程序继续循环到最外层时,::WSAWaitForMultipleEvents无限等待所有的事件,只要有一个事件响应,就会进入到下一层循环,如果是接收就重复上述的动作,如果是读写就进入:
else if(event.lNetworkEvents & FD_READ) // 处理FD_READ通知消息 { if(event.iErrorCode[FD_READ_BIT] == 0) { char szText[256]; int nRecv = ::recv(sockArray[i], szText, strlen(szText), 0); if(nRecv > 0) { szText[nRecv] = '\0'; printf("接收到数据:%s \n", szText); } } } else if(event.lNetworkEvents & FD_CLOSE) // 处理FD_CLOSE通知消息 { if(event.iErrorCode[FD_CLOSE_BIT] == 0) { ::closesocket(sockArray[i]); for(int j=i; j<nEventTotal-1; j++) { sockArray[j] = sockArray[j+1]; sockArray[j] = sockArray[j+1]; } nEventTotal--; } } else if(event.lNetworkEvents & FD_WRITE) // 处理FD_WRITE通知消息 { }
如此往复,不就达到了不断接收连接和处理数据的问题么。
这里还重复一下,网上很多程序都没有处理多个事件同时受信的情况,在网上和各种资料中,也有的只使用一个::WSAWaitForMultipleEvents函数,但参数的设置得重新来过,而且得小心的处理各种的事件和异常的发生。可能在小并发量和小数据量时没有问题,但并发一多数据一大,可能会出现丢数据的问题,没有做过测试,但可能是很大的。否则不会说遍历调用这个函数了。
2.4 FD_WRITE 事件的触发
这里得罗嗦两句FD_WRITE 事件的触发,前面的都好理解,主要是啥时候儿会触发这个事件呢,我们在一开始只对接收和关闭进行了监听,为什么没有这个FD_WRITE事件的
监听呢,
这就引出了下面的东东:(从一个网友那转来)
下面是MSDN中对FD_WRITE触发机制的解释:
The FD_WRITE network event is handled slightly differently. An FD_WRITE network event is recorded when a socket is first connected with connect/WSAConnect or
accepted with accept/WSAAccept, and then after a send fails with WSAEWOULDBLOCK and buffer space becomes available. Therefore, an application can assume that
sends are possible starting from the first FD_WRITE network event setting and lasting until a send returns WSAEWOULDBLOCK. After such a failure the
application will find out that sends are again possible when an FD_WRITE network event is recorded and the associated event object is set
FD_WRITE事件只有在以下三种情况下才会触发
①client 通过connect(WSAConnect)首次和server建立连接时,在client端会触发FD_WRITE事件
②server通过accept(WSAAccept)接受client连接请求时,在server端会触发FD_WRITE事件
③send(WSASend)/sendto(WSASendTo)发送失败返回WSAEWOULDBLOCK,并且当缓冲区有可用空间时,则会触发FD_WRITE事件
①②其实是同一种情况,在第一次建立连接时,C/S端都会触发一个FD_WRITE事件。
主要是③这种情况:send出去的数据其实都先存在winsock的发送缓冲区中,然后才发送出去,如果缓冲区满了,那么再调用send(WSASend,sendto,WSASendTo)的话,就会返回一个 WSAEWOULDBLOCK的错误码,接下来随着发送缓冲区中的数据被发送出去,缓冲区中出现可用空间时,一个 FD_WRITE 事件才会被触发,这里比较容易混淆的是 FD_WRITE 触发的前提是 缓冲区要先被充满然后随着数据的发送又出现可用空间,而不是缓冲区中有可用空间,也就是说像如下的调用方式可能出现问题
else if(event.lNetworkEvents & FD_WRITE) { if(event.iErrorCode[FD_WRITE_BIT] == 0) { send(g_sockArray[nIndex], buffer, buffersize); .... } else { } }
问题在于建立连接后 FD_WRITE 第一次被触发, 如果send发送的数据不足以充满缓冲区,虽然缓冲区中仍有空闲空间,但是 FD_WRITE 不会再被触发,程序永远也等不到可以发送的网络事件。
基于以上原因,在收到FD_WRITE事件时,程序就用循环或线程不停的send数据,直至send返回WSAEWOULDBLOCK,表明缓冲区已满,再退出循环或线程。
当缓冲区中又有新的空闲空间时,FD_WRITE 事件又被触发,程序被通知后又可发送数据了。
上面代码片段中省略的对 FD_WRITE 事件处理
else if(event.lNetworkEvents & FD_WRITE) { if(event.iErrorCode[FD_WRITE_BIT] == 0) { while(TRUE) { // 得到要发送的buffer,可以是用户的输入,从文件中读取等 GetBuffer.... if(send(g_sockArray[nIndex], buffer, buffersize, 0) == SOCKET_ERROR) { // 发送缓冲区已满 if(WSAGetLastError() == WSAEWOULDBLOCK) break; else ErrorHandle... } } } else { ErrorHandle.. break; } }
DWORD WINAPI Connect(LPVOID lpParam) { //////////////////第1步:初始化,创建,连接套接字////////////////// WSADATA WsaData;int err; err = WSAStartup (0x0002, &WsaData);if(err!=0) return 1; //0x0002代表版本2.0 socket_client=socket(AF_INET,SOCK_STREAM,0); if(socket_client==INVALID_SOCKET){AfxMessageBox("创建套接字错误!\n");return 1;} SOCKADDR_IN sconnect_pass; sconnect_pass.sin_family=AF_INET; sconnect_pass.sin_addr.S_un.S_addr=inet_addr("127.0.0.1"); sconnect_pass.sin_port=htons(55551); if (SOCKET_ERROR==connect(socket_client,(SOCKADDR*)&sconnect_pass,sizeof(SOCKADDR))) { AfxMessageBox("连接服务端错误\n"); return 1; } else { //将套接口s置于”非阻塞模式“ u_long u1=1;//0为保持默认的阻塞,非0表示改为非阻塞 ioctlsocket(socket_client,FIONBIO,(u_long*)&u1); //--------------①创建事件对象----------------- WSAEVENT ClientEvent=WSACreateEvent(); if (ClientEvent==WSA_INVALID_EVENT) { #ifdef _DEBUG ::OutputDebugString("创建事件错误!\n"); #endif // _DEBUG AfxMessageBox("WSACreateEvent() Failed,Error=【%d】\n"); return 1; } //--------------②网络事件注册------------ int WESerror=WSAEventSelect(socket_client,ClientEvent,FD_READ|FD_CLOSE); if (WESerror==INVALID_SOCKET) { #ifdef _DEBUG ::OutputDebugString("网络事件注册错误!\n"); #endif // _DEBUG AfxMessageBox("WSAEventSelect() Failed,Error=【%d】\n"); return -1; } //-----------准备工作--------------- //WSAWaitForMultipleEvents只能等待64个事件,若想更多,则创建额外的工作线程 SOCKET sockArray[WSA_MAXIMUM_WAIT_EVENTS]; WSAEVENT eventArray[WSA_MAXIMUM_WAIT_EVENTS]; int nEventCount = 0; sockArray[0]=socket_client; eventArray[nEventCount]=ClientEvent; nEventCount++;//事件个数+1,第1次等待1个事件,注意WSAWaitForMultipleEvents的参数1是动态 int t=1;//超时次数 //------------循环处理------------- while (1) { //---------------⑦等待事件对象-------------- int nIndex=WSAWaitForMultipleEvents(nEventCount,eventArray,FALSE,40000,FALSE);//参数1:注意是动态增减的,不能固定死 .注:参数1与2本质一样,但数值不一样.如果参 数1为1个,那么数组括号内[]为0 //参数3:参数1中的任何一个有消息进来,都立刻停止阻塞,运行下一步操作 AfxMessageBox("响应事件,进入下一步\n");//进来时为0,响应时为对应的数组标签号 if (nIndex==WSA_WAIT_FAILED)//------7.1调用失败--------- { AfxMessageBox("WSAEventSelect调用失败\n"); break;//退出while(1)循环 } else if (nIndex==WSA_WAIT_TIMEOUT)//-------7.2超时--------- { if (t<3) { AfxMessageBox("第【%d】次超时\n"); t++; continue; } else { AfxMessageBox("第【%d】次超时,退出\n"); break; } } //---------------7.3网络事件触发事件对象句柄的工作状态-------- else { WSANETWORKEVENTS event;//该结构记录网络事件和对应出错代码 //---------⑧网络事件查询----------- WSAEnumNetworkEvents(sockArray[nIndex-WSA_WAIT_EVENT_0],NULL,&event); WSAResetEvent(eventArray[nIndex-WSA_WAIT_EVENT_0]); if (event.lNetworkEvents&FD_READ) //-------8.2处理FD_READ通知消息 { if (event.iErrorCode[FD_READ_BIT]==0) { char m_RecvBuffer[4096]; PCMD_HEADER pcm = (PCMD_HEADER)m_RecvBuffer; if(recv(sockArray[nIndex-WSA_WAIT_EVENT_0],(char*)&m_RecvBuffer,sizeof(m_RecvBuffer),0)==SOCKET_ERROR) { AfxMessageBox("接收失败,退出重recv接收!"); break; } else { switch ( pcm->ncmd ) { case CMD_AS_REP_C_MACHINE_LOGIN://很明显这个pcm->ncmd,是登录包中ncmd标识符 { PAREP_C_MACHINE_LOGIN cmd = (PAREP_C_MACHINE_LOGIN)pcm; if (cmd->nStatus==1) { AfxMessageBox("收到登录回复包(Client->Server)状态:成功!"); } else { AfxMessageBox("收到登录回复包(Client->Server)状态:失败!"); } } break; } } } } else if (event.lNetworkEvents&FD_CLOSE) //---------8.3处理FD_CLOSE通知消息 { if (event.iErrorCode[FD_CLOSE_BIT]==0) //客户端正常关闭 { closesocket(sockArray[nIndex-WSA_WAIT_EVENT_0]); WSACloseEvent(eventArray[nIndex-WSA_WAIT_EVENT_0]); AfxMessageBox("套接字已关闭连接\n");//注:会触发7.1调用失败 } else //客户端异常已关闭 { if (event.iErrorCode[FD_CLOSE_BIT]==10053)//右键->转到定义,可以查看到很多错误标识.按需设置(此处仅设置了客户端没有通知服务端,就非法关闭了) { closesocket(sockArray[nIndex-WSA_WAIT_EVENT_0]); WSACloseEvent(eventArray[nIndex-WSA_WAIT_EVENT_0]); AfxMessageBox("服务端非法关闭连接\n");//注:会触发7.1调用失败 } } for (int j=nIndex-WSA_WAIT_EVENT_0;j<nEventCount-1;j++) { sockArray[j]=sockArray[j+1]; eventArray[j]=eventArray[j+1]; } nEventCount--; } }// end 网络事件触发 }//end while ////////////////////////////////////////////////////////////////////////// } AfxMessageBox("服务端已退出.客户端退出中\n"); closesocket(socket_client); WSACleanup(); return 0; } void CMyDlg::OnBnClickedButtonRun() { //发包 C_MACHINE_LOGIN_SYSTEM cmd; strcpy(cmd.sMachineCode,"20100904164702750199");//机器码 CString str; str.Format("%d",cmd.nVersion); if(send(socket_client,(char*)&cmd,sizeof(cmd),0)==SOCKET_ERROR) { #ifdef _DEBUG ::OutputDebugString("发送失败:发送机器码!\n"); #endif // _DEBUG } }
这里就不再进行详细的分析,比照服务端,这里会更简单,需要说明的是,在这里可以使用WSAConnect这个函数来达到连接的目的,不用使用这个东西,当然,如果这样的话,你的发送和接收都要使用WSARecv和 WSASend函数。主要是使用overloapped重叠IO,使用起来更简单明了。