详细解析WSAEventSelect模型
一,模型的例程(服務(wù)端):
先舉一個王艷平網(wǎng)絡(luò)通信上的例子:
二、例程的分析
1、事件的創(chuàng)建和綁定
前面的一些設(shè)置我們略過,從WSAEVENT 開始說起,跟蹤發(fā)現(xiàn)在winsock2.h中有如下定義:
#define WSAEVENT??????????????? HANDLE
這個事件說明是一個句柄,我們知道在事件中有兩種狀態(tài),一種是手動處理事件,一種是自動的,這里使用WSACreateEvent()這個函數(shù)創(chuàng)建返回的事件句柄,正常的返回的情況下,其創(chuàng)建的是一個手工處理的句柄,否則,其返回WSA_INVALID_EVENT,表明創(chuàng)建未成功,如果需要知道更多的信息WSAGetLastError()這個函數(shù)來得到具體的信息出錯代碼。這里埋伏下了一個雷,為什么創(chuàng)建的是手工處理的事件(manually reset ),那后面為什么沒有WSAResetEvent()這個函數(shù)來處理事件,先記下。
然后接著講,
::WSAEventSelect(sListen, event, FD_ACCEPT|FD_CLOSE); // 添加到表中 eventArray[nEventTotal] = event; sockArray[nEventTotal] = sListen; nEventTotal++; 將事件綁定到監(jiān)聽的套接字上,這里我們只對這個套接字的接收和關(guān)閉兩個消息有興趣,所以只監(jiān)聽這兩個消息,那別的讀寫啥的呢,不要急,慢慢向下看。eventArray和sockArray,定義的是WSA_MAXIMUM_WAIT_EVENTS大小,而在頭文件中#define WSA_MAXIMUM_WAIT_EVENTS (MAXIMUM_WAIT_OBJECTS),后者被定義成64,這也是需要注意的一點,這個模型單線程只能處理最多64個事件,再多就只能用多線程了,不過,這里重點說明一下,這個模型即使你使用多線程,最多也只能處理1200個左右的處理量(正常情況),否則,會造成整個程序的性能下降,至于怎么下降,還真沒有真正的測試,只是從書上和資料上看是這么講的。
接著原來,程序然后進(jìn)入了死循環(huán),在這個循環(huán)里,因為是簡單的使用嘛,所以很多的異常并沒有進(jìn)行控制,但是為了說明用法,就得簡單一些不是么?
2、事件的監(jiān)聽和控制處理
2.1 事件的監(jiān)聽 int nIndex = ::WSAWaitForMultipleEvents(nEventTotal, eventArray, FALSE, WSA_INFINITE, FALSE); nIndex = nIndex - WSA_WAIT_EVENT_0; 先說這個索引為什么要減去WSA_WAIT_EVENT_0這個值,因為事件的起始值在內(nèi)核中是進(jìn)行定義了的,不過,在這里這個東西最終定義仍然是0。然后我們看這個函數(shù)
::WSAWaitForMultipleEvents(nEventTotal, eventArray, FALSE, WSA_INFINITE, FALSE),
這個函數(shù)用來監(jiān)聽多個事件(就是上面我們綁定的事件)的狀態(tài),有狀態(tài)或者是事件被觸發(fā),就會返回,否則會按照你設(shè)置的參數(shù)進(jìn)行操作。
前面兩個參數(shù),第一個是監(jiān)聽的數(shù)量,最小是一,MSDN上有,第二是一個事件的數(shù)組,第三個是精彩的去處,如果設(shè)置成TRUE,那么只有這第二個事件數(shù)組中的所有的事件都受信或者說觸發(fā),才會動作,如果是FALSE呢,則只要有一個就可以動作。第五個是超時設(shè)置,可以是0,是WSA_INFINITE,也可以是其它的數(shù)值,這里有一個問題,如果設(shè)置為0會造成程序的CPU占用率過高,WSA_INFINITE則可能會出現(xiàn)在等待數(shù)量為一個字時,且第三個參數(shù)設(shè)置為TRUE,產(chǎn)生死套接字的長期阻塞。所以還是設(shè)置成一個經(jīng)驗值為好,至于這個經(jīng)驗值是多少,看你的程序的具體的應(yīng)用了。
其實這個函數(shù)本質(zhì)還是調(diào)用WaitForMulipleObjectsEx這個函數(shù),MSDN上講WSAEventSelect模型在等待時不占用CPU時間,就是這個原因,所以其比阻塞的SOCKET通信要效率高很多,其實那個消息的模型WSAAsycSelect和這個事件的模型也差不多,異曲同工之妙吧。不過適用范圍是有區(qū)別的,這個可以用在WINCE上。消息則不行。
這里就又引出一個注意點,在這個模型里,如果同時有幾個事件受信,或者說觸發(fā),那么nIndex = ::WSAWaitForMultipleEvents()只返回最前面的一個事件,那么怎么解決其后面的呢,書上有曰:多次循環(huán)調(diào)用這個就可以了,所以才會引出下面的再次在for循環(huán)里調(diào)用
nIndex = ::WSAWaitForMultipleEvents(1, &eventArray[i], TRUE, 1000, FALSE);
注意這里參數(shù)的變化,數(shù)量為1,事件為[i],但事件會不斷的增長,全面受信改成了TRUE,超時為1000,最后的這個參數(shù)在這里只能設(shè)置成FALSE,具體為什么查MSDN去。
如果這里我們處理的不好,如果把1000改成無限等待的話,就可以出現(xiàn)上面說的死套接字的無限阻塞,也就是說如果一個套接字死掉了,你沒有在事件隊伍里刪除他,那么他就會一直在這兒阻塞,即使后面有事件也無法得到響應(yīng),但是,如果你的套接字只有一個連接的話,就沒有什么了,可以改成無限等待。不過,最好還是別這樣,因為如果你處理一個失誤,就會產(chǎn)生死的套接字(比如重連,但你沒有刪除先前無用的套接字)。
用兩個::WSAWaitForMultipleEvents函數(shù),
一個用來處理監(jiān)聽多個事件數(shù)組,一個用來遍歷每個數(shù)組事件,
防止出現(xiàn)丟失響應(yīng)的現(xiàn)象,所以其參數(shù)的設(shè)置是不同的,一定要引起注意。
2.2事件的處理
然后戲又來了,上面說的讀寫監(jiān)聽呢,就在這里出現(xiàn)了,包括上面埋伏下的一個雷,也在這里處理了:
首先調(diào)用::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++; } } 代碼里重新調(diào)用了事件創(chuàng)建和事件綁定函數(shù),并且將兩個數(shù)組自動增大,最最重要的是我們終于看到了,FD_READ|FD_CLOSE|FD_WRITE,
明白了吧,這個簡單的程序的本質(zhì)其實是將 讀 寫 和 接收關(guān)閉 的套接字混合到了一起,
而在后面的服務(wù)器例程里,我們發(fā)現(xiàn),這個已經(jīng)拆開,并且重新手動設(shè)置受信的事件,調(diào)用了::ResetEvent(event)。這樣不就完美的拆除了上面的雷么。
2.3 其它處理方法
當(dāng)程序繼續(xù)循環(huán)到最外層時,::WSAWaitForMultipleEvents無限等待所有的事件,只要有一個事件響應(yīng),就會進(jìn)入到下一層循環(huán),如果是接收就重復(fù)上述的動作,如果是讀寫就進(jìn)入:
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("接收到數(shù)據(jù):%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通知消息 { }
如此往復(fù),不就達(dá)到了不斷接收連接和處理數(shù)據(jù)的問題么。
這里還重復(fù)一下,網(wǎng)上很多程序都沒有處理多個事件同時受信的情況,在網(wǎng)上和各種資料中,也有的只使用一個::WSAWaitForMultipleEvents函數(shù),但參數(shù)的設(shè)置得重新來過,而且得小心的處理各種的事件和異常的發(fā)生。可能在小并發(fā)量和小數(shù)據(jù)量時沒有問題,但并發(fā)一多數(shù)據(jù)一大,可能會出現(xiàn)丟數(shù)據(jù)的問題,沒有做過測試,但可能是很大的。否則不會說遍歷調(diào)用這個函數(shù)了。
2.4 FD_WRITE 事件的觸發(fā)
這里得羅嗦兩句FD_WRITE 事件的觸發(fā),前面的都好理解,主要是啥時候兒會觸發(fā)這個事件呢,我們在一開始只對接收和關(guān)閉進(jìn)行了監(jiān)聽,為什么沒有這個FD_WRITE事件的
監(jiān)聽呢,
這就引出了下面的東東:(從一個網(wǎng)友那轉(zhuǎn)來)
下面是MSDN中對FD_WRITE觸發(fā)機(jī)制的解釋:
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事件只有在以下三種情況下才會觸發(fā)
①client 通過connect(WSAConnect)首次和server建立連接時,在client端會觸發(fā)FD_WRITE事件
②server通過accept(WSAAccept)接受client連接請求時,在server端會觸發(fā)FD_WRITE事件
③send(WSASend)/sendto(WSASendTo)發(fā)送失敗返回WSAEWOULDBLOCK,并且當(dāng)緩沖區(qū)有可用空間時,則會觸發(fā)FD_WRITE事件
①②其實是同一種情況,在第一次建立連接時,C/S端都會觸發(fā)一個FD_WRITE事件。
主要是③這種情況:send出去的數(shù)據(jù)其實都先存在winsock的發(fā)送緩沖區(qū)中,然后才發(fā)送出去,如果緩沖區(qū)滿了,那么再調(diào)用send(WSASend,sendto,WSASendTo)的話,就會返回一個 WSAEWOULDBLOCK的錯誤碼,接下來隨著發(fā)送緩沖區(qū)中的數(shù)據(jù)被發(fā)送出去,緩沖區(qū)中出現(xiàn)可用空間時,一個 FD_WRITE 事件才會被觸發(fā),這里比較容易混淆的是 FD_WRITE 觸發(fā)的前提是 緩沖區(qū)要先被充滿然后隨著數(shù)據(jù)的發(fā)送又出現(xiàn)可用空間,而不是緩沖區(qū)中有可用空間,也就是說像如下的調(diào)用方式可能出現(xiàn)問題
else if(event.lNetworkEvents & FD_WRITE) { if(event.iErrorCode[FD_WRITE_BIT] == 0) { send(g_sockArray[nIndex], buffer, buffersize); .... } else { } }
問題在于建立連接后 FD_WRITE 第一次被觸發(fā), 如果send發(fā)送的數(shù)據(jù)不足以充滿緩沖區(qū),雖然緩沖區(qū)中仍有空閑空間,但是 FD_WRITE 不會再被觸發(fā),程序永遠(yuǎn)也等不到可以發(fā)送的網(wǎng)絡(luò)事件。
基于以上原因,在收到FD_WRITE事件時,程序就用循環(huán)或線程不停的send數(shù)據(jù),直至send返回WSAEWOULDBLOCK,表明緩沖區(qū)已滿,再退出循環(huán)或線程。
當(dāng)緩沖區(qū)中又有新的空閑空間時,FD_WRITE 事件又被觸發(fā),程序被通知后又可發(fā)送數(shù)據(jù)了。
上面代碼片段中省略的對 FD_WRITE 事件處理
else if(event.lNetworkEvents & FD_WRITE) { if(event.iErrorCode[FD_WRITE_BIT] == 0) { while(TRUE) { // 得到要發(fā)送的buffer,可以是用戶的輸入,從文件中讀取等 GetBuffer.... if(send(g_sockArray[nIndex], buffer, buffersize, 0) == SOCKET_ERROR) { // 發(fā)送緩沖區(qū)已滿 if(WSAGetLastError() == WSAEWOULDBLOCK) break; else ErrorHandle... } } } else { ErrorHandle.. break; } } 如果你不是大數(shù)據(jù)量的不斷的發(fā)送數(shù)據(jù),建議你忽略這個事件,畢竟緩沖區(qū)不是很容易被弄滿的,結(jié)果就是你的發(fā)送事件無法完成。
2.5異常的處理
主要是0個連接時,處理CPU的占用率的問題,以及在多于64個事件時的監(jiān)聽處理問題。而且包括上面講的,沒有雙循環(huán)時的多事件同時受信的問題。
2.6 多線程服務(wù)端
這個大家可以看王艷平的書,說得很清楚,需要注意的是在他的主服務(wù)程序里,使用的是int nRet = ::WaitForSingleObject(event, 5*1000); 所以下面要手動的重新對事件進(jìn)行設(shè)置,否則這個事件就再無法監(jiān)聽得到了。
其它的難度主要是面向?qū)ο蟮脑O(shè)計封裝要弄明白,如果這個弄明白知道封裝SOCKET和THREAD結(jié)構(gòu)體的目的是什么,再照著書上看就不會有錯了, 但提醒一點,線程結(jié)構(gòu)體中的第一個事件是重建事件,不要和其它的監(jiān)聽事件弄混了。
如果做一個介于書上兩種代碼間的小框架,可以用一個線程來監(jiān)聽ACCEPT和CLOSE事件,另外的線程監(jiān)聽小于64個的讀寫等事件,一般的小的SOCKET通信應(yīng)該就沒有什么問題了。重要的是你要把這個服務(wù)端封裝好,有時間做一下。
三、例程(客戶端)
先上一段代碼: DWORD WINAPI Connect(LPVOID lpParam) { //第1步:初始化,創(chuàng)建,連接套接字// 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("創(chuàng)建套接字錯誤!\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("連接服務(wù)端錯誤\n"); return 1; } else { //將套接口s置于”非阻塞模式“ u_long u1=1;//0為保持默認(rèn)的阻塞,非0表示改為非阻塞 ioctlsocket(socket_client,FIONBIO,(u_long*)&u1); //--------------①創(chuàng)建事件對象----------------- WSAEVENT ClientEvent=WSACreateEvent(); if (ClientEvent==WSA_INVALID_EVENT) { #ifdef _DEBUG ::OutputDebugString("創(chuàng)建事件錯誤!\n"); #endif // _DEBUG AfxMessageBox("WSACreateEvent() Failed,Error=【%d】\n"); return 1; } //--------------②網(wǎng)絡(luò)事件注冊------------ int WESerror=WSAEventSelect(socket_client,ClientEvent,FD_READ|FD_CLOSE); if (WESerror==INVALID_SOCKET) { #ifdef _DEBUG ::OutputDebugString("網(wǎng)絡(luò)事件注冊錯誤!\n"); #endif // _DEBUG AfxMessageBox("WSAEventSelect() Failed,Error=【%d】\n"); return -1; } //-----------準(zhǔn)備工作--------------- //WSAWaitForMultipleEvents只能等待64個事件,若想更多,則創(chuàng)建額外的工作線程 SOCKET sockArray[WSA_MAXIMUM_WAIT_EVENTS]; WSAEVENT eventArray[WSA_MAXIMUM_WAIT_EVENTS]; int nEventCount = 0; sockArray[0]=socket_client; eventArray[nEventCount]=ClientEvent; nEventCount++;//事件個數(shù)+1,第1次等待1個事件,注意WSAWaitForMultipleEvents的參數(shù)1是動態(tài) int t=1;//超時次數(shù) //------------循環(huán)處理------------- while (1) { //---------------⑦等待事件對象-------------- int nIndex=WSAWaitForMultipleEvents(nEventCount,eventArray,FALSE,40000,FALSE);//參數(shù)1:注意是動態(tài)增減的,不能固定死 .注:參數(shù)1與2本質(zhì)一樣,但數(shù)值不一樣.如果參 數(shù)1為1個,那么數(shù)組括號內(nèi)[]為0 //參數(shù)3:參數(shù)1中的任何一個有消息進(jìn)來,都立刻停止阻塞,運(yùn)行下一步操作 AfxMessageBox("響應(yīng)事件,進(jìn)入下一步\n");//進(jìn)來時為0,響應(yīng)時為對應(yīng)的數(shù)組標(biāo)簽號 if (nIndex==WSA_WAIT_FAILED)//------7.1調(diào)用失敗--------- { AfxMessageBox("WSAEventSelect調(diào)用失敗\n"); break;//退出while(1)循環(huán) } else if (nIndex==WSA_WAIT_TIMEOUT)//-------7.2超時--------- { if (t<3) { AfxMessageBox("第【%d】次超時\n"); t++; continue; } else { AfxMessageBox("第【%d】次超時,退出\n"); break; } } //---------------7.3網(wǎng)絡(luò)事件觸發(fā)事件對象句柄的工作狀態(tài)-------- else { WSANETWORKEVENTS event;//該結(jié)構(gòu)記錄網(wǎng)絡(luò)事件和對應(yīng)出錯代碼 //---------⑧網(wǎng)絡(luò)事件查詢----------- 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標(biāo)識符 { PAREP_C_MACHINE_LOGIN cmd = (PAREP_C_MACHINE_LOGIN)pcm; if (cmd->nStatus==1) { AfxMessageBox("收到登錄回復(fù)包(Client->Server)狀態(tài):成功!"); } else { AfxMessageBox("收到登錄回復(fù)包(Client->Server)狀態(tài):失敗!"); } } break; } } } } else if (event.lNetworkEvents&FD_CLOSE) //---------8.3處理FD_CLOSE通知消息 { if (event.iErrorCode[FD_CLOSE_BIT]==0) //客戶端正常關(guān)閉 { closesocket(sockArray[nIndex-WSA_WAIT_EVENT_0]); WSACloseEvent(eventArray[nIndex-WSA_WAIT_EVENT_0]); AfxMessageBox("套接字已關(guān)閉連接\n");//注:會觸發(fā)7.1調(diào)用失敗 } else //客戶端異常已關(guān)閉 { if (event.iErrorCode[FD_CLOSE_BIT]==10053)//右鍵->轉(zhuǎn)到定義,可以查看到很多錯誤標(biāo)識.按需設(shè)置(此處僅設(shè)置了客戶端沒有通知服務(wù)端,就非法關(guān)閉了) { closesocket(sockArray[nIndex-WSA_WAIT_EVENT_0]); WSACloseEvent(eventArray[nIndex-WSA_WAIT_EVENT_0]); AfxMessageBox("服務(wù)端非法關(guān)閉連接\n");//注:會觸發(fā)7.1調(diào)用失敗 } } for (int j=nIndex-WSA_WAIT_EVENT_0;j<nEventCount-1;j++) { sockArray[j]=sockArray[j+1]; eventArray[j]=eventArray[j+1]; } nEventCount--; } }// end 網(wǎng)絡(luò)事件觸發(fā) }//end while // } AfxMessageBox("服務(wù)端已退出.客戶端退出中\(zhòng)n"); closesocket(socket_client); WSACleanup(); return 0; } void CMyDlg::OnBnClickedButtonRun() { //發(fā)包 C_MACHINE_LOGIN_SYSTEM cmd; strcpy(cmd.sMachineCode,"20100904164702750199");//機(jī)器碼 CString str; str.Format("%d",cmd.nVersion); if(send(socket_client,(char*)&cmd,sizeof(cmd),0)==SOCKET_ERROR) { #ifdef _DEBUG ::OutputDebugString("發(fā)送失敗:發(fā)送機(jī)器碼!\n"); #endif // _DEBUG } }
這里就不再進(jìn)行詳細(xì)的分析,比照服務(wù)端,這里會更簡單,需要說明的是,在這里可以使用WSAConnect這個函數(shù)來達(dá)到連接的目的,不用使用這個東西,當(dāng)然,如果這樣的話,你的發(fā)送和接收都要使用WSARecv和 WSASend函數(shù)。主要是使用overloapped重疊IO,使用起來更簡單明了。
超強(qiáng)干貨來襲 云風(fēng)專訪:近40年碼齡,通宵達(dá)旦的技術(shù)人生總結(jié)
以上是生活随笔為你收集整理的详细解析WSAEventSelect模型的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Windows Socket五种I/O模
- 下一篇: sockaddr与sockaddr_in