天天看點

socket 程式設計 : shutdown vs close

TCP/IP 四次揮手

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

socket 程式設計 : shutdown vs close

四次揮手

這是一個很經典的示例圖,衆所周知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 信号,如果這個信号沒有被 服務端程序處理的話,預設會導緻服務端程序退出。

轉載于:https://www.cnblogs.com/cnblogs-wangzhipeng/p/10162026.html

繼續閱讀