socket的阻塞模式和非阻塞模式

如何將socket設置爲非阻塞模式

無論是Windows還是Linux,默認創建的socket都是阻塞模式的。

在linux上,我們可以使用fcntl函數或者ioctl函數給創建的socket增加O_NONBLOCK標誌來將socket設置爲非阻塞模式。

int oldSocketFlag = fcntl(sockfd, F_GETFL, 0);
int newSocketFlag = oldSocketFlag | O_NONBLOCK;
fcntl(sockfd, F_SETFL, newSocketFlag);

Linux上的socket函數也可以直接在創建時將socket設置爲非阻塞式,socket函數簽名如下:

int socket(int domain, int type, int protocol);

type參數增加一個SOCK_NONBLOCK標誌即可,例如:

int s = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, IPPROTO_TCP);

不僅如此,在Linux上利用accept函數返回的代表與客戶端通信的socket也提供了一個擴展函數accept4,直接將accept函數返回的socket設置爲非阻塞的;

int accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen);
int accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen, int flags);

只要將accept4函數的最後一個參數flags設置爲SOCK_NONBLOCK即可。

以下代碼等價。

socklen_t addrlen = sizeof(clientaddr);
int clientfd = accept4(listenfd, &clientaddr, &addrlen, SOCK_NONBLOCK);

socklen_t addrlen = sizeof(clientaddr);
int clientfd = accept(listenfd,&clientaddr,&addrlen);
if(clientfd != -1)
{
    int oldSocketFlag = fcntl(clientfd, F_GETFL, 0);
    int newSocketFlag = oldSocketFlag | O_NONBLOCK;
    fcntl(clientfd, F_SETFL, newSocketFlag);
}

send和recv函數在阻塞和非阻塞模式下的表現

send函數在本質上並不是向網絡上發送數據,而是將應用層發送緩衝區的數據拷貝到內核緩衝區中,至於數據什麼時候會從網卡緩衝區中真正地發到網絡中,要根據TCP/IP協議棧的行爲來確定。如果socket設置了TCP_NODELAY選項(即禁用nagel算法)。存放到內核緩衝區的數據就會被立即發送出去,反之,一次放入內核緩衝區的數據包如果太小,則系統會在多個小的數據包湊成一個足夠大的數據包之後纔會將數據發送出去。

recv函數在本質上並不是從網絡上收取數據,而是將內核緩衝區中的數據拷貝到應用程序的緩衝區中。在拷貝完成後會將內核緩衝區中的該部分數據移除。

 

 

通過上圖可以知道,不同的程序在進行網絡通信時,發送的一方會將內核緩衝區的數據通過網絡傳輸給接收方的內核緩衝區。在應用程序A與應用程序B建立連接之後,假設應用程序A不斷調用send函數,則數據會不斷拷貝至對應的內核緩衝區中,如果應用程序B一直不調用recv函數,那麼在應用程序B的內核緩衝區被填滿之後,應用程序A的內核緩衝區也會被填滿,此時應用程序A繼續調用send函數會發生什麼結果?具體的結果取決於socket是否是阻塞模式,這裏先給出結論。

  1. socket是阻塞模式時,繼續調用send/recv函數,程序會阻塞在send/recv調用處;
  2. socket是非阻塞模式時,繼續調用send/recv函數,send/recv不會阻塞程序執行流,而是立即出錯返回,我們會得到一個相關的錯誤碼,在Linux上該錯誤碼爲EWOULDBLOCKEAGAIN

非阻塞模式下send和recv函數的返回值總結

返回值n返回值的含義
大於0 成功發送send或接收recvn字節
0 對端關閉連接
小於0 出錯,被信號中斷,對端TCP窗口太小導致數據發送不出去或者當前網卡緩衝區已無數據可接收

這三種情況:

  1. 返回值大於0。當send/recv函數的返回值大於0時,表示發送或接收多少字節。需要注意的是,在這種情況下,我們一定要判斷send函數的返回值是不是我們期望發送的字節數,而不是簡單判斷其返回值大於0。舉個例子
    int n = send(socket, buf, buf_length, 0);
    if(n > 0)
    {
        printf("send data successful");
    }

    雖然返回值n大於0,但在實際情況中,由於對端的TCP窗口可能因爲缺少一部分字節就滿了,所以n的值可能爲 ( 0 , b u f _ l e n g t h ] (0, buf\_length] (0,buf_length]。當 0 < n < b u f _ l e n g t h 0 < n < buf\_length 0<n<buf_length​​時,雖然此時send函數調用成功,但在業務上並不算正確,因爲有部分數據並沒有被髮送出去。我們可能在一次測試中測不出n小於buf_length的情況,但不代表實際上不存在​​。所以,建議要麼在返回值n等於buf_length時才認爲正確,要麼在一個循環中調用send函數,如果數據一次性發送不完,則記錄偏移量,下一次從偏移量處接着發送,直到全部發送完爲止。

    //不推薦的方式
    int n = send(socket, buf, buf_length, 0);
    if(n == buf_length)
    {
        printf("send data successfully\n");
    }
    
    //推薦的方式
    bool SendData(const char* buf, int buf_length)
    {
        //已經發送的字節數
        int sent_bytes = 0;
        int ret = 0;
        while(true)
        {
            ret = send(m_hSocket, buf + sent_bytes, buf_length - sent_bytes, 0);
            if(ret == -1)
            {
                if(errno == EWOULDBLOCK)
                {
                    //嚴謹的做法:如果發送不出去,則應該緩存尚未發送出去的數據
                    break;
                }
                else if(errno == EINTR)
                    continue;
                else
                    return false;
            }
            else if(ret == 0)
                return false;
            sent_bytes += ret;
            if(sent_bytes == buf_length)
                break;
        }
        return true;
    }

     

    1. 返回值等於0。在通常情況下,如果sendrecv函數返回0,我們就認爲對端關閉了連接,我們這端也關閉連接即可。send函數主動發送0字節時也會返回0,這是一種特例。
    2. 返回值小於0。對於sendrecv函數返回值小於0的情況(即返回-1),此時並不表示send或者recv函數一定調用出錯。

    下表表示的是非阻塞模式下,socketsendrecv返回值,對於阻塞模式下的socket,如果返回值爲-1,則一定表示出錯。

    返回值和錯誤碼send函數recv函數
    返回-1,錯誤碼是EWOULDBLOCK或EAGAIN TCP窗口太小,數據暫時發送不出去 在當前緩衝區中無可讀數據
    返回-1,錯誤碼是EINTR 被信號中斷,需要重試 被信號中斷,需要重試
    返回-1,錯誤碼不是以上3種 出錯 出錯

    阻塞與非阻塞socket的各自使用場景

    阻塞的socket函數在調用sendrecvconnectaccept等函數時,如果特定的條件不滿足,就會阻塞其調用線程直至超時,非阻塞的socket恰恰相反。

    非阻塞模式一般用於需要支持高併發多QPS的場景(如服務器程序),但是正如前文所述,這種模式讓程序的執行流和控制邏輯變得複雜;相反,阻塞模式邏輯簡單,程序結構簡單明瞭,常用於一些特殊場景中。

    應用場景一:某程序需要臨時發送一個文件,文件分段發送,每發送一段,對端都會給予一個響應,該程序可以單獨開一個任務線程,在這個任務線程函數裏面,使用先sendrecvsendrecv的模式,每次sendrecv都是阻塞模式的。

    應用場景二:A端與B端之間的通信只有問答模式,即A端每發送給B端一個請求,B端比定會給A端一個響應,除此之外,B端不會向A端推送任何數據,此時A端就可以採用阻塞模式,在每次send完請求後,都可以直接使用阻塞式的recv函數接收應答包。

    發送給B端一個請求,B端比定會給A端一個響應,除此之外,B端不會向A端推送任何數據,此時A端就可以採用阻塞模式,在每次send完請求後,都可以直接使用阻塞式的recv函數接收應答包。

     

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章