电竞比分网-中国电竞赛事及体育赛事平台

分享

非阻塞模式WinSock編程入門

 orion360doc 2010-12-29
非阻塞模式WinSock編程入門

 

介紹

WinSock是Windows提供的包含了一系列網(wǎng)絡編程接口的套接字程序庫。在這篇文章中,我們將介紹如何把它的非阻塞模式引入到應用程序中。文章中所討論的通信均為面向連接的通信(TCP),為清晰起見,文章對代碼中的一些細枝末節(jié)進行了刪減,大家可以依照文末的鏈接下載完整的工程源碼來獲取這部分內(nèi)容。

 

阻塞模式WinSock

         下述偽代碼給出了阻塞模式下WinSock的使用方式。

view plaincopy to clipboardprint?
//---------------------------------------   
// 服務器   
//----------------------------------------   
// WinSock初始化   
WSAStartup();   
  
// 創(chuàng)建服務器套接字   
SOCKET server = socket();   
  
// 綁定到本機端口   
bind(server);    
  
// 開始監(jiān)聽   
listen(server);    
  
// 接收到客戶端連接,分配一個客戶端套接字   
SOCKET client = accept(server);    
  
// 使用新分配的客戶端套接字進行消息收發(fā)   
send(client);    
recv(client);   
  
// 關閉客戶端套接字   
closesocket(client);    
  
// 關閉服務器套接字   
closesocket(server);   
  
// 卸載WinSock   
WSACleanup();  
//---------------------------------------
// 服務器
//----------------------------------------
// WinSock初始化
WSAStartup();

// 創(chuàng)建服務器套接字
SOCKET server = socket();

// 綁定到本機端口
bind(server); 

// 開始監(jiān)聽
listen(server); 

// 接收到客戶端連接,分配一個客戶端套接字
SOCKET client = accept(server); 

// 使用新分配的客戶端套接字進行消息收發(fā)
send(client); 
recv(client);

// 關閉客戶端套接字
closesocket(client); 

// 關閉服務器套接字
closesocket(server);

// 卸載WinSock
WSACleanup();  

view plaincopy to clipboardprint?
//---------------------------------------   
// 客戶端   
//---------------------------------------   
WSAStartup();   
  
// 創(chuàng)建客戶端套接字   
SOCKET client = socket();   
  
// 綁定本機端口   
bind(client);   
  
// 連接到服務器   
ServerAddress server;   
connect(client, server);   
  
// 確立連接后收發(fā)消息   
recv(client);   
send(client);   
  
// 關閉客戶端套接字   
closesocket(client);   
  
WSACleanup();  
//---------------------------------------
// 客戶端
//---------------------------------------
WSAStartup();

// 創(chuàng)建客戶端套接字
SOCKET client = socket();

// 綁定本機端口
bind(client);

// 連接到服務器
ServerAddress server;
connect(client, server);

// 確立連接后收發(fā)消息
recv(client);
send(client);

// 關閉客戶端套接字
closesocket(client);

WSACleanup();  

         代碼中,服務器端的accept(),客戶端的connect(),以及服務器和客戶端中共同的recv()、send()函數(shù)均會產(chǎn)生阻塞。

服務器在調(diào)用accept()后不會返回,直到接收到客戶端的連接請求;

客戶端在調(diào)用connect()后不會返回,直到對服務器連接成功或者失敗;

服務器和客戶端在調(diào)用recv()后不會返回,直到接收到并讀取完一條消息;

服務器和客戶端在調(diào)用send()后不會返回,直到發(fā)送完待發(fā)送的消息。

如果這兩段代碼被放在Windows程序的主線程中,你會發(fā)現(xiàn)消息循環(huán)被阻塞,程序不再響應用戶輸入及重繪請求。為了解決這個問題,你可能會想到開辟另外一個線程來運行這些代碼。這是可行的,但是考慮到每個SOCKET都不應該被其他SOCKET的操作所阻塞,是不是需要為每個SOCKET開辟一個線程?再考慮到同一SOCKET的一個讀寫操作也不應該被另外一個讀寫操作所阻塞,是不是應該再為每個SOCKET的讀和寫分別開辟一個線程?一般來說,這種自實現(xiàn)的多線程解決方案帶來的諸多線程管理方面的問題,是你絕對不會想要遇到的。

 

非阻塞模式WinSock

         所幸的是,WinSock同時提供了非阻塞模式,并提出了幾種I/O模型。最常見的I/O模型有select模型、WSAAsyncSelect模型及WSAEventSelect模型,下面選擇其中的WSAAsyncSelect模型進行介紹。

         使用WSAAsyncSelect模型將非阻塞模式引入到應用程序中的過程看起來很簡單,事實上你只需要多添加一個函數(shù)就夠了。

int WSAAsyncSelect(SOCKET s, HWND hWnd, unsigned int wMsg, long lEvent);

該函數(shù)會自動將套接字設置為非阻塞模式,并且把發(fā)生在該套接字上且是你所感興趣的事件,以Windows消息的形式發(fā)送到指定的窗口,你需要做的就是在傳統(tǒng)的消息處理函數(shù)中處理這些事件。參數(shù)hWnd表示指定接受消息的窗口句柄;參數(shù)wMsg表示消息碼值(這意味著你需要自定義一個Windows消息碼);參數(shù)IEvent表示你希望接受的網(wǎng)絡事件的集合,它可以是如下值的任意組合:

FD_READ, FD_WRITE, FD_OOB, FD_ACCEPT, FD_CONNECT, FD_CLOSE

         之后,就可以在我們熟知的Windows消息處理函數(shù)中處理這些事件。如果在某一套接字s上發(fā)生了一個已命名的網(wǎng)絡事件,應用程序窗口hWnd會接收到消息wMsg。參數(shù)wParam即為該事件相關的套接字s;參數(shù)lParam的低字段指明了發(fā)生的網(wǎng)絡事件,lParam的高字段則含有一個錯誤碼,事件和錯誤碼可以通過下面的宏從lParam中取出:

#define WSAGETSELECTEVENT(lParam) LOWORD(lParam)

#define WSAGETSELECTERROR(lParam) HIWORD(lParam)

 

下面繼續(xù)使用偽代碼來幫助闡述如何將上一節(jié)的阻塞模式WinSock應用升級到非阻塞模式。

首先自定義一個Windows消息碼,用于標識我們的網(wǎng)絡消息。

view plaincopy to clipboardprint?
#define WM_CUSTOM_NETWORK_MSG (WM_USER + 100)  
#define WM_CUSTOM_NETWORK_MSG (WM_USER + 100) 

 

服務器端,在監(jiān)聽之前,將監(jiān)聽套接字置為非阻塞模式,并且標明其感興趣的事件為FD_ACCEPT。

view plaincopy to clipboardprint?
…   
WSAAsyncSelect(server, wnd, WM_CUSTOM_NETWORK_MSG, FD_ACCEPT);   
  
// 開始監(jiān)聽   
listen(server);  
WSAAsyncSelect(server, wnd, WM_CUSTOM_NETWORK_MSG, FD_ACCEPT);

// 開始監(jiān)聽
listen(server);  

客戶端,在連接之前,將套接字置為非阻塞模式,并標明其感興趣的事件為FD_CONNECT。

view plaincopy to clipboardprint?
…   
WSAAsyncSelect(client, wnd, WM_CUSTOM_NETWORK_MSG, FD_CONNECT);   
  
// 連接到服務器   
ServerAddress server;   
connect(client, server);  
WSAAsyncSelect(client, wnd, WM_CUSTOM_NETWORK_MSG, FD_CONNECT);

// 連接到服務器
ServerAddress server;
connect(client, server);  

接著,在Windows消息處理函數(shù)中,我們將處理監(jiān)聽事件、連接事件、及讀寫事件,方便起見,這里將服務器和客戶端的處理代碼放在了一起。

view plaincopy to clipboardprint?
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)   
{   
    switch (message)   
    {   
    …   
    case WM_CUSTOM_NETWORK_MSG: // 自定義的網(wǎng)絡消息碼   
        {   
            SOCKET socket = (SOCKET)wParam; // 發(fā)生網(wǎng)絡事件的套接字   
            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: // 收到網(wǎng)絡包,需要讀取   
                {   
                    // 使用套接字讀取網(wǎng)絡包   
                    recv(socket);   
                }   
                break;   
            case FD_WRITE:   
                {   
                    // FD_WRITE的處理后面會具體討論   
                }   
                break;   
            case FD_CLOSE: // 套接字的連接方(而非本地socket)關閉消息   
                {   
                }   
                break;   
            default:   
                break;   
            }   
        }   
        break;   
    …   
    }   
    …   
}  
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_CUSTOM_NETWORK_MSG: // 自定義的網(wǎng)絡消息碼
{
SOCKET socket = (SOCKET)wParam; // 發(fā)生網(wǎng)絡事件的套接字
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: // 收到網(wǎng)絡包,需要讀取
{
// 使用套接字讀取網(wǎng)絡包
recv(socket);
}
break;
case FD_WRITE:
{
// FD_WRITE的處理后面會具體討論
}
break;
case FD_CLOSE: // 套接字的連接方(而非本地socket)關閉消息
{
}
break;
default:
break;
}
}
break;
}
}  

以上就是非阻塞模式WinSock的應用框架,WSAAsyncSelect模型將套接字和Windows消息機制很好地粘合在一起,為用戶異步SOCKET應用提供了一種較優(yōu)雅的解決方案。

 

擴展討論

         WinSock在系統(tǒng)底層為套接字收發(fā)網(wǎng)絡數(shù)據(jù)各提供一個緩沖區(qū),接收到的網(wǎng)絡數(shù)據(jù)會緩存在這里等待應用程序讀取,待發(fā)送的網(wǎng)絡數(shù)據(jù)也會先寫進這里之后通過網(wǎng)絡發(fā)送。

相關的,針對FD_READ和FD_WRITE事件的讀寫處理,因涉及的內(nèi)容稍微復雜而容易使人困惑,這里需要特別進行討論。

         在FD_READ事件中,使用recv()函數(shù)讀取網(wǎng)絡包數(shù)據(jù)時,由于事先并不知道完整網(wǎng)絡包的大小,所以需要多次讀取直到讀完整個緩沖區(qū)。這就需要類似如下代碼的調(diào)用:

view plaincopy to clipboardprint?
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;   
    }   
}   
// 此時數(shù)據(jù)已經(jīng)從緩沖區(qū)全部拷貝到buf中,你可以在這里對buf做一些操作   
…   
free(buf);  
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;
}
}
// 此時數(shù)據(jù)已經(jīng)從緩沖區(qū)全部拷貝到buf中,你可以在這里對buf做一些操作
free(buf);
  

         這一切看起來都沒有什么問題,但是如果程序運行起來,你會收到比預期多出許多的FD_READ事件。如MSDN所述,正常的情況下,應用程序應當為每一個FD_READ消息僅調(diào)用一次recv()函數(shù)。如果一個應用程序需要在一個FD_READ事件處理中調(diào)用多次recv(),那么它將會收到多個FD_READ消息,因為每次未讀完緩沖區(qū)的recv()調(diào)用,都會重新觸發(fā)一個FD_READ消息。針對這種情況,我們需要在讀取網(wǎng)絡包前關閉掉FD_READ消息通知,讀取完這后再進行恢復,關閉FD_READ消息的方法很簡單,只需要調(diào)用WSAAsyncSelect時參數(shù)lEvent中FD_READ字段不予設置即可。

view plaincopy to clipboardprint?
// 關閉FD_READ事件通知   
WSAAsyncSelect(socket, hWnd, message, FD_WRITE | FD_CLOSE);   
// 讀取網(wǎng)絡包   
…   
// 再次打開FD_READ事件通知   
WSAAsyncSelect(socket, hWnd, message, FD_WRITE | FD_CLOSE | FD_READ);  
// 關閉FD_READ事件通知
WSAAsyncSelect(socket, hWnd, message, FD_WRITE | FD_CLOSE);
// 讀取網(wǎng)絡包
// 再次打開FD_READ事件通知
WSAAsyncSelect(socket, hWnd, message, FD_WRITE | FD_CLOSE | FD_READ);  

         第二個需要討論的是FD_WRITE事件。這個事件指明緩沖區(qū)已經(jīng)準備就緒,有了多出的空位可以讓應用程序寫入數(shù)據(jù)以供發(fā)送。該事件僅在兩種情況下被觸發(fā):

1. 套接字剛建立連接時,表明準備就緒可以立即發(fā)送數(shù)據(jù)。

2. 一次失敗的send()調(diào)用后緩沖區(qū)再次可用時。如果系統(tǒng)緩沖區(qū)已經(jīng)被填滿,那么此時調(diào)用send()發(fā)送數(shù)據(jù),將返回SOCKET_ERROR,使用WSAGetLastError()會得到錯誤碼WSAEWOULDBLOCK表明被阻塞。這種情況下當緩沖區(qū)重新整理出可用空間后,會向應用程序發(fā)送FD_WRITE消息,示意其可以繼續(xù)發(fā)送數(shù)據(jù)了。

所以說收到FD_WRITE消息并不單純地等同于這是使用send()的唯一時機。一般來說,如果需要發(fā)送消息,直接調(diào)用send()發(fā)送即可。如果該次調(diào)用返回值為SOCKET_ERROR且WSAGetLastError()得到錯誤碼WSAEWOULDBLOCK,這意味著緩沖區(qū)已滿暫時無法發(fā)送,此刻我們需要將待發(fā)數(shù)據(jù)保存起來,等到系統(tǒng)發(fā)出FD_WRITE消息后嘗試重新發(fā)送。也就是說,你需要針對FD_WRITE構建一套數(shù)據(jù)重發(fā)的機制,文末的工程源碼里包含有這套機制以供大家參考,這里不再贅述。

 

結語

         至此,如何在非阻塞模式下使用WinSock進行編程介紹完畢,這個框架可以滿足大多數(shù)網(wǎng)絡游戲客戶端及部分服務器的通信需求。更多應用層面上的問題(如TCP粘包等)這里沒有討論,或許會在以后的文章中給出。

         文章相關工程源碼請移步此處下載http://download.csdn.net/source/2852485。該源碼展示了采用非阻塞模式編程的服務器和客戶端,建立連接后,在服務器窗口輸入空格會向所有客戶端發(fā)送一條字符串消息。源碼中對網(wǎng)絡通信部分做了簡單封裝,所以代碼結構會和文中的偽代碼稍有不同。

謝謝您的閱讀!

 



本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/trcj1/archive/2010/11/23/6029163.aspx

    本站是提供個人知識管理的網(wǎng)絡存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導購買等信息,謹防詐騙。如發(fā)現(xiàn)有害或侵權內(nèi)容,請點擊一鍵舉報。
    轉藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多