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


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

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

在Linux中,可以再創建socket是直接將它設置為非阻塞模式

int socket (int __domain, int __type, int __protocol)

__type增加SOCK_NOBLOCK

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

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

sendrecv函數並不是直接向網絡上發送數據和接收數據

send函數是將應用層發送緩沖區的數據拷貝到內核緩沖區中

recv函數是將內核緩沖區的數據拷貝到應用緩沖區

可以用下面這張圖來描述:

image-20210706212341938

通過上圖我們可以知道,不同的程序進行網絡通信時,發送的一方會將內核緩沖區的數據通過網絡傳輸給接收方的內核緩沖區。

在應用程序A與應用程序B建立TCP連接后,假設A不斷調用send函數,會將數據不斷拷貝到對應的內核緩沖區,如果應用程序不調用recv函數,那么在應用程序B的內核緩沖區被填滿后,A的緩沖區也隨后被填滿,此時如果A繼續調用send函數會有什么后果呢?

  • socket處於阻塞模式時,繼續調用send/recv函數,程序會阻塞在send/recv調用處
  • socket處於非阻塞模式時,繼續調用send/recv函數,會返回錯誤碼
  1. socket阻塞模式下send函數的表現

    代碼來自《C++服務器開發精髓》

    服務端代碼:

    #include <sys/types.h> 
    #include <sys/socket.h>
    #include <arpa/inet.h>
    #include <unistd.h>
    #include <iostream>
    #include <string.h>
    
    int main(int argc, char* argv[])
    {
        //1.創建一個偵聽socket
        int listenfd = socket(AF_INET, SOCK_STREAM, 0);
        if (listenfd == -1)
        {
            std::cout << "create listen socket error." << std::endl;
            return -1;
        }
    
        //2.初始化服務器地址
        struct sockaddr_in bindaddr;
        bindaddr.sin_family = AF_INET;
        bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);
        bindaddr.sin_port = htons(3000);
        if (bind(listenfd, (struct sockaddr *)&bindaddr, sizeof(bindaddr)) == -1)
        {
            std::cout << "bind listen socket error." << std::endl;
    		close(listenfd);
            return -1;
        }
    
    	//3.啟動偵聽
        if (listen(listenfd, SOMAXCONN) == -1)
        {
            std::cout << "listen error." << std::endl;
    		close(listenfd);
            return -1;
        }
    
        while (true)
        {
            struct sockaddr_in clientaddr;
            socklen_t clientaddrlen = sizeof(clientaddr);
    		//4. 接受客戶端連接
            int clientfd = accept(listenfd, (struct sockaddr *)&clientaddr, &clientaddrlen);
            if (clientfd != -1)
            {         	
    			//只接受連接,不調用recv收取任何數據
    			std:: cout << "accept a client connection." << std::endl;
            }
        }
    	
    	//7.關閉偵聽socket
    	close(listenfd);
    
        return 0;
    }
    
    

    客戶端代碼:

    /**
     * 驗證阻塞模式下send函數的行為,client端
     * zhangyl 2018.12.17
     */
    #include <sys/types.h> 
    #include <sys/socket.h>
    #include <arpa/inet.h>
    #include <unistd.h>
    #include <iostream>
    #include <string.h>
    
    #define SERVER_ADDRESS "127.0.0.1"
    #define SERVER_PORT     3000
    #define SEND_DATA       "helloworld"
    
    int main(int argc, char* argv[])
    {
        //1.創建一個socket
        int clientfd = socket(AF_INET, SOCK_STREAM, 0);
        if (clientfd == -1)
        {
            std::cout << "create client socket error." << std::endl;
            return -1;
        }
    
        //2.連接服務器
        struct sockaddr_in serveraddr;
        serveraddr.sin_family = AF_INET;
        serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS);
        serveraddr.sin_port = htons(SERVER_PORT);
        if (connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) == -1)
        {
            std::cout << "connect socket error." << std::endl;
            close(clientfd);
            return -1;
        }
    
        //3. 不斷向服務器發送數據,或者出錯退出
        int count = 0;
        while (true)
        {
            int ret = send(clientfd, SEND_DATA, strlen(SEND_DATA), 0);
            if (ret != strlen(SEND_DATA))
            {
                std::cout << "send data error." << std::endl;
                break;
            } 
            else
            {
                count ++;
                std::cout << "send data successfully, count = " << count << std::endl;
            }
        }
    
        //5. 關閉socket
        close(clientfd);
    
        return 0;
    }
    
    

    先啟動server在啟動client,客戶端會不斷向服務端發送helloworld,每次發送成功后會打印計數器,運行一頓時間后,停止打印,計數器不再增加

20210706214738

當程序不再有輸出,說明阻塞在某個函數gdb看一看

(gdb) bt
#0  0x00007ffff7d03690 in __libc_send (fd=3, buf=0x555555556045, len=10, 
    flags=0) at ../sysdeps/unix/sysv/linux/send.c:28
#1  0x00005555555553bb in main (argc=1, argv=0x7fffffffdf28) at client.cpp:42
(gdb) 

果然是send函數

上面這個例子證明了如果一端一直發送數據,另一端不接收數據,內核緩沖區很快就會被填滿,發生阻塞. 其實這里所說的內核緩沖區就是TCP窗口

我們現在利用tcpdump工具查看一下這種情況下TCP窗口的大小

22:01:57.543364 IP 127.0.0.1.53382 > 127.0.0.1.3000: Flags [S], seq 1832090129, win 65495, options [mss 65495,sackOK,TS val 451488646 ecr 0,nop,wscale 7], length 0
22:01:57.543379 IP 127.0.0.1.3000 > 127.0.0.1.53382: Flags [S.], seq 1797517498, ack 1832090130, win 65483, options [mss 65495,sackOK,TS val 451488646 ecr 451488646,nop,wscale 7], length 0
22:01:57.543386 IP 127.0.0.1.53382 > 127.0.0.1.3000: Flags [.], ack 1797517499, win 512, options [nop,nop,TS val 451488646 ecr 451488646], length 0
...
22:02:11.342670 IP 127.0.0.1.3000 > 127.0.0.1.53382: Flags [.], ack 1832177322, win 0, options [nop,nop,TS val 451502445 ecr 451488936], length 0

win就是TCP窗口的大小可以看出,逐漸減小最后變為零

  1. socket非阻塞模式下send函數的表現

    就是返回一個錯誤碼,不阻塞了,略...

  2. socket阻塞模式下recv函數的表現

    阻塞了就...

  3. socket非阻塞模式下recv函數的表現

    recv在沒有數據可讀的情況下,會立即返回,返回值為-1

非阻塞模式下sendrecv函數返回值總結

返回值n 含義
大於0 成功發送或接受n字節
等於零 對方關閉連接
小於零 出錯,被信號中斷,TCP窗口太小導致數據發送不出去,或者當前網卡緩沖區已經無數據可以接受

詳細介紹:

  1. 返回值大於0

    sendrecv函數返回值大於0時,表示發送或者接收多少字節.需要注意的是,在這種情況下,**判斷send返回值是否等於要發送的字節數,而不是簡單地判斷返回值是否大於零

    int n = send(socketfd,buf,buf_length,0);
        if (n>0)
        {
            printf("send successfully");
        }
    

    很多新手就會寫出以上的代碼(比如我...)雖然返回值大於零,但由於對端TCP窗口已滿,搜易我們所期望發送的字節,並沒有全部被對方接收,所以n的大小在區間(0,buf_length)內

    解決辦法:

    • 在返回值等於buf_length時才認為正確
    • 在一個循環中調用send函數,如果一次性發送不完,記錄偏移量,接着從偏移量處發送
  2. 返回值等於0

    • 對端關閉了連接
    • 特殊情況:send函數主動發送了0字節
  3. 小於零

    出錯啦唄


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM