socket 編程 : shutdown vs close

TCP/IP 四次揮手

首先做者先描述一下TCP/IP 協議中四次揮手的過程,若是對此已經熟悉的讀者能夠跳過本節。linux

四次揮手

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

shutdown vs close

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

功能上

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

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.

主要注意的有兩點:1、一個進程中調用 close 函數會減小 fd的內核引用計數, 若是是最後一個引用 fd 的進程調用了close, 就會將fd 對應的資源完全釋放; 2、在進程中調用close 後 該fd不能夠再使用。網絡

對應於 tcp/ip socket 編程來說,若是一個 socket 在 n 個進程中使用,只有一個進程 close( socket fd) 是不會觸發 tcp/ip 的四次揮手過程。可是 在調用 close函數後, 該socket fd不能夠在該進程中被函數調用來與其餘進程通訊。less

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

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

實際狀況

從函數的介紹上,咱們能夠很清楚的看出二者的區別,那實際上二者實際上在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)等等。四、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 信號,若是這個信號沒有被 服務端進程處理的話,默認會致使服務端進程退出。

相關文章
相關標籤/搜索