详细解析WSAEventSelect模型
 一,模型的例程(服務端):
 先舉一個王艷平網絡通信上的例子:
 二、例程的分析
 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++; 將事件綁定到監聽的套接字上,這里我們只對這個套接字的接收和關閉兩個消息有興趣,所以只監聽這兩個消息,那別的讀寫啥的呢,不要急,慢慢向下看。eventArray和sockArray,定義的是WSA_MAXIMUM_WAIT_EVENTS大小,而在頭文件中#define WSA_MAXIMUM_WAIT_EVENTS (MAXIMUM_WAIT_OBJECTS),后者被定義成64,這也是需要注意的一點,這個模型單線程只能處理最多64個事件,再多就只能用多線程了,不過,這里重點說明一下,這個模型即使你使用多線程,最多也只能處理1200個左右的處理量(正常情況),否則,會造成整個程序的性能下降,至于怎么下降,還真沒有真正的測試,只是從書上和資料上看是這么講的。
接著原來,程序然后進入了死循環,在這個循環里,因為是簡單的使用嘛,所以很多的異常并沒有進行控制,但是為了說明用法,就得簡單一些不是么?
2、事件的監聽和控制處理
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函數,
一個用來處理監聽多個事件數組,一個用來遍歷每個數組事件,
 防止出現丟失響應的現象,所以其參數的設置是不同的,一定要引起注意。
 
 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; } } 如果你不是大數據量的不斷的發送數據,建議你忽略這個事件,畢竟緩沖區不是很容易被弄滿的,結果就是你的發送事件無法完成。
2.5異常的處理
主要是0個連接時,處理CPU的占用率的問題,以及在多于64個事件時的監聽處理問題。而且包括上面講的,沒有雙循環時的多事件同時受信的問題。
2.6 多線程服務端
這個大家可以看王艷平的書,說得很清楚,需要注意的是在他的主服務程序里,使用的是int nRet = ::WaitForSingleObject(event, 5*1000); 所以下面要手動的重新對事件進行設置,否則這個事件就再無法監聽得到了。
其它的難度主要是面向對象的設計封裝要弄明白,如果這個弄明白知道封裝SOCKET和THREAD結構體的目的是什么,再照著書上看就不會有錯了, 但提醒一點,線程結構體中的第一個事件是重建事件,不要和其它的監聽事件弄混了。
如果做一個介于書上兩種代碼間的小框架,可以用一個線程來監聽ACCEPT和CLOSE事件,另外的線程監聽小于64個的讀寫等事件,一般的小的SOCKET通信應該就沒有什么問題了。重要的是你要把這個服務端封裝好,有時間做一下。
三、例程(客戶端)
先上一段代碼: 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,使用起來更簡單明了。
超強干貨來襲 云風專訪:近40年碼齡,通宵達旦的技術人生總結
以上是生活随笔為你收集整理的详细解析WSAEventSelect模型的全部內容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: Windows Socket五种I/O模
- 下一篇: sockaddr与sockaddr_in
