详细解析WSAAsyncSelect模型
介紹
WinSock是Windows提供的包含了一系列網絡編程接口的套接字程序庫。在這篇文章中,我們將介紹如何把它的非阻塞模式引入到應用程序中。
阻塞模式WinSock.下述偽代碼給出了阻塞模式下WinSock的使用方式。
//服務器
WSAStartup();
SOCKET server = socket();
bind(server);
listen(server);
SOCKET client = accept(server);
send(client);
recv(client);
closesocket(client);
closesocket(server);?
WSACleanup();
//客戶端
WSAStartup();
SOCKET client=socket();?
bind(client);
ServerAddress server;
connect(client, server);
recv(client);
send(client);
closesocket(client);
WSACleanup();
代碼中,服務器端的accept(),客戶端的connect(),以及服務器和客戶端中共同的recv()、send()函數均會產生阻塞。
服務器在調用accept()后不會返回,直到接收到客戶端的連接請求;
客戶端在調用connect()后不會返回,直到對服務器連接成功或者失敗;
服務器和客戶端在調用recv()后不會返回,直到接收到并讀取完一條消息;
服務器和客戶端在調用send()后不會返回,直到發送完待發送的消息。
如果這兩段代碼被放在Windows程序的主線程中,你會發現消息循環被阻塞,程序不再響應用戶輸入及重繪請求。為了解決這個問題,
你可能會想到開辟另外一個線程來運行這些代碼。這是可行的,但是考慮到每個SOCKET都不應該被其他SOCKET的操作所阻塞,是不是
需要為每個SOCKET開辟一個線程?再考慮到同一SOCKET的一個讀寫操作也不應該被另外一個讀寫操作所阻塞,是不是應該再為每個
SOCKET的讀和寫分別開辟一個線程?一般來說,這種自實現的多線程解決方案帶來的諸多線程管理方面的問題,是你絕對不會想要遇
到的。
?
非阻塞模式WinSock
所幸的是,WinSock同時提供了非阻塞模式,并提出了幾種I/O模型。最常見的I/O模型有select模型、WSAAsyncSelect模型及
WSAEventSelect模型,下面選擇其中的WSAAsyncSelect模型進行介紹。使用WSAAsyncSelect模型將非阻塞模式引入到應用程序中的過
程看起來很簡單,事實上你只需要多添加一個函數就夠了。
int WSAAsyncSelect(SOCKET s, HWND hWnd, unsigned int wMsg, long lEvent);
該函數會自動將套接字設置為非阻塞模式,并且把發生在該套接字上且是你所感興趣的事件,以Windows消息的形式發送到指定的窗口,
你需要做的就是在傳統的消息處理函數中處理這些事件。參數hWnd表示指定接受消息的窗口句柄;參數wMsg表示消息碼值(這意味著你
需要自定義一個Windows消息碼);參數IEvent表示你希望接受的網絡事件的集合,它可以是如下值的任意組合:FD_READ, FD_WRITE,?
FD_OOB, FD_ACCEPT, FD_CONNECT, FD_CLOSE 之后,就可以在我們熟知的Windows消息處理函數中處理這些事件。如果在某一套接字s上
發生了一個已命名的網絡事件,應用程序窗口hWnd會接收到消息wMsg。參數wParam即為該事件相關的套接字s;參數lParam的低字段指
明了發生的網絡事件,lParam的高字段則含有一個錯誤碼,事件和錯誤碼可以通過下面的宏從lParam中取出:
#define WSAGETSELECTEVENT(lParam) LOWORD(lParam)
#define WSAGETSELECTERROR(lParam) HIWORD(lParam)
下面繼續使用偽代碼來幫助闡述如何將上一節的阻塞模式WinSock應用升級到非阻塞模式。
首先自定義一個Windows消息碼,用于標識我們的網絡消息。
#define WM_CUSTOM_NETWORK_MSG (WM_USER + 100)?
//服務器端,在監聽之前,將監聽套接字置為非阻塞模式,并且標明其感興趣的事件為FD_ACCEPT。
WSAAsyncSelect(server, wnd, WM_CUSTOM_NETWORK_MSG, FD_ACCEPT);?
listen(server);?
//客戶端,在連接之前,將套接字置為非阻塞模式,并標明其感興趣的事件為FD_CONNECT。
WSAAsyncSelect(client, wnd, WM_CUSTOM_NETWORK_MSG, FD_CONNECT);
ServerAddress?server;
connect(client,?server);
//接著,在Windows消息處理函數中,我們將處理監聽事件、連接事件、及讀寫事件,方便起見,這里將服務器和客戶端的處理代碼放在
了一起。
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) ? ?
{ ? ?
? ? switch (message) ? ?
? ? {
? ? case WM_CUSTOM_NETWORK_MSG: // 自定義的網絡消息碼 ? ?
? ? ? ? { ? ?
? ? ? ? ? ? SOCKET socket = (SOCKET)wParam; // 發生網絡事件的套接字 ? ?
? ? ? ? ? ? long event = WSAGETSELECTEVENT(lParam); // 事件 ? ?
? ? ? ? ? ? int error = WSAGETSELECTERROR(lParam); // 錯誤碼 ? ?
? ??
? ? ? ? ? ? switch (event) ? ?
? ? ? ? ? ? { ? ?
? ? ? ? ? ? case FD_ACCEPT: // 服務器收到新客戶端的連接請求 ? ?
? ? ? ? ? ? ? ? { ? ?
? ? ? ? ? ? ? ? ? ? // 接收到客戶端連接,分配一個客戶端套接字 ? ?
? ? ? ? ? ? ? ? ? ? SOCKET client = accept(socket); ? ??
? ? ? ? ? ? ? ? ? ? // 將新分配的客戶端套接字置為非阻塞模式,并標明其感興趣的事件為讀、寫及關閉 ? ?
? ? ? ? ? ? ? ? ? ? WSAAsyncSelect(client, hWnd, message, FD_READ | FD_WRITE | FD_CLOSE); ? ?
? ? ? ? ? ? ? ? } ? ?
? ? ? ? ? ? ? ? break; ? ?
? ? ? ? ? ? case FD_CONNECT: // 客戶端連接到服務器的操作返回結果 ? ?
? ? ? ? ? ? ? ? { ? ?
? ? ? ? ? ? ? ? ? ? // 成功連接到服務器,將客戶端套接字置為非阻塞模式,并標明其感興趣的事件為讀、寫及關閉 ? ?
? ? ? ? ? ? ? ? ? ? WSAAsyncSelect(socket, hWnd, message, FD_READ | FD_WRITE | FD_CLOSE); ? ?
? ? ? ? ? ? ? ? } ? ?
? ? ? ? ? ? ? ? break; ? ?
? ? ? ? ? ? case FD_READ: // 收到網絡包,需要讀取 ? ?
? ? ? ? ? ? ? ? { ? ?
? ? ? ? ? ? ? ? ? ? // 使用套接字讀取網絡包 ? ?
? ? ? ? ? ? ? ? ? ? recv(socket); ? ?
? ? ? ? ? ? ? ? } ? ?
? ? ? ? ? ? ? ? break; ? ?
? ? ? ? ? ? case FD_WRITE: ? ?
? ? ? ? ? ? ? ? { ? ?
? ? ? ? ? ? ? ? ? ? // FD_WRITE的處理后面會具體討論 ? ?
? ? ? ? ? ? ? ? } ? ?
? ? ? ? ? ? ? ? break; ? ?
? ? ? ? ? ? case FD_CLOSE: // 套接字的連接方(而非本地socket)關閉消息 ? ?
? ? ? ? ? ? ? ? { ? ?
? ? ? ? ? ? ? ? } ? ?
? ? ? ? ? ? ? ? break; ? ?
? ? ? ? ? ? default: ? ?
? ? ? ? ? ? ? ? break; ? ?
? ? ? ? ? ? } ? ?
? ? ? ? } ? ?
? ? ? ? break; ? ?
? ? … ? ?
? ? } ? ?
? ? … ? ?
} ? ?
以上就是非阻塞模式WinSock的應用框架,WSAAsyncSelect模型將套接字和Windows消息機制很好地粘合在一起,為用戶異步SOCKET應用提供
了一種較優雅的解決方案。
擴展討論
WinSock在系統底層為套接字收發網絡數據各提供一個緩沖區,接收到的網絡數據會緩存在這里等待應用程序讀取,待發送的網絡數據也會先
寫進這里之后通過網絡發送。相關的,針對FD_READ和FD_WRITE事件的讀寫處理,因涉及的內容稍微復雜而容易使人困惑,這里需要特別進行
討論。在FD_READ事件中,使用recv()函數讀取網絡包數據時,由于事先并不知道完整網絡包的大小,所以需要多次讀取直到讀完整個緩沖區
。這就需要類似如下代碼的調用:
void* buf = 0; ? ?
int size = 0; ? ?
while (true) ? ?
{ ? ?
? ? char tmp[128]; ? ?
? ? int bytes = recv(socket, tmp, 128, 0); ? ?
? ? if (bytes <= 0) ? ?
? ? ? ? break; ? ?
? ? else ? ?
? ? { ? ?
? ? ? ? int new_size = size + bytes; ? ?
? ? ? ? buf = realloc(buf, new_size); ? ?
? ? ? ? memcpy((void*)(((char*)buf) + size), tmp, bytes); ? ?
? ? ? ? size = new_size; ? ?
? ? } ? ?
} ? ?
//此時數據已經從緩沖區全部拷貝到buf中,你可以在這里對buf做一些操作 ? ? ?
free(buf); ? ?
這一切看起來都沒有什么問題,但是如果程序運行起來,你會收到比預期多出許多的FD_READ事件。如MSDN所述,正常的情況下,應用程序應
當為每一個FD_READ消息僅調用一次recv()函數。如果一個應用程序需要在一個FD_READ事件處理中調用多次recv(),那么它將會收到多個
FD_READ消息,因為每次未讀完緩沖區的recv()調用,都會重新觸發一個FD_READ消息。針對這種情況,我們需要在讀取網絡包前關閉掉FD_READ
消息通知,讀取完這后再進行恢復,關閉FD_READ消息的方法很簡單,只需要調用WSAAsyncSelect時參數lEvent中FD_READ字段不予設置即可。
//關閉FD_READ事件通知 ? ?
WSAAsyncSelect(socket, hWnd, message, FD_WRITE | FD_CLOSE); ? ?
// 讀取網絡包 ? ?
… ? ?
// 再次打開FD_READ事件通知 ? ?
WSAAsyncSelect(socket, hWnd, message, FD_WRITE | FD_CLOSE | FD_READ); ? ?
第二個需要討論的是FD_WRITE事件。這個事件指明緩沖區已經準備就緒,有了多出的空位可以讓應用程序寫入數據以供發送。該事件僅在兩種
情況下被觸發:
1. 套接字剛建立連接時,表明準備就緒可以立即發送數據。
2. 一次失敗的send()調用后緩沖區再次可用時。如果系統緩沖區已經被填滿,那么此時調用send()發送數據,將返回SOCKET_ERROR,使用
WSAGetLastError()會得到錯誤碼WSAEWOULDBLOCK表明被阻塞。這種情況下當緩沖區重新整理出可用空間后,會向應用程序發送FD_WRITE消息,
示意其可以繼續發送數據了。
所以說收到FD_WRITE消息并不單純地等同于這是使用send()的唯一時機。一般來說,如果需要發送消息,直接調用send()發送即可。如果該次
調用返回值為SOCKET_ERROR且WSAGetLastError()得到錯誤碼WSAEWOULDBLOCK,這意味著緩沖區已滿暫時無法發送,此刻我們需要將待發數據
保存起來,等到系統發出FD_WRITE消息后嘗試重新發送。也就是說,你需要針對FD_WRITE構建一套數據重發的機制,文末的工程源碼里包含有
這套機制以供大家參考,這里不再贅述。
結語
至此,如何在非阻塞模式下使用WinSock進行編程介紹完畢,這個框架可以滿足大多數網絡游戲客戶端及部分服務器的通信需求。更多應用層面
上的問題(如TCP粘包等)這里沒有討論,或許會在以后的文章中給出。
WSAAsyncSelect模型(同步I/O模型)
這里為什么說他是同步的,就是因為實際的數據的Copy是同步進行///的,而不是異步的,只是相應的通知機制(通知數據已經準備好了),是異步的
這個模型允許應用程序以Windows消息的形式可在一個套接字上,接收網絡事件通知
具體的做法是在建好一個套接字后,調用WSAAsyncSelect函數。
在我看來,WSAAsyncSelect是最簡單的一種Winsock I/O模型(之所以說它簡單是因為一個主線程就搞定了)。
這里,我們需要做的僅僅是:
1.在WM_CREATE消息處理函數中,初始化Windows Socket library,創建監聽套接字,綁定,監聽,并且調用WSAAsyncSelect函數表示我們關心在監聽套接字上發生的FD_ACCEPT事件;
2.自定義一個消息WM_SOCKET,一旦在我們所關心的套接字(監聽套接字和客戶端套接字)上發生了某個事件,系統就會調用WndProc并且message參數被設置為WM_SOCKET;
3.在WM_SOCKET的消息處理函數中,分別對FD_ACCEPT、FD_READ和FD_CLOSE事件進行處理;
4.在窗口銷毀消息(WM_DESTROY)的處理函數中,我們關閉監聽套接字,清除Windows Socket library
WSAAsyncSelect模型是Windows?socket的一個異步IO模型。利用該模型可以接收以Windows消息為基礎的網絡事件。Windows?sockets應用程序在創建套接字后,調用WSAAsyncSelect函數注冊感興趣的網絡事件,當該事件發生時Windows窗口收到消息,應用程序就可以對接收到的網絡時間進行處理。
?
WSAAsyncSelect是select模型的異步版本。在應用程序使用select函數時會發生阻塞現象??梢酝ㄟ^select的timeout參數設置阻塞的時間。在設置的時間內,select函數等待,直到一個或多個套接字滿足可讀或可寫的條件。
而WSAAsyncSelect是非阻塞的。Windows?sockets程序在調用recv或send之前,調用WSAAsyncSelect注冊網絡事件。WSAAsyncSelect函數立即返回。當系統中數據準備好時,會向應用程序發送消息。此此消息的處理函數中可以調用recv或send進行接收或發送數據。
?
WSAAsyncSelect模型與select模型的相同點是它們都可以對多個套接字進行管理。但它們也有不小的區別。首先WSAAsyncSelect模型是異步的,且通知方式不同。更重要的一點是:WSAAsyncSelect模型應用在基于消息的Windows環境下,使用該模型時必須創建窗口,而select模型可以廣泛應用在Unix系統,使用該模型不需要創建窗口。最后一點區別:應用程序在調用WSAAsyncSelect函數后,套接字就被設置為非阻塞狀態。而使用select函數不改變套接字的工作方式。
?
WSAAsyncSelect函數。
該函數告訴系統當網絡事件發生時為套接字發送消息。聲明如下:?
[html]?view plaincopy?????s為需要通知的套接字。
?????hWnd為當網絡事件發生時接收消息的窗口句柄。
?????wMsg為當網絡事件發生時窗口收到的消息。在此消息的響應函數內對網絡事件進行處理。
?????lEvent為應用程序感興趣的網絡事件集合。
?????應用程序調用該函數后自動將套接字設置為非阻塞模式。通常用戶自定義消息應該在WM_USER的基礎之上定義。如WM_USER+1,以避免與Windows預定義的消息發生混淆。
?????網絡事件可以有以下幾種:
?
?????FD_READ:套接字可讀通知。
?????FD_WRITE:可寫通知。
?????FD_ACCEPT:服務器接收連接的通知。
?????FD_CONNECT:有客戶連接通知。
?????FD_OOB:外帶數據到達通知。
?????FD_CLOSE:套接字關閉通知。
?????FD_QOS:服務質量發生變化通知。
?????FD_GROUP_QOS:組服務質量發生變化通知。
?????FD_ROUTING_INTERFACE_CHANGE:與路由器接口發生變化的通知。
?????FD_ADDRESS_LIST_CHANGE:本地地址列表發生變化的通知。
?
?????開發人員應向應用程序注冊感興趣的網絡事件??梢詫⑺鼈儼次换虿鹘olEvent函數。如:
[cpp]?view plaincopy?????上述代碼表示:當套接字連接到來、有數據可讀或這套接字關閉的網絡事件發生時,WM_SOCKET消息就會發送給hWnd為句柄的窗口。
?????消息處理函數。
?????消息處理函數是對網絡事件發生時窗口消息的處理。它的聲明如下:
[cpp]?view plaincop?????hWnd為窗口句柄。
?????uMsg為當網絡事件發生時的消息。
?????wParam為消息參數。該參數表明發生網絡事件的套接字。
?????lParam也為消息參數。低字節表明已發生的網絡事件。高字節包含錯誤代碼。
?
?????在Windows?sockets應用程序中,當WindowProc接收到網絡消息時,在該函數內執行下面的步驟:
?????1:讀取lParam的高字節,判斷是否有錯誤發生??梢允褂?span style="font-family:'Times New Roman'">WSAGETSElECTERROR宏。
?????2:如果沒有錯誤,讀取lParam的低字節,檢查發生了什么網絡事件,可以使用WSAGETSELECTEVENT宏。
?
?????WSAGETSElECTERROR和WSAGETSELECTEVENT宏定義如下:
?
[cpp]?view plaincopy
?
接下來就需要創建窗口和將網絡消息與消息處理函數關聯起來。如果使用MFC可以使用MFC提供的宏來進行處理。
注意:多次調用WSAAsyncSelect時,最后一次調用會取消前面注冊的網絡事件。
?
因為調用accept接受的套接字和監聽套接字具有同樣的屬性。所以,任何為監聽套接字設置的網絡事件對接受套接字同樣起作用。如果一個監聽套接字請求FD_ACCEPT、FD_READ和FD_WRITE網絡事件。則在該監聽套接字上接受的任何套接字也會請求FD_ACCEPT,FD_READ和FD_WRITE網絡事件。
?
FD_CLOSE網絡事件用來判斷套接字是否已經關閉。錯誤代碼會指出套接字是從容關閉還是硬關閉。如果為0,為從容關閉。若錯誤代碼為WSAECONNRESET,則套接字是硬關閉。調用closesocket不會投遞FD_CLOSE事件。
發生網絡事件的條件。
?
下列條件下會發生FD_READ事件:
1:當調用WSAAsyncSelect函數時,如果當前有數據可讀。
2:當數據到達并且沒有發送FD_READ網絡事件時。
3:調用recv()或這recvfrom,如果仍有數據可讀里。
?
下列情況下會發生FD_WRITE事件:
1:調用WSAAsyncSelect函數時,如果能夠發送數據時。
2:connect或者accept函數后,連接已經建立時。
3:調用send或者sendto函數,返回WSAWOULDBLOCK錯誤后,再次調用send()或者sendto函數可能成功時。因為此時可能是套接字還處于不可寫狀態,多次調用直到調用成功為止。
?
WSAAsyncSelect的優勢與不足。
該模型是在基于消息的Windows環境下開發應用程序。開發人員可以像處理其他消息一樣,對網絡事件進行處理。而且為確保接受所有數據提供了很好的機制。
不足:由于該模型基于Windows消息機制,必須在應用程序中創建窗口。雖然可以在開發中,確定是否顯示該窗口。?由于調用WSAAsyncSelect函數后自動將套接字設置為非阻塞狀態,當應用程序接收到網絡事件時,未必能夠成功返回。這無疑增加了使用該模型的難度。
接下來展示一個使用如何WSAAsyncSelect模型的例子。該程序使用WSAAsyncSelect模型管理接受的客戶端套接字。編碼步驟如下:
?????1:聲明自定義消息。如WM_SOCKET
?????2:聲明窗口例程。
?????3:將自定義消息與消息處理函數相關聯。
?????4:初始化套接字動態庫,創建套接字。
?????5:調用WSAAsyncSelect注冊感興趣的網絡事件。本例服務器感興趣的網絡事件有FD_ACCEPT和FD_CLOSE。
?????6:綁定套接字開始監聽。
?????一:聲明自定義消息:
?
[cpp]?view plaincopy?????除了聲明自定義消息外還需要聲明最大字符串長度、服務器監聽端口、數據緩沖區。
?
[cpp]?view plaincopy?????二:聲明消息處理函數并與消息關聯:
?
?????1:在窗口類頭文件中聲明消息處理函數。如:
?
[cpp]?view plaincopy?
?????2:在消息映射宏中將自定義消息如聲明的消息處理函數關聯:
?
[cpp]?view plaincopy?
?????3:實現消息處理函數:
[cpp]?view plaincopy
?
三:創建套接字并注冊感興趣的網絡事件.
1:初始化套接字動態庫,并創建套接字。
[cpp]?view plaincopy2:注冊感興趣的網絡事件:
[cpp]?view plaincopy?
3:綁定套接字并監聽。
[cpp]?view plaincopy?
四:退出
[cpp]?view plaincopy
五:CClient類。
自定義類CClient類用于管理服務器接受客戶端的新建套接字。在該類中實現與客戶端通信。
?
六;管理客戶端套接字鏈表。
當服務器接受一個客戶端連接請求后就會創建一個CClient實例。將該實例地址加入鏈表中。
===============================================================================================================
//
/// WSAAsyncEvent模型(同步I/O模型)
///這里為什么說他是同步的,就是因為實際的數據的Copy是同步進行///的,而不是異步的,只是相應的通知機制(通知數據已經準備好了),///是異步的
/// 和WSAAsyncSelect模型類似的是,它也允許應用程序在一個或多個套接字上,接收以事件為
/// 基礎的網絡事件通知。在用新模型開發的應用程序中,也能接收和處理所有那些事件。
/// 該模型最主要的差別在于網絡事件會投遞至一個事件對象句柄,而非投遞至一個窗口例程。
/// 它的基本思想是將每個套接字都和一個WSAEVENT對象對應起來,并且在關聯的時候指定需要
/// 關注的哪些網絡事件。一旦在某個套接字上發生了我們關注的事件(FD_READ和FD_CLOSE),
/// 與之相關聯的WSAEVENT對象被Signaled。
/// 程序定義了兩個全局數組,一個套接字數組,一個WSAEVENT對象數組,其大小都是MAXIMUM_WAIT_OBJECTS(64),
/// 兩個數組中的元素一一對應。
//
/// WSAEventSelect function specifies an event object to be associated with the supplied set of
/// FD_XXX network events.
//
/// 附加裝置:事件監視器的集合,每一個事件監視器監視一個Socket上的相應的相應的行為
/// 微軟的信箱非常暢銷,購買微軟信箱的人以百萬計數......以至于蓋茨每天24小時給客戶打電話
/// ,累得腰酸背痛,喝蟻力神都不好使。微軟改進了他們的信箱:在客戶的家中添加一個附加裝置
/// ,這個裝置會監視客戶的信箱,每當新的信件來臨,此裝置會發出“新信件到達”聲,提醒老陳
/// 去收信。蓋茨終于可以睡覺了。??
//
| #include?<winsock2.h> |
總結
以上是生活随笔為你收集整理的详细解析WSAAsyncSelect模型的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 用完成例程(Completion Rou
- 下一篇: Windows Socket五种I/O模