1. 程式人生 > >面試官求你了,別再問我TCP的三次握手和四次揮手

面試官求你了,別再問我TCP的三次握手和四次揮手

少點程式碼,多點頭髮

本文已經收錄至我的GitHub,歡迎大家踴躍star 和 issues。

https://github.com/midou-tech/articles

三次握手建立連結,四次揮手斷開連結。這個問題算非常經典的問題,也是面試官非常喜歡問的問題。

不誇張的說,龍叔在校招面試的時候每一家公司都問到過關於三次握手和四次揮手相關的問題,相信大家也都差不多被面試官各種懟。

這個問題的重要性,已經意識到。不說廢話了,接下來就是聽龍叔給你安排的明明白白。

先畫個圖,看下TCP的建立連線 和 斷開連線的整體過程。

tcp三次握手四次揮手

看完這個圖相信聰明的你在整體對三次握手和四次揮手有了一些基本把控。但是,裡面的細節肯定是會有些生疏或者模糊的,接下來就一個一個問題的揭露本質。

在解釋之前先看點基礎知識做做鋪墊。

TCP狀態轉移解釋

狀態 描述
CLOSED 阻塞或關閉狀態,表示主機當前沒有正在傳輸或者建立的連結
LISTEN 監聽狀態,表示伺服器做好準備,等待建立傳輸連結
SYN RECV 收到第一次的傳輸請求,還未進行確認
SYN SENT 傳送完第一個SYN報文,等待收到確認
ESTABLISHED 連結正常建立之後進入資料傳輸階段
FIN WAIT1 主動傳送第一個FIN報文之後進入該狀態
FIN WAIT2 已經收到第一個FIN的確認訊號,等待對方傳送關閉請求
TIMED WAIT 完成雙向連結關閉,等待分組消失
CLOSING 雙方同時關閉請求,等待對方確認時
CLOSE WAIT 收到對方的關閉請求並進行確認進入該狀態
LAST ACK 等待最後一次確認關閉的報文

再看下TCP的報文格式

TCP報文格式

首部有20位元組的固定長度,含義如下:

  1. 源埠和目的埠

各佔2位元組,就是儲存源埠號和目的埠的

  1. 序號seq

佔4位元組,表示的範圍就是整形的範圍[0~2^32]。序號使用在給資料部分每個位元組進行編號的,編號方式是mod 2^32 。

  1. 確認號ack

佔4位元組,範圍也是無符號整數的範圍。使用在對端傳輸給我的資料最後一個位元組序號,例如A傳輸給B 101—500,此時B返回的確認號一定是小於等於501的。當B段正確接收資料之後才會返回確認號,換句話說確認號之前的資料已經全部接收。

  1. 資料偏移

佔4bit,資料偏移很多人很容易想到是不是表示資料的長度,那就錯了。偏移嘛,指的是TCP起始位置到資料部分的起始位置的偏移,也就是TCP首部的長度。

  1. 保留

佔6bit,保留欄位顧名思義,就是為今後使用,預設置為0。

  1. 緊急URG控制位

佔用1bit,URG=1,表示緊急指標有效,此時tcp資料優先傳輸。相當於生活中的緊急通道,特殊情況時使用。

在網路中也會有特殊情況,例如,傳送一個很長的程式在遠端伺服器上執行,此時發現程式有bug,需要中斷執行,因此我們從鍵盤輸入Ctrl c,假如不使用緊急資料,需要在緩衝區裡排隊,都知道是bug了,還要排隊,這怕是要出鍋啊。

此時使用緊急資料傳輸,不需要排隊,直接中斷程式是不是更符合我們的預期。

需要注意一點是,即使視窗為0時,也可以傳送緊急資料。

如何使用緊急URG控制位,在socket程式設計中send函式flag引數

send(int socket, const void *buffer, size_t length, int flags);

flags引數傳MSG_OOB巨集時,表示此時有緊急資料。MSG_OOB是個巨集,

  1. 確認ACK

佔1bit,當ACK=1時生效。TCP有條硬性規定,當建立連結成功後所有傳輸的資料報文都必須把ACK置為1。

  1. 推送PSH

佔1bit,傳送方把PSH置為1時 會立即傳送該資料包,接收方收到PSH=1的報文會立即處理交付給應用層處理。是不是感覺和URG很像,其實還是有些區別的。

  • 兩者相同點:

URG與PSH兩者都使用於緊急處理的情況,用來快速傳輸緊急資料。

  • 兩者不同點

URG置為1時,對於傳送發,“帶外資料”與正常情況下應該傳送的訊息資料一起,封裝成資料報傳送,省去了在佇列中等待的時間。 在接收方,解析報文後,獲取資料之後還是要放在快取區中,等待滿了之後在向上往應用層交付。

PSH置為1時,對於傳送方,表明這些資料不需要等向下傳送的快取區滿,立刻封裝成報文,傳送,省去了等待發送快取區到達滿的狀態的時間。 在接收方,也不需要等接受快取區滿,直接向上交付給應用層。

  1. 復位RST

佔1bit,當RST=1時,TCP會主動釋放連結,兩種情況會用上。

TCP出現嚴重差錯時,會主動釋放連線,重建連結,傳輸資料。

遇到非法報文或者拒絕連線時會把RST置為1.

  1. 同步SYN

佔1bit,同步控制位,用來在傳輸連線建立時同步傳輸連線序號。

SYN=1時,表示這是一個連線請求或連線確認報文。

SYN=1,ACK=0,表明這是一個連線請求資料段,如果對方同意建立連線,則對方會返回一個SYN=1、ACK=1的確認。

  1. FIN控制位

佔1bit,用於釋放一個傳輸連線。

FIN=1時,表示資料已全部傳輸完成,傳送端沒有資料要傳輸了,要求釋放當前連線,但是接收端仍然可以繼續接收還沒有接收完的資料。

FIN=0,正常傳輸資料。

  1. 視窗大小

佔16bit,2byte,用於表示傳送方可以接受的最大資料大小。

該視窗是動態變化的,用作流量控制時使用。

  1. 檢驗和

佔16bit,2byte,用於對TCP頭部,偽頭部,資料三個部分進行校驗。

  1. 緊急指標

佔16bit,2byte,用於記錄緊急資料的末尾在資料段中的位置。

當URG=1時,該指標才生效。

  1. 可選項

可選項最長可達40byte,是可選的,可以沒有。當可選項不存在時,TCP頭部長度為20byte。

可選項可以包括視窗縮放選項(Window ScaleOption,WSopt)、MSS(最大資料段大小)選項、SACK(選擇性確認)選項、時間戳(Timestamp)選項等。

  1. 資料

TCP資料部分,由應用層應用程式提交的資料。

TCP頭部是基礎知識,必須瞭解才能更好的理解TCP資料如何封裝和傳輸,以及在建立連結和斷開連結時都在操作那些地方。

三次握手建立連線

三次握手如何建立連線?

三次握手建立連結

從圖中可以清楚的看到,三次握手的過程,我在在把過程清楚的解釋一遍,順便說下每個過程容易被問到的知識點。

採用C/S模式解釋,假設C端發起傳輸請求。

在傳送建立連結請求之前,C端是保持CLOSED狀態,S端最開始也是處於CLOSED狀態,當執行listen函式套接字進入被動監聽狀態。

所謂被動監聽,是指當沒有客戶端請求時,套接字處於“睡眠”狀態,只有當接收到客戶端請求時,套接字才會被“喚醒”來響應請求。

第一次:C端傳送SYN=1的請求報文,此時C端進入SYN SENT狀態,等待伺服器確認。

此時如果報文丟失傳送不到對端會如何?

C端傳送報文之後會啟動一個定時器,在超時之後未收到S端的確認,會再次傳送SYN請求,每次嘗試的時間會是第一次的二倍,如果總的總嘗試時間為75秒,此次建立連結失敗。

第二次:S端收到C端傳送的SYN報文(建立連結請求)後,S端必須返回確認號並且同時傳送一條SYN報文,此時進入SYN RCVD狀態。

為啥要連帶傳送SYN報文?

TCP是全雙工通訊,協議規定當收到建立連結請求後必須返回序列號,同時建立本端到對端的通訊連結。這也叫做捎帶應答機制。

如果第二次報文丟失怎麼辦?

在傳送完ACK+SYN報文後會啟動一個定時器,超時沒有收到ACK確認,會再次傳送,會進行多次重試。超時時間依舊每次翻倍,重試次數可設定。

修改 /proc/sys/net/ipv4/tcp_synack_retries 的值

第三次:C端收到S端發的ACK+SYN報文,需要返回一個應答ACK的報文,此時該連線會進入半連線狀態的佇列,當S端收到ACK後,一條完整的全雙工TCP連結建立完成,雙方進入ESTABLISHED狀態。

這裡有個常用攻擊手段,攻擊者偽造一個SYN請求傳送給服務端,服務端響應之後,會收不到C端的ACK確認,服務端會不斷的重試,預設會重試五次。

此時服務端會維持這個連結的所有資源,如果有大量這樣的請求,服務端的資源會被耗完。

這就是DOS攻擊。

如果第三次報文丟失怎麼辦?

S端在發出ACK+SYN報文後會啟動一個定時器,在超時觸發還沒收到ACK就確認是丟失了,會重試一次傳送。

這裡面的每個狀態都必須搞明白,面試官也超級愛問上面的狀態轉移。

龍叔還遇到過一個面試官問我用過socket程式設計麼?問我用過哪些socket函式?

C端socket程式設計程式碼

//C端
#define PORT  8080
#define BUFFER_SIZE 1024
int main(int argc, char **argv)
{
    //定義IPV4的TCP連線的套接字描述符
    int sock_cli = socket(AF_INET,SOCK_STREAM, 0);
    //定義sockaddr_in
    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = inet_addr(argv[1]);
    servaddr.sin_port = htons(PORT);  
 
    //連線伺服器,成功返回0,錯誤返回-1
    int ret = connect(sock_cli, (struct sockaddr *)&servaddr, sizeof(servaddr));
 
    //客戶端將控制檯輸入的資訊傳送給伺服器端,伺服器原樣返回資訊,阻塞
    while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
    {   
        ret=send(sock_cli, sendbuf, strlen(sendbuf),0); ///傳送
        recv(sock_cli, recvbuf, sizeof(recvbuf),0); ///接收
        fputs(recvbuf, stdout);
    }
 
    close(sock_cli); // 關閉連線
    return 0;
}

S端socket程式設計程式碼

int main(int argc, char **argv)
{
    //定義IPV4的TCP連線的套接字描述符
    int server_sockfd = socket(AF_INET,SOCK_STREAM, 0);
    //定義sockaddr_in
    struct sockaddr_in server_sockaddr;
    server_sockaddr.sin_family = AF_INET;
    server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_sockaddr.sin_port = htons(PORT);
 
    //bind成功返回0,出錯返回-1
    if(bind(server_sockfd,(struct sockaddr *)&server_sockaddr,sizeof(server_sockaddr))==-1)
 
    //listen成功返回0,出錯返回-1,允許同時監聽的連線數為QUEUE_SIZE
    if(listen(server_sockfd,QUEUE_SIZE) == -1)
 
    for(;;)
    {
        struct sockaddr_in client_addr;
        socklen_t length = sizeof(client_addr);
        //程序阻塞在accept上,成功返回非負描述字,出錯返回-1
        int conn = accept(server_sockfd, (struct sockaddr*)&client_addr,&length);
 
        //處理資料部分
      ...
    }
 
    close(server_sockfd);
    return 0;
}

為什麼需要三次握手建立連結,2次可以麼,4次行不行?

這問題問的,面試官是咋了?在這明知故問的,整些有的沒的。肯定是不行啊,RFC 標準就是這樣寫的啊。

可不敢這樣回答啊,標準是說的三次握手建立連結,可沒說四次不行啊。要是這樣答,妥妥的會收到,同學我們今天的面試到此基本結束了,你回家等訊息...

龍叔來說說這個問題,為什麼不能兩次?

如果第二次不傳送SYN+ACK,只是傳送確認應答訊息ACK,會造成只能建立單向通訊,而且不能應答。而TCP是全雙工通訊的,而且必須保證可靠性。

如果第二傳送SYN+ACK,不用應答。此時會出現三種情況

一、二次握手失敗,C端會重複傳送SYN報文,等待對端傳送確認報文,S端會儲存tcp連線的所有資源,大量的這種情況會導致S資源耗盡。

二、二次握手成功,S收不到ACK會重複傳送SYN+ACK報文。

三、二次握手完以後,雙方以為連線建立成功,即可開始通訊。假如此時連線並沒有真的建立成功,S端開始傳送訊息,會造成網路擁堵發生。

為什麼不能是四次?

四次其實原則上來說是可以的,就是把第二次的ACK和SYN分兩次傳送。在理論上是完全可以行得通的,但是TCP本著節約網路網路資源的前提。

還有一種是不拆開二次握手的捎帶應答,三次握手之後C端繼續傳送SYN報文,其時這是徒勞的。第三次完成以後連結已經建立,後面無論多少次都是徒勞。

如果雙方同時建立連線,會發生什麼情況?

TCP同時建立連結

這就是雙方同時建立連結的情況,情況還不錯,反正能建立成功,這點是肯定的。但是要注意兩點

第一、此時只會建立一條全雙工的TCP連結,不是兩條。

第二、雙方沒有CS之分,兩端都是同時承擔兩個角色,客戶端和伺服器。

四次揮手斷開連結

先整個圖看下四次揮手的整個過程和狀態轉移。狀態轉移會考看仔細點。

四次揮手斷開連結

依舊採用C/S模式解釋此過程。

第一次:當C端的應用程式結束資料傳輸是,會向S端傳送一個帶有FIN附加標記的報文段(FIN表示英文finish),此時C端進入FIN_WAIT1狀態,C端不能在傳送資料到S端。

第二次:S端收到FIN報文會響應一個ACK報文,S端進入CLOSE_WAIT狀態。進入此狀態後S端把剩餘未傳送的資料傳送到C端,C端收到S端的ACK之後,進入FIN_WAIT2狀態。

同時繼續接受S端傳輸的其他資料包。

第三次:S端處理完自己待發送的資料之後,也會發送FIN斷開連結的請求,S端進入LAST_ACK狀態。

第四次:C端收到S端的斷開連結請求後會啟動一個定時器,該定時器時長是2MSL(最大段報文生存時間),同時傳送最後一次ACK報文。

為什麼要四次揮手?

TCP是全雙工的通訊機制,每個方向必須單獨進行關閉。

TCP傳輸連線關閉的原則如下:

當一端完成它的資料傳送任務後就可以傳送一個FIN欄位置1的資料段來終止這個方向的資料傳送;當另一端收到這個FIN資料段後,必須通知它的應用層 對端已經終止了那個方向的資料傳送。

為什麼不能用三次握手中捎帶應答機制減少一次握手?

這點到是很迷惑人,但是掌握了TCP傳輸的一些細節就會發現並不難。

TCP是全雙工通訊的,S收到斷開連結請求後只是表示C端不會傳輸資料到S端了,但是並不表示S端不傳輸資料到C端。

如果採用捎帶應答,S端將無法把剩餘的資料傳輸到C端。

為何最後一次ACK之後需要等待2MSL的時間?

網路是不可靠的,TCP是可靠協議,必須保證最後一次報文送達之後才能斷開連結,否則會再次收到S端的FIN報文資訊。

而等待2MSL時間就是為了保證最後最後一次報文丟失時還能重新發送。

為何是2MSL的時間?

2MSL是報文一個往返的最長時間,假設小於這個時間會發生,ACK丟了,但是還沒接收到對方重傳的FIN我方就重新發送了ACK。

如果已經建立了連線,但是客戶端突然出現故障了怎麼辦?

這個不難TCP自己做了保證,TCP預設有個定時器,每次收到客戶端的請求後會把定時器設定好,通常設定兩小時,超過兩小時還沒收到資料。

服務端會發送一個探測報文,以後每隔75秒鐘傳送一次。若一連發送10個探測報文仍然沒反應,伺服器就認為客戶端出了故障,接著就關閉連線。

總結

三次握手和四次揮手的知識基本告一段落了,就講到這裡了,如果有什麼不明白的地方可以加我微信探討。

後面還會出一篇網路程式設計常用的linux命令列工具,比如ping、tcpdump、netstat、nc等等,在出一篇計算機網路的總結文章。計算機網路這部分基本完結了,如果有不懂得可以看看公號裡面前面的文章。