《Windows網路與通訊程式設計》讀書筆記----WSAEventSelect模型

來源:互聯網
上載者:User

WSAEventSelect模型

Winsock提供另一種有用的非同步事件通知I/O模型――WSAEventSelect模型。這個模型與WSAAsyncSelect模型類似,允許應用程式在一個或者多個通訊端上接收基於事件網路通知。它與WSAAsyncSelect模型類似是因為它也接收FD_XXX類型的網路事件,不過並不是依靠Windows的訊息驅動機制,而是經由事件物件控點通知。

使用這個模型的基本思路是為感興趣的一組網路事件建立一個事件對象,再調用WSAEventSelect函數將網路事件和事件對象關聯起來。當網路事件發生時,Winsock使相應的事對象觸發,在事件對象上的等待函數就會返回。之後調用WSAEnumNetworkEvents函數便可擷取到底發生了什麼網路事件

 

具體編程流程:

1、建立一個事件控制代碼表和一個對應的通訊端控制代碼表

2、每建立一個通訊端,就建立一個事件對象,把它們的控制代碼分別放入上面的兩個表中,並調用WSAEventSelect添加它們的關聯。

3、調用WSAWaitForMultipleEvents在所有事件對象上等待,此函數返回後,我們對事件控制代碼表中每個事件調用       WSAWaitForMultipleEvents函數,以便確認在哪些通訊端上發生了網路事件。

4、處理髮生的網路事件,繼續在事件對象上等待。


書上原始碼的實現流程:

1、聲明兩個結構體,其中一個為通訊端對象。typedefstruct _SOCKET_OBJ{SOCKET s ;//通訊端,調用WSAEventSelect與相應事件對象就是用這個通訊端HANDLE event ;//與通訊端相關的事件控制代碼,,調用WSAEventSelect與相應通訊端關聯就是用這個事件對象了sockaddr_in addrRemote ;//用戶端地址_SOCKET_OBJ *pNext ;//下一個地址,通訊端對象用鏈表串連起來}SOCKET_OBJ,*PSOCKET_OBJ ;


因為WSAWaitForMultipleEvents最多支援WSA_MAXIMUM_WAIT_EVENTS個對象,而WSA_MAXIMUM_WAIT_EVENTS被定義為64。因此這個I/O模型在一個線程中同一時間最多能支援64個通訊端,如果需要使用這個模型管理更多通訊端,就需要建立額外的背景工作執行緒,WSAWaitForMultipleEvents更多的對象了。或者不用建立線程的方法,而用另外的資料結構儲存要等待的對象,設定一個逾時,分批輪詢等待。

所以需要聲明另外一個結構體,線程對象,用於背景工作執行緒管理對應的通訊端對象。

//線程對象,每一個線程負責管理最多63串連,其中一個事件對象用於指示重建操作typedefstruct _THREAD_OBJ{HANDLE events[WSA_MAXIMUM_WAIT_EVENTS] ; //記錄當前線程要等待的事件對象的控制代碼,事件對象表int nSocketCount ; //記錄當前線程處理的通訊端的數量,最多為63個PSOCKET_OBJ pSockHeader ; //當前線程處理的通訊端對象的列表,pSockHeader指向表頭PSOCKET_OBJ pSockTail ; //pSockTail指向表尾CRITICAL_SECTION cs ; //關鍵程式碼片段變數,為的是同步對本結構的訪問。有兩個線程會對線程對象進行訪問。一個是監聽線程,需要分配新連 //接的通訊端對象給線程對象。一個是對應於該線程對象的背景工作執行緒,管理所擁有的通訊端對象_THREAD_OBJ *pNext ; //指向下一個THREAD_OBJ對象,為的是連成一個表}  THREAD_OBJ ,*PTHREAD_OBJ ;

線程

主線程

主線程負責監聽新串連的到來,關聯新通訊端與新的事件對象,分配新的通訊端對象到現存或者建立的線程對象當中,並每隔一段時間,列印“已經接受過的串連”和“當前串連數”。

 

背景工作執行緒:

背景工作執行緒負責等待自己所屬線程對象擁有的通訊端對象。

執行重建操作或者分別處理可讀、可寫、關閉通訊端的事件。

另外一點需要說明的是,線程對象中的event[0]對象,是用於指示線程是否需要重建通訊端對象與事件對象的映射關係。因為通訊端對象線上程對象中是以鏈表形式儲存的,而事件對象則是以數組形式儲存的。當有中間的通訊端對象釋放時,事件對象數組的索引與鏈表中第幾個通訊端對象就不是一一對應的關係了,所以需要移動事件對象數組中事件對象。

線程對象、線程對象中的通訊端鏈表和事件對象數組映射關係。

 

Bug

另外,書上的代碼有一處地方是有Bug的。就是HandleIO函數中關於各種事件的判斷。FD_CLOSE和FD_READ事件事實上是可以同時發生的,也就是用戶端將資料和FIN標誌一起發送的情況。而書上的代碼則是將這兩個事件分開了。所以當出現“用戶端將資料和FIN標誌一起發送”的情況發生了,伺服器並不知道,就會一直持有已經關閉的通訊端對象不釋放,造成一定量記憶體浪費。

後話:

另外一點就是,雖然網上有源碼,但是感覺還是自己敲出來比較實際點。雖然花了點時間,但是對自己理解這個模型很有協助,也懂得了一點設計思路。

原始碼:

#define _WIN32_WINNT 0x0400   #include<windows.h>#include<cstdio>#include"InitSocket.h"CInitSock initSock ; //進入main函數前已經進行了初始化//通訊端對象結構,s和event成員必須放在最開始的位置typedefstruct _SOCKET_OBJ{SOCKET s ;//通訊端HANDLE event ;//與通訊端相關的事件控制代碼sockaddr_in addrRemote ;//用戶端地址_SOCKET_OBJ *pNext ;//下一個地址}SOCKET_OBJ,*PSOCKET_OBJ ;//線程對象,每一個線程負責管理最多64串連typedefstruct _THREAD_OBJ{HANDLE events[WSA_MAXIMUM_WAIT_EVENTS] ; //記錄當前線程要等待的事件對象的控制代碼,事件對象表int nSocketCount ; //記錄當前線程處理的通訊端的數量PSOCKET_OBJ pSockHeader ; //當前線程處理的通訊端對象的列表,pSockHeader指向表頭PSOCKET_OBJ pSockTail ; //pSockTail指向表尾CRITICAL_SECTION cs ; //關鍵程式碼片段變數,為的是同步對本結構的訪問_THREAD_OBJ *pNext ; //指向下一個THREAD_OBJ對象,為的是連成一個表}  THREAD_OBJ ,*PTHREAD_OBJ ;//通訊端對象處理函數PSOCKET_OBJ GetSocketObj(SOCKET s) ; //申請一個通訊端對象,初始化它的成員void FreeSocketObj(PSOCKET_OBJ pSocket) ; //釋放一個通訊端對象//線程對象處理函數PTHREAD_OBJ GetThreadObj() ;void FreeThreadObj(PTHREAD_OBJ pThread)  ;//重建立立線程對象的events數組void RebuildArray(PTHREAD_OBJ pThread) ;//向一個線程的通訊端列表中插入一個通訊端BOOL InsertSocketObj(PTHREAD_OBJ pThead,PSOCKET_OBJ pSocket) ;//將一個通訊端對象安排給閒置線程處理void AssignToFreeThread(PSOCKET_OBJ pSocket) ;//從給定線程的通訊端對象列表中移除一個通訊端對象void RemoveSocketObj(PTHREAD_OBJ pThread,PSOCKET_OBJ pSocket) ;//背景工作執行緒負責處理客戶的I/O的請求DWORD WINAPI SeverThread(LPVOID lpParam)  ; //處理真正的I/OBOOL HandleIO(PTHREAD_OBJ pThread,PSOCKET_OBJ pSocket) ;//FindSocketObj函數根據事件對象在events數組中的索引尋找相應的通訊端對象PSOCKET_OBJ FindSocketObj(PTHREAD_OBJ pThread,int nIndex) ;//全域變數PTHREAD_OBJ g_pThreadList ; //指向線程對象列表表頭CRITICAL_SECTION g_cs ;//同步對全域變數的訪問//主線程維護的LONG g_nTotalConnections ; //總共串連數量,也就是處理過的,包括已經斷開的LONG g_nCurrentConnections ; //當前串連數量//主函數int main(void){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_addr = INADDR_ANY ; //所有介面地址if(SOCKET_ERROR == bind(sListen,(sockaddr*)&sin,sizeof(sin))){printf("Failed bind()\n") ;return -1 ;}listen(sListen,200) ; //建立監聽事件對象,並關聯到監聽的通訊端WSAEVENT event = WSACreateEvent() ;WSAEventSelect(sListen,event,FD_ACCEPT|FD_CLOSE) ; //事件選擇監聽通訊端,選擇串連和退出事件InitializeCriticalSection(&g_cs) ;//處理客戶串連請求,列印狀態資訊while(TRUE){int nRet = WaitForSingleObject(event,5*1000) ;if(nRet == WAIT_FAILED){printf("Failed WaitForSingleObject() \n") ;break ;}else if(nRet == WSA_WAIT_TIMEOUT) //定時顯示狀態資訊{printf("\n") ;printf("Total Connections : %d\n",g_nTotalConnections) ;printf("Current Connections : %d\n",g_nCurrentConnections) ;}else//有串連事件發生,監聽事件被觸發{ResetEvent(event) ;//迴圈處理所有未決的串連while(TRUE){sockaddr_in si ;int nLen = sizeof(si) ;SOCKET sNew = accept(sListen,(sockaddr *)&si,&nLen) ; //因為是已經經過了事件選擇,所以是立即返回的 if(SOCKET_ERROR == sNew){break ;}PSOCKET_OBJ pSocket = GetSocketObj(sNew) ;pSocket->addrRemote = si ;WSAEventSelect(pSocket->s,pSocket->event,FD_READ|FD_CLOSE|FD_WRITE) ;//添加新的通訊端對象和相應的事件AssignToFreeThread(pSocket) ;}}}DeleteCriticalSection(&g_cs) ;return 0 ;}//申請一個通訊端對象,初始化它的成員PSOCKET_OBJ GetSocketObj(SOCKET s) {PSOCKET_OBJ pSocket = (PSOCKET_OBJ)GlobalAlloc(GPTR,sizeof(SOCKET_OBJ)) ; //不區分全域與局部堆,推薦使用HeapAllocif(pSocket != NULL){pSocket->s = s ;pSocket->event = WSACreateEvent() ;}return pSocket ;}//釋放通訊端對象void FreeSocketObj(PSOCKET_OBJ pSocket) {CloseHandle(pSocket->event) ;if(pSocket->s != INVALID_SOCKET){closesocket(pSocket->s) ;}GlobalFree(pSocket) ;}//得到一個線程對象PTHREAD_OBJ GetThreadObj() {PTHREAD_OBJ pThread = (PTHREAD_OBJ)GlobalAlloc(GPTR,sizeof(THREAD_OBJ)) ;if(pThread != NULL){InitializeCriticalSection(&pThread->cs) ;//建立一個事件對象,用於指示該該線程的控制代碼數組需要重建,pThread->events[0] = WSACreateEvent() ;//將新申請的線程對象添加到列表中EnterCriticalSection(&g_cs) ;pThread->pNext = g_pThreadList ; //插入到線程對象鏈表g_pThreadList = pThread ;LeaveCriticalSection(&g_cs) ;}return pThread ;}//釋放一個線程對象void FreeThreadObj(PTHREAD_OBJ pThread) {//線上程對象列表中尋找pThread所指的對象,如果找到就從中移除EnterCriticalSection(&g_cs) ;PTHREAD_OBJ p = g_pThreadList ;if(p == pThread) //如果要刪除的是頭結點{g_pThreadList = p->pNext ;}else{while(p != NULL && p->pNext != pThread){p = p->pNext ;}if(p != NULL){//此時p是pThread的前一個,即"p->pNext == pThread"p->pNext = pThread->pNext ;}}LeaveCriticalSection(&g_cs);//釋放資源CloseHandle(pThread->events[0]) ;DeleteCriticalSection(&pThread->cs) ;GlobalFree(pThread) ;}//重建立立線程對象的events數組.因為事件對象是用數組來儲存的,而通訊端對象是用鏈表來儲存的,當有通訊端對象關閉的時候會出現映射不一致的情況.void RebuildArray(PTHREAD_OBJ pThread) {EnterCriticalSection(&pThread->cs) ;//為了同步監聽連接線程對本線程對象的訪問PSOCKET_OBJ pSocket = pThread->pSockHeader ;int n = 1 ; //從第1個開始寫,第0個用於指示需要重建了while(pSocket != NULL){pThread->events[n] = pSocket->event ;pSocket = pSocket->pNext ;n++ ;}LeaveCriticalSection(&pThread->cs) ;}//向一個線程的通訊端列表中插入一個通訊端BOOL InsertSocketObj(PTHREAD_OBJ pThread,PSOCKET_OBJ pSocket) {BOOL bRet = FALSE ;EnterCriticalSection(&pThread->cs) ;if(pThread->nSocketCount < WSA_MAXIMUM_WAIT_EVENTS-1)//一個線程最多能夠等待的事件對象個數{if(NULL == pThread->pSockHeader)//線程的通訊端列表為空白的{pThread->pSockHeader = pThread->pSockTail = pSocket ;}else{pThread->pSockTail->pNext = pSocket ;pThread->pSockTail = pSocket ;}pThread->nSocketCount++ ;bRet = TRUE ;}LeaveCriticalSection(&pThread->cs) ;//插入成功,說明成功處理了客戶的串連請求if(bRet){InterlockedIncrement(&g_nTotalConnections) ; //原子操作InterlockedIncrement(&g_nCurrentConnections) ;}return bRet ;}//將一個通訊端對象安排給閒置線程處理void AssignToFreeThread(PSOCKET_OBJ pSocket){pSocket->pNext = NULL ;EnterCriticalSection(&g_cs) ;PTHREAD_OBJ pThread = g_pThreadList ; //線程對象鏈表頭//試圖插入到現存線程while(pThread != NULL){if(InsertSocketObj(pThread,pSocket)){break ;}pThread = pThread->pNext ;}//沒有空閑線程,為這個通訊端建立新的線程if(NULL == pThread){pThread = GetThreadObj() ;InsertSocketObj(pThread,pSocket) ;CreateThread(NULL,0,SeverThread,pThread,0,NULL) ; //用beginthreadex,一個線程對象對應一個線程,線程參數為線程對應的線程對象}LeaveCriticalSection(&g_cs) ;//指示新線程重建控制代碼數組WSASetEvent(pThread->events[0]) ;}//從給定線程的通訊端對象列表中移除一個通訊端對象 void RemoveSocketObj(PTHREAD_OBJ pThread,PSOCKET_OBJ pSocket){EnterCriticalSection(&pThread->cs) ;//在通訊端對象列表中尋找指定的通訊端對象,找到後將之移除PSOCKET_OBJ pTest = pThread->pSockHeader ;if(pTest == pSocket) //刪除的是頭結點{if(pThread->pSockHeader == pThread->pSockTail)  //而且只有一個結點{pThread->pSockTail = pThread->pSockHeader = pTest->pNext ;}else //不只有一個頭結點{pThread->pSockHeader = pTest->pNext ;}}else {while(pTest != NULL && pTest->pNext != pSocket){pTest = pTest->pNext ;}if(pTest != NULL){if(pThread->pSockTail == pSocket) //刪除的是尾結點{pThread->pSockTail = pTest ;}pTest->pNext = pSocket->pNext ;}}pThread->nSocketCount-- ;LeaveCriticalSection(&pThread->cs) ;WSASetEvent(pThread->events[0]) ; //指示線程重建控制代碼數組InterlockedDecrement(&g_nCurrentConnections) ; //說明一個串連中斷}//背景工作執行緒負責處理客戶的I/O的請求DWORD WINAPI SeverThread(LPVOID lpParam) {//取得本線程的對象的指標PTHREAD_OBJ pThread = (PTHREAD_OBJ)lpParam ; while(TRUE){//等待網路事件int nIndex = WSAWaitForMultipleEvents(pThread->nSocketCount+1,pThread->events,FALSE,WSA_INFINITE,FALSE) ;nIndex = nIndex - WSA_WAIT_EVENT_0 ;//這裡是會觸發索引一定是最低的嗎?//因為有可能已經有多個觸發,所以需要查看nIndex後面是否已觸發的事件對象for(int i = nIndex ; i < pThread->nSocketCount+1 ; ++i){nIndex = WSAWaitForMultipleEvents(1,&pThread->events[i],TRUE,1000,FALSE) ; //只等待單獨一個事件對象if(nIndex == WSA_WAIT_FAILED || nIndex == WSA_WAIT_TIMEOUT)  {continue ;}else//有事件觸發了{if(0 == i)//events[0] 已觸發,重建數組{RebuildArray(pThread) ;//如果沒有客戶I/O要處理了,則本線程退出if(0 == pThread->nSocketCount){FreeThreadObj(pThread) ;return 0 ;}WSAResetEvent(pThread->events[0]) ;//重設事件對應}else//處理網路事件{//尋找對應的通訊端對象指標,調用HandleIO處理網路事件.用的是索引i進行尋找,所以前面就需要重建數組了.PSOCKET_OBJ pSocket = (PSOCKET_OBJ)FindSocketObj(pThread,i) ;if(pSocket != NULL){if(!HandleIO(pThread,pSocket)) //當HandleIO返回FALSE時,代表通訊端關閉,或者有錯誤發生,雖然重建數組{RebuildArray(pThread) ;}}else{printf("Unable to find socket object\n") ;}}}}}return 0 ;}//FindSocketObj函數根據事件對象在events數組中的索引尋找相應的通訊端對象PSOCKET_OBJ FindSocketObj(PTHREAD_OBJ pThread,int nIndex) //nIndex 從1開始{//在通訊端列表中尋找PSOCKET_OBJ pSocket = pThread->pSockHeader ;while(--nIndex)//倒序尋找的原因{if(NULL == pSocket){return NULL ;}pSocket = pSocket->pNext ;}return pSocket ;}//處理真正的I/OBOOL HandleIO(PTHREAD_OBJ pThread,PSOCKET_OBJ pSocket){//擷取具體發生的網路事件WSANETWORKEVENTS event ;WSAEnumNetworkEvents(pSocket->s,pSocket->event,&event) ; //將通訊端綁定到某一個網路事件上面do{if(event.lNetworkEvents & FD_READ){if(event.iErrorCode[FD_READ_BIT] == 0){char szText[256] ;int nRecv = recv(pSocket->s,szText,sizeof(szText),0) ; //sizeof?if(nRecv >0){szText[nRecv] = '\0' ;printf("接收到資料:%s\n",szText) ;}}else{break ;}}if(event.lNetworkEvents & FD_CLOSE) //這裡要減少串連數才行,不能/*else if*/,書上就是這樣做,導致不能關閉串連 {if(event.iErrorCode[FD_CLOSE_BIT] == 0){printf("關閉一個串連\n") ;break ;}else{printf("關閉串連時出錯\n") ;break ;}}if(event.lNetworkEvents & FD_WRITE) //不能用else if{if(event.iErrorCode[FD_WRITE_BIT] == 0){}else{break ;}}return TRUE ;}while(FALSE) ;//如果通訊端關閉,或者有錯誤發生,程式都會轉到這裡執行 RemoveSocketObj(pThread,pSocket) ;FreeSocketObj(pSocket) ;return FALSE ;}

總結:

WSAEventSelect模型雖然感覺相對於select模型和WSAAsyncSelect模型來說,有一定的伸縮性,但是對於成千上萬的串連來說,基於WSAEventSelect模型的伺服器可能會建立過多的線程,而隨之而來的便是背景工作執行緒之間環境切換帶來的巨大花銷。另外,如果用戶端在串連之後的一段較短的時間會中斷連線,也會造成伺服器短時間內建立、銷毀大量的線程,這裡也應該會帶來一定開銷。












相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.