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,使用起來更簡單明瞭。