非阻塞模式WinSock编程入门
WinSock是Windows提供的包含了一系列網絡編程接口的套接字程序庫。在這篇文章中,我們將介紹如何把它的非阻塞模式引入到應用程序中。文章中所討論的通信均為面向連接的通信(TCP),為清晰起見,文章對代碼中的一些細枝末節進行了刪減,大家可以依照文末的鏈接下載完整的工程源碼來獲取這部分內容。
?
阻塞模式WinSock
?????????下述偽代碼給出了阻塞模式下WinSock的使用方式。
view plaincopy to clipboardprint?
?
view plaincopy to clipboardprint?
?
?????????代碼中,服務器端的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消息碼,用于標識我們的網絡消息。
view plaincopy to clipboardprint?
?
?
服務器端,在監聽之前,將監聽套接字置為非阻塞模式,并且標明其感興趣的事件為FD_ACCEPT。
view plaincopy to clipboardprint?
?
客戶端,在連接之前,將套接字置為非阻塞模式,并標明其感興趣的事件為FD_CONNECT。
view plaincopy to clipboardprint?
?
接著,在Windows消息處理函數中,我們將處理監聽事件、連接事件、及讀寫事件,方便起見,這里將服務器和客戶端的處理代碼放在了一起。
view plaincopy to clipboardprint?
?
以上就是非阻塞模式WinSock的應用框架,WSAAsyncSelect模型將套接字和Windows消息機制很好地粘合在一起,為用戶異步SOCKET應用提供了一種較優雅的解決方案。
?
擴展討論
???????? WinSock在系統底層為套接字收發網絡數據各提供一個緩沖區,接收到的網絡數據會緩存在這里等待應用程序讀取,待發送的網絡數據也會先寫進這里之后通過網絡發送。
相關的,針對FD_READ和FD_WRITE事件的讀寫處理,因涉及的內容稍微復雜而容易使人困惑,這里需要特別進行討論。
?????????在FD_READ事件中,使用recv()函數讀取網絡包數據時,由于事先并不知道完整網絡包的大小,所以需要多次讀取直到讀完整個緩沖區。這就需要類似如下代碼的調用:
view plaincopy to clipboardprint?
?
?????????這一切看起來都沒有什么問題,但是如果程序運行起來,你會收到比預期多出許多的FD_READ事件。如MSDN所述,正常的情況下,應用程序應當為每一個FD_READ消息僅調用一次recv()函數。如果一個應用程序需要在一個FD_READ事件處理中調用多次recv(),那么它將會收到多個FD_READ消息,因為每次未讀完緩沖區的recv()調用,都會重新觸發一個FD_READ消息。針對這種情況,我們需要在讀取網絡包前關閉掉FD_READ消息通知,讀取完這后再進行恢復,關閉FD_READ消息的方法很簡單,只需要調用WSAAsyncSelect時參數lEvent中FD_READ字段不予設置即可。
view plaincopy to clipboardprint?
?
?????????第二個需要討論的是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粘包等)這里沒有討論,或許會在以后的文章中給出。
?????????文章相關工程源碼請移步此處下載http://download.csdn.net/source/2852485。該源碼展示了采用非阻塞模式編程的服務器和客戶端,建立連接后,在服務器窗口輸入空格會向所有客戶端發送一條字符串消息。源碼中對網絡通信部分做了簡單封裝,所以代碼結構會和文中的偽代碼稍有不同。
謝謝您的閱讀!
總結
以上是生活随笔為你收集整理的非阻塞模式WinSock编程入门的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: UDP和TCP的区别(详细)
- 下一篇: 多进程和多线程的优缺点