socket 編程 : shutdown vs close


TCP/IP 四次揮手

首先作者先描述一下TCP/IP 協議中四次揮手的過程,如果對此已經熟悉的讀者可以跳過本節。

四次揮手

四次揮手
這是一個很經典的示例圖,眾所周知tcp socket 在一個生命周期中有很多個狀態,讀者可以使用ss命令查看,其中在斷開連接的時候 client端 會經歷如下三個狀態:FIN_WAIT1、FIN_WAIT2、TIME_WAIT 直到CLOSED, 而server端會經歷CLOSE_WAIT、LAST_ACK 直到CLOSED。

shutdown vs close

在linux c++ 網絡編程中 socket的關閉有兩個常用的函數 close 和 shutdown兩個函數。作者今天討論一下在tcp/ip 協議中這兩個函數有什么不同。

功能上

linux有一個特點:file、 socket、 dev 都會通過一個 file description (文件描述符)標識,都抽象成IO操作。 對於close 函數來講,socket 的 fd 與其他fd 描述符沒啥區別。下面給出 close 函數的描述

close() closes a file descriptor, so that it no longer refers to any
file and may be reused.  Any record locks (see fcntl(2)) held on the
file it was associated with, and owned by the process, are removed
(regardless of the file descriptor that was used to obtain the lock).

If fd is the last file descriptor referring to the underlying open
file description (see open(2)), the resources associated with the
open file description are freed; if the file descriptor was the last
reference to a file which has been removed using unlink(2), the file
is deleted.

主要注意的有兩點:一、一個進程中調用 close 函數會減少 fd的內核引用計數, 如果是最后一個引用 fd 的進程調用了close, 就會將fd 對應的資源徹底釋放; 二、在進程中調用close 后 該fd不可以再使用。

對應於 tcp/ip socket 編程來講,如果一個 socket 在 n 個進程中使用,只有一個進程 close( socket fd) 是不會觸發 tcp/ip 的四次揮手過程。但是 在調用 close函數后, 該socket fd不可以在該進程中被函數調用來與其他進程通信。

tcp/ip 是一個全雙工的面向鏈接的通信協議,一個tcp/ip socket可以同時用於收取和發送信息, 那么就可能存在如下的場景: 進程不再需要讀取數據 但仍然需要接受數據 或者 相反的情況。shutdown() 函數就具有這種能力,shutdown()函數描述如下:

The shutdown() call causes all or part of a full-duplex connection on
the socket associated with sockfd to be shut down.  If how is
SHUT_RD, further receptions will be disallowed.  If how is SHUT_WR,
further transmissions will be disallowed.  If how is SHUT_RDWR,
further receptions and transmissions will be disallowed.

在調用函數的時候可以設置關閉的模式:SHUT_RD 關閉讀取、 SHUT_WR 關閉寫入、 SHUT_RDWR 完全關閉。

實際情況

從函數的介紹上,我們可以很清楚的看出兩者的區別,那實際上兩者實際上在tcp/ip 協議中會觸發怎樣操作?? 作者做了一個簡單的實驗:
通過 tcpdump 抓取 close函數、以及shutdown的三種關閉模式的網絡包,分析其在底層網絡上的行為。

下面貼出測試代碼的主函數(可以忽略代碼部分,直接看實際的實驗結果):

    
int main(int argc, char const *argv[])
{
    /* code */
    if (argc < 2) {
        printf("must select: 1:server or  2: client.\n");
        return -1;
    }

    int type = atoi(argv[1]);
    CTcp* s = new CTcp();
    if(type == 1 ){
        s->registryCallBackMethod((void*)recv_cb_ch, NULL, CBase::READ); 
        s->registryCallBackMethod((void*)close_cb, NULL, CBase::CLOSE);    
   
        s->bindAddress("127.0.0.1", 9906);
        s->startServer();
        std::cin.get();
    }else{
        s->connect("127.0.0.1", 9906, 5);
        s->sendMessage("hello", sizeof("hello"));
        printf("must input the test type:\n 1: close 2: shutdown: \n");
        scanf("%d", &type);
        int client = s->socketClient();
        switch(type){
        case 1:{
            close(client); 
            std::cin.get();
        }
        break;
        case 2:{
            printf("please input shutdown type:\n 1: read, 2: write, 3: all\n");
            scanf("%d", &type);
            if (type == 1){
                shutdown(client, SHUT_RD);
            }else if(type == 2){
                shutdown(client, SHUT_WR);
            }else{
                shutdown(client, SHUT_RDWR);
            }
            
            std::cin.get();
            std::cin.get();

        }
        break;
        default:
            printf("the type is not support %d\n", type);
        }
    }

    delete s;
    s = NULL;
    return 0;
}

作者的實驗環境是在 centos 系統的雲主機中,調用的socket函數為標准庫函數,下面貼出實驗過程和結果:

setup1. 啟動服務端

    $ ./shutdown 1
        the max is 3
        the server time is out

setup2. 啟動tcpdump監聽

    $sudo tcpdump -i lo -vv 
        tcpdump: listening on lo, link-type EN10MB (Ethernet), capture size 262144 bytes

因為服務端綁定的是 127.0.0.1地址,所以在tcpdump 中指定了 lo (本地回環網卡)。

setup3. 啟動客戶端

    $./shutdown 2
        connect .....
        connect is success!
        must input the test type:
        1: close 2: shutdown: 

客戶端在鏈接后會自動發送一個'hello' 消息給服務端, 此時tcpdump抓取到如下的數據包:

09:17:41.773070 IP (tos 0x0, ttl 64, id 17657, offset 0, flags [DF], proto TCP (6), length 60)
localhost.41894 > localhost.9906: Flags [S], cksum 0xfe30 (incorrect -> 0x3df3), seq 967462950, win 43690, options [mss 65495,sackOK,TS val 2188873883 ecr 0,nop,wscale 7], length 0
09:17:41.773098 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 60)
localhost.9906 > localhost.41894: Flags [S.], cksum 0xfe30 (incorrect -> 0xc0f0), seq 989081322, ack 967462951, win 43690, options [mss 65495,sackOK,TS val 2188873883 ecr 2188873883,nop,wscale 7], length 0
09:17:41.773124 IP (tos 0x0, ttl 64, id 17658, offset 0, flags [DF], proto TCP (6), length 52)
localhost.41894 > localhost.9906: Flags [.], cksum 0xfe28 (incorrect -> 0x9335), seq 1, ack 1, win 342, options [nop,nop,TS val 2188873883 ecr 2188873883], length 0
09:17:41.773168 IP (tos 0x0, ttl 64, id 17659, offset 0, flags [DF], proto TCP (6), length 58)
localhost.41894 > localhost.9906: Flags [P.], cksum 0xfe2e (incorrect -> 0x4f55), seq 1:7, ack 1, win 342, options [nop,nop,TS val 2188873883 ecr 2188873883], length 6
09:17:41.773177 IP (tos 0x0, ttl 64, id 14859, offset 0, flags [DF], proto TCP (6), length 52)
localhost.9906 > localhost.41894: Flags [.], cksum 0xfe28 (incorrect -> 0x932f), seq 1, ack 7, win 342, options [nop,nop,TS val 2188873883 ecr 2188873883], length 0

其中 9906 端口是服務端端口, 41894 端口是客戶端端口。前三個報文中雙方完成了三次握手,同步了報文首地址偏移量(seq)、窗口大小(win)、報文最大存活時間(mss)等等。4、5報文 完成了'hello' 消息的發送和應答,客戶端的報文偏移量 seq = sizeof('hello') + 1 = 7。

setup4. 開始測試

  • close 測試:

        $ ./shutdown 2
            connect .....
            connect is success!
            must input the test type:
            1: close 2: shutdown: 
            1
    

    tcpdump 抓取報文顯示:

      09:32:59.182336 IP (tos 0x0, ttl 64, id 1672, offset 0, flags [DF], proto TCP (6), length 52)
      localhost.41896 > localhost.9906: Flags [F.], cksum 0xfe28 (incorrect -> 0x5aca), seq 7, ack 1, win 342, options [nop,nop,TS val 2189791308 ecr 2189785847], length 0
      09:32:59.223130 IP (tos 0x0, ttl 64, id 48256, offset 0, flags [DF], proto TCP (6), length 52)
      localhost.9906 > localhost.41896: Flags [.], cksum 0xfe28 (incorrect -> 0x454c), seq 1, ack 8, win 342, options [nop,nop,TS val 2189791349 ecr 2189791308], length 0
      09:33:02.183021 IP (tos 0x0, ttl 64, id 48257, offset 0, flags [DF], proto TCP (6), length 52)
      localhost.9906 > localhost.41896: Flags [F.], cksum 0xfe28 (incorrect -> 0x39bc), seq 1, ack 8, win 342, options [nop,nop,TS val 2189794308 ecr 2189791308], length 0
      09:33:02.183053 IP (tos 0x0, ttl 64, id 24386, offset 0, flags [DF], proto TCP (6), length 52)
      localhost.41896 > localhost.9906: Flags [.], cksum 0x2e03 (correct), seq 8, ack 2, win 342, options [nop,nop,TS val 2189794309 ecr 2189794308], length 0
    

    可見close觸發了tcp/ip的四次揮手, 在雙方互相發送FIN 消息並確認后結束了socket鏈接。

  • shutdown + SHUT_RD 測試:

        ./shutdown 2
            connect .....
            connect is success!
            must input the test type:
            1: close 2: shutdown: 
            2
            please input shutdown type:
            1: read, 2: write, 3: all
            1
    

    此時查看tcpdump的抓取記錄會發現沒有任何新增的數據包,這說明在此種情況下客戶端並未發送任何報文給服務端。

  • shutdown + SHUT_WR 測試:

        ./shutdown 2
            connect .....
            connect is success!
            must input the test type:
            1: close 2: shutdown: 
            2
            please input shutdown type:
            1: read, 2: write, 3: all
            2
    

    tcpdump 抓取報文顯示:

      localhost.41900 > localhost.9906: Flags [F.], cksum 0xfe28 (incorrect -> 0x173a), seq 7, ack 1, win 342, options [nop,nop,TS val 2190212694 ecr 2190205129], length 0
      09:40:00.602136 IP (tos 0x0, ttl 64, id 5571, offset 0, flags [DF], proto TCP (6), length 52)
      localhost.9906 > localhost.41900: Flags [.], cksum 0xfe28 (incorrect -> 0xf983), seq 1, ack 8, win 342, options [nop,nop,TS val 2190212735 ecr 2190212694], length 0
      09:40:03.561641 IP (tos 0x0, ttl 64, id 5572, offset 0, flags [DF], proto TCP (6), length 52)
      localhost.9906 > localhost.41900: Flags [F.], cksum 0xfe28 (incorrect -> 0xedf3), seq 1, ack 8, win 342, options [nop,nop,TS val 2190215694 ecr 2190212694], length 0
      09:40:03.561661 IP (tos 0x0, ttl 64, id 30067, offset 0, flags [DF], proto TCP (6), length 52)
      localhost.41900 > localhost.9906: Flags [.], cksum 0xfe28 (incorrect -> 0xe23b), seq 8, ack 2, win 342, options [nop,nop,TS val 2190215694 ecr 2190215694], length 0
    

    可以看出它觸發了tcp/ip四次揮手的操作。

  • shutdown + SHUT_RDWR 測試:

    它也會觸發四次揮手操作。

總結

簡單的總結一下如上的測試:

operator send FIN
close yes
shutdown SHUTRD no
shutdown SHUTWR yes
shutdown SHUTRDWR yes

值得一提的是,在client端調用close() 函數后,如果server 端沒有調用 close()函數,四次揮手就會無法完成。此時client端 socket 會進入 TIME_WAIT 狀態,直到時間耗盡才會回收socket分配的資源,而server端在此后繼續發送消息會觸發 SINGLE_PIPE 信號,如果這個信號沒有被 服務端進程處理的話,默認會導致服務端進程退出。


免責聲明!

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



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