WSAEventSelect模型编程
这个模型是一个简单的异步事件模型,使用起来比较方便,如今说一下其的具体的用法和须要注意的地方。
一,模型的例程(服务端):
先举一个王艳平网络通讯上的例子:ios
-
- #include "initsock.h"
- #include <stdio.h>
- #include <iostream.h>
- #include <windows.h>
-
- 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);
-
- 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
- {
-
- WSANETWORKEVENTS event;
- ::WSAEnumNetworkEvents(sockArray[i], eventArray[i], &event);
- if(event.lNetworkEvents & 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)
- {
- 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)
- {
- 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)
- {
- }
- }
- }
- }
- return 0;
- }
2、例程的分析
一、事件的建立和绑定
前面的一些设置咱们略过,从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++;
将事件绑定到监听的套接字上,这里咱们只对这个套接字的接收和关闭两个消息有兴趣,因此只监听这两个消息,那别的读写啥的呢,不要急,慢慢向下看。eventArray和sockArray,定义的是WSA_MAXIMUM_WAIT_EVENTS大小,而在头文件中#define WSA_MAXIMUM_WAIT_EVENTS (MAXIMUM_WAIT_OBJECTS),
后者被定义成64,这也是须要注意的一点,这个模型单线程只能处理最多64个事件,再多就只能用多线程了,不过,这里重点说明一下,这个模型即便你使用多线程,
最多也只能处理1200个左右的处理量(正常状况),不然,会形成整个程序的性能降低,至于怎么降低,还真没有真正的测试,只是从书上和资料上看是这么讲的。
接着原来,程序而后进入了死循环,在这个循环里,由于是简单的使用嘛,因此不少的异常并无进行控制,可是为了说明用法,就得简单一些不是么?
二、事件的监听和控制处理
2.1 事件的监听
- int nIndex = ::WSAWaitForMultipleEvents(nEventTotal, eventArray, FALSE, WSA_INFINITE, FALSE);
- nIndex = nIndex - WSA_WAIT_EVENT_0;
先说这个索引为何要减去WSA_WAIT_EVENT_0这个值,由于事件的起始值在内核中是进行定义了的,不过,在这里这个东西最终定义仍然是0。而后咱们看这个函数
::WSAWaitForMultipleEvents(nEventTotal, eventArray, FALSE, WSA_INFINITE, FALSE),
这个函数用来监听多个事件(就是上面咱们绑定的事件)的状态,有状态或者是事件被触发,就会返回,不然会按照你设置的参数进行操做。
前面两个参数,第一个是监听的数量,最小是一,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函数,windows
一个用来处理监听多个事件数组,一个用来遍历每一个数组事件,数组
防止出现丢失响应的现象,因此其参数的设置是不一样的,必定要引发注意。服务器
2.2事件的处理网络
而后戏又来了,上面说的读写监听呢,就在这里出现了,包括上面埋伏下的一个雷,也在这里处理了:多线程
首先调用::WSAEnumNetworkEvents(sockArray[i], eventArray[i], &event),把上面的雷给拆了,并发
::WSAEnumNetworkEvents会自动重置事件,app
而后获得事件的索引或者说ID,框架
- if(event.lNetworkEvents & 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)
- {
- 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)
- {
- 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)
- {
- }
如此往复,不就达到了不断接收链接和处理数据的问题么。
这里还重复一下,网上不少程序都没有处理多个事件同时受信的状况,在网上和各类资料中,也有的只使用一个::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)
- {
-
- 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)
- {
-
- WSADATA WsaData;int err;
- err = WSAStartup (0x0002, &WsaData);if(err!=0) return 1;
- 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
- {
-
- u_long u1=1;
- 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;
- }
-
-
- SOCKET sockArray[WSA_MAXIMUM_WAIT_EVENTS]; WSAEVENT eventArray[WSA_MAXIMUM_WAIT_EVENTS];
- int nEventCount = 0;
- sockArray[0]=socket_client; eventArray[nEventCount]=ClientEvent;
- nEventCount++;
- int t=1;
-
- while (1)
- {
-
- int nIndex=WSAWaitForMultipleEvents(nEventCount,eventArray,FALSE,40000,FALSE);
-
- 数1为1个,那么数组括号内[]为0
-
- AfxMessageBox("响应事件,进入下一步\n");
- if (nIndex==WSA_WAIT_FAILED)
- {
- AfxMessageBox("WSAEventSelect调用失败\n");
- break;
- }
- else if (nIndex==WSA_WAIT_TIMEOUT)
- {
- if (t<3)
- {
- AfxMessageBox("第【%d】次超时\n");
- t++;
- continue;
- }
- else
- {
- AfxMessageBox("第【%d】次超时,退出\n");
- break;
- }
- }
-
- else
- {
- WSANETWORKEVENTS event;
-
- WSAEnumNetworkEvents(sockArray[nIndex-WSA_WAIT_EVENT_0],NULL,&event);
- WSAResetEvent(eventArray[nIndex-WSA_WAIT_EVENT_0]);
- if (event.lNetworkEvents&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:
- {
- 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)
- {
- if (event.iErrorCode[FD_CLOSE_BIT]==0)
- {
- closesocket(sockArray[nIndex-WSA_WAIT_EVENT_0]);
- WSACloseEvent(eventArray[nIndex-WSA_WAIT_EVENT_0]);
- AfxMessageBox("套接字已关闭链接\n");
- }
- else
- {
- if (event.iErrorCode[FD_CLOSE_BIT]==10053)
- {
- closesocket(sockArray[nIndex-WSA_WAIT_EVENT_0]);
- WSACloseEvent(eventArray[nIndex-WSA_WAIT_EVENT_0]);
- AfxMessageBox("服务端非法关闭链接\n");
- }
- }
- for (int j=nIndex-WSA_WAIT_EVENT_0;j<nEventCount-1;j++)
- {
- sockArray[j]=sockArray[j+1];
- eventArray[j]=eventArray[j+1];
- }
- nEventCount--;
- }
- }
- }
-
- }
- 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,使用起来更简单明了。