天天看點

djano 字段不重複_硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題...

djano 字段不重複_硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題...

  腳本之家

你與百萬開發者在一起

djano 字段不重複_硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題...
djano 字段不重複_硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題...
djano 字段不重複_硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題...

本文經授權轉自公衆号 小林coding (ID:CodingLin)

如若轉載請聯系原公衆号

前言

不管面試 Java 、C/C++、Python 等開發崗位,

TCP

的知識點可以說是的必問的了。

任 TCP 虐我千百遍,我仍待 TCP 如初戀。

遙想小林當年校招時常因

TCP

面試題被刷,真是又愛又狠….

過去不會沒關系,今天就讓我們來消除這份恐懼,微笑着勇敢的面對它吧!

是以小林整理了關于TCP 三次握手和四次揮手的面試題型,跟大家一起探讨探讨。

  1. TCP 基本認識
djano 字段不重複_硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題...
  1. TCP 連接配接建立
djano 字段不重複_硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題...
  1. TCP 連接配接斷開
djano 字段不重複_硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題...
  1. Socket 程式設計
djano 字段不重複_硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題...
PS:本次文章不涉及 TCP 流量控制、擁塞控制、可靠性傳輸等方面知識,這些留在下篇哈!

正文

01 TCP 基本認識

瞧瞧 TCP 頭格式

我們先來看看 TCP 頭的格式,标注顔色的表示與本文關聯比較大的字段,其他字段不做詳細闡述。

djano 字段不重複_硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題...

TCP 頭格式

序列号:在建立連接配接時由計算機生成的随機數作為其初始值,通過 SYN 包傳給接收端主機,每發送一次資料,就「累加」一次該「資料位元組數」的大小。用來解決網絡包亂序問題。

确認應答号:指下一次「期望」收到的資料的序列号,發送端收到這個确認應答以後可以認為在這個序号以前的資料都已經被正常接收。用來解決不丢包的問題。

控制位:

  • ACK:該位為

    1

    時,「确認應答」的字段變為有效,TCP 規定除了最初建立連接配接時的

    SYN

    包之外該位必須設定為

    1

  • RST:該位為

    1

    時,表示 TCP 連接配接中出現異常必須強制斷開連接配接。
  • SYC:該位為

    1

    時,表示希望建立連,并在其「序列号」的字段進行序列号初始值的設定。
  • FIN:該位為

    1

    時,表示今後不會再有資料發送,希望斷開連接配接。當通信結束希望斷開連接配接時,通信雙方的主機之間就可以互相交換

    FIN

    位置為 1 的 TCP 段。
為什麼需要 TCP 協定?TCP 工作在哪一層?

IP

層是「不可靠」的,它不保證網絡包的傳遞、不保證網絡包的按序傳遞、也不保證網絡包中的資料的完整性。

djano 字段不重複_硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題...

OSI 參考模型與 TCP/IP 的關系

如果需要保障網絡資料包的可靠性,那麼就需要由上層(傳輸層)的

TCP

協定來負責。

因為 TCP 是一個工作在傳輸層的可靠資料傳輸的服務,它能確定接收端接收的網絡包是無損壞、無間隔、非備援和按序的。

什麼是 TCP ?

TCP 是面向連接配接的、可靠的、基于位元組流的傳輸層通信協定。

djano 字段不重複_硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題...
  • 面向連接配接:一定是「一對一」才能連接配接,不能像 UDP 協定 可以一個主機同時向多個主機發送消息,也就是一對多是無法做到的;
  • 可靠的:無論的網絡鍊路中出現了怎樣的鍊路變化,TCP 都可以保證一個封包一定能夠到達接收端;
  • 位元組流:消息是「沒有邊界」的,是以無論我們消息有多大都可以進行傳輸。并且消息是「有序的」,當「前一個」消息沒有收到的時候,即使它先收到了後面的位元組已經收到,那麼也不能扔給應用層去處理,同時對「重複」的封包會自動丢棄。
什麼是 TCP 連接配接?

我們來看看 RFC 793 是如何定義「連接配接」的:

Connections: 

The reliability and flow control mechanisms described above requirethat TCPs initialize and maintain certain status information foreach data stream.  

The combination of this information, includingsockets, sequence numbers, and window sizes, is called a connection.

簡單來說就是,用于保證可靠性和流量控制維護的某些狀态資訊,這些資訊的組合,包括Socket、序列号和視窗大小稱為連接配接。

djano 字段不重複_硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題...

是以我們可以知道,建立一個 TCP 連接配接是需要用戶端與伺服器端達成上述三個資訊的共識。

  • Socket:由 IP 位址和端口号組成
  • 序列号:用來解決亂序問題等
  • 視窗大小:用來做流量控制
如何唯一确定一個 TCP 連接配接呢?

TCP 四元組可以唯一的确定一個連接配接,四元組包括如下:

  • 源位址
  • 源端口
  • 目的位址
  • 目的端口
djano 字段不重複_硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題...

TCP 四元組

源位址和目的位址的字段(32位)是在 IP 頭部中,作用是通過 IP 協定發送封包給對方主機。

源端口和目的端口的字段(16位)是在 TCP 頭部中,作用是告訴 TCP 協定應該把封包發給哪個程序。

有一個 IP 的伺服器監聽了一個端口,它的 TCP 的最大連接配接數是多少?

伺服器通常固定在某個本地端口上監聽,等待用戶端的連接配接請求。

是以,用戶端 IP 和 端口是可變的,其理論值計算公式如下:

djano 字段不重複_硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題...

對 IPv4,用戶端的 IP 數最多為

2

32

次方,用戶端的端口數最多為

2

16

次方,也就是服務端單機最大 TCP 連接配接數,約為

2

48

次方。

當然,服務端最大并發 TCP 連接配接數遠不能達到理論上限。

  • 首先主要是檔案描述符限制,Socket 都是檔案,是以首先要通過

    ulimit

    配置檔案描述符的數目;
  • 另一個是記憶體限制,每個 TCP 連接配接都要占用一定記憶體,作業系統是有限的。
UDP 和 TCP 有什麼差別呢?分别的應用場景是?

UDP 不提供複雜的控制機制,利用 IP 提供面向「無連接配接」的通信服務。

UDP 協定真的非常簡,頭部隻有

8

個位元組( 64 位),UDP 的頭部格式如下:

djano 字段不重複_硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題...

UDP 頭部格式

  • 目标和源端口:主要是告訴 UDP 協定應該把封包發給哪個程序。
  • 包長度:該字段儲存了 UDP 首部的長度跟資料的長度之和。
  • 校驗和:校驗和是為了提供可靠的 UDP 首部和資料而設計。

TCP 和 UDP 差別:

1. 連接配接

  • TCP 是面向連接配接的傳輸層協定,傳輸資料前先要建立連接配接。
  • UDP 是不需要連接配接,即刻傳輸資料。

2. 服務對象

  • TCP 是一對一的兩點服務,即一條連接配接隻有兩個端點。
  • UDP 支援一對一、一對多、多對多的互動通信

3. 可靠性

  • TCP 是可靠傳遞資料的,資料可以無差錯、不丢失、不重複、按需到達。
  • UDP 是盡最大努力傳遞,不保證可靠傳遞資料。

4. 擁塞控制、流量控制

  • TCP 有擁塞控制和流量控制機制,保證資料傳輸的安全性。
  • UDP 則沒有,即使網絡非常擁堵了,也不會影響 UDP 的發送速率。

5. 首部開銷

  • TCP 首部長度較長,會有一定的開銷,首部在沒有使用「選項」字段時是

    20

    個位元組,如果使用了「選項」字段則會變長的。
  • UDP 首部隻有 8 個位元組,并且是固定不變的,開銷較小。

TCP 和 UDP 應用場景:

由于 TCP 是面向連接配接,能保證資料的可靠性傳遞,是以經常用于:

  • FTP

    檔案傳輸
  • HTTP

    /

    HTTPS

由于 UDP 面向無連接配接,它可以随時發送資料,再加上UDP本身的處理既簡單又高效,是以經常用于:

  • 包總量較少的通信,如

    DNS

    SNMP

  • 視訊、音頻等多媒體通信
  • 廣播通信
為什麼 UDP 頭部沒有「首部長度」字段,而 TCP 頭部有「首部長度」字段呢?

原因是 TCP 有可變長的「選項」字段,而 UDP 頭部長度則是不會變化的,無需多一個字段去記錄 UDP 的首部長度。

為什麼 UDP 頭部有「包長度」字段,而 TCP 頭部則沒有「包長度」字段呢?

先說說 TCP 是如何計算負載資料長度:

djano 字段不重複_硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題...

其中 IP 總長度 和 IP 首部長度,在 IP 首部格式是已知的。TCP 首部長度,則是在 TCP 首部格式已知的,是以就可以求得 TCP 資料的長度。

大家這時就奇怪了問:“ UDP 也是基于 IP 層的呀,那 UDP 的資料長度也可以通過這個公式計算呀?為何還要有「包長度」呢?”

這麼一問,确實感覺 UDP 「包長度」是備援的。

因為為了網絡裝置硬體設計和處理友善,首部長度需要是

4

位元組的整數倍。

如果去掉 UDP 「包長度」字段,那 UDP 首部長度就不是

4

位元組的整數倍了,是以小林覺得這可能是為了補全 UDP 首部長度是

4

位元組的整數倍,才補充了「包長度」字段。

02 TCP 連接配接建立

TCP 三次握手過程和狀态變遷

TCP 是面向連接配接的協定,是以使用 TCP 前必須先建立連接配接,而建立連接配接是通過三次握手而進行的。

djano 字段不重複_硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題...

TCP 三次握手

  • 一開始,用戶端和服務端都處于

    CLOSED

    狀态。先是服務端主動監聽某個端口,處于

    LISTEN

    狀态
djano 字段不重複_硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題...

第一個封包—— SYN 封包

  • 用戶端會随機初始化序号(

    client_isn

    ),将此序号置于 TCP 首部的「序号」字段中,同時把

    SYN

    标志位置為

    1

    ,表示

    SYN

    封包。接着把第一個 SYN 封包發送給服務端,表示向服務端發起連接配接,該封包不包含應用層資料,之後用戶端處于

    SYN-SENT

    狀态。
djano 字段不重複_硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題...

第二個封包 —— SYN + ACK 封包

  • 服務端收到用戶端的

    SYN

    封包後,首先服務端也随機初始化自己的序号(

    server_isn

    ),将此序号填入 TCP 首部的「序号」字段中,其次把 TCP 首部的「确認應答号」字段填入

    client_isn + 1

    , 接着把

    SYN

    ACK

    标志位置為

    1

    。最後把該封包發給用戶端,該封包也不包含應用層資料,之後服務端處于

    SYN-RCVD

    狀态。
djano 字段不重複_硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題...

第三個封包 —— ACK 封包

  • 用戶端收到服務端封包後,還要向服務端回應最後一個應答封包,首先該應答封包 TCP 首部

    ACK

    标志位置為

    1

    ,其次「确認應答号」字段填入

    server_isn + 1

    ,最後把封包發送給服務端,這次封包可以攜帶客戶到伺服器的資料,之後用戶端處于

    ESTABLISHED

    狀态。
  • 伺服器收到用戶端的應答封包後,也進入

    ESTABLISHED

    狀态。

從上面的過程可以發現第三次握手是可以攜帶資料的,前兩次握手是不可以攜帶資料的,這也是面試常問的題。

一旦完成三次握手,雙方都處于

ESTABLISHED

狀态,此緻連接配接就已建立完成,用戶端和服務端就可以互相發送資料了。

如何在 Linux 系統中檢視 TCP 狀态?

TCP 的連接配接狀态檢視,在 Linux 可以通過

netstat -napt

指令檢視。

djano 字段不重複_硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題...

TCP 連接配接狀态檢視

為什麼是三次握手?不是兩次、四次?

相信大家比較常回答的是:“因為三次握手才能保證雙方具有接收和發送的能力。”

這回答是沒問題,但這回答是片面的,并沒有說出主要的原因。

在前面我們知道了什麼是TCP 連接配接:

  • 用于保證可靠性和流量控制維護的某些狀态資訊,這些資訊的組合,包括Socket、序列号和視窗大小稱為連接配接。

是以,重要的是為什麼三次握手才可以初始化Socket、序列号和視窗大小并建立 TCP 連接配接。

接下來以三個方面分析三次握手的原因:

  • 三次握手才可以阻止曆史重複連接配接的初始化(主要原因)
  • 三次握手才可以同步雙方的初始序列号
  • 三次握手才可以避免資源浪費

原因一:避免曆史連接配接

我們來看看 RFC 793 指出的 TCP 連接配接使用三次握手的首要原因:

The principle reason for the three-way handshake is to prevent old duplicate connection initiations from causing confusion.

簡單來說,三次握手的首要原因是為了防止舊的重複連接配接初始化造成混亂。

網絡環境是錯綜複雜的,往往并不是如我們期望的一樣,先發送的資料包,就先到達目标主機,反而它很騷,可能會由于網絡擁堵等亂七八糟的原因,會使得舊的資料包,先到達目标主機,那麼這種情況下 TCP 三次握手是如何避免的呢?

djano 字段不重複_硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題...

三次握手避免曆史連接配接

用戶端連續發送多次 SYN 建立連接配接的封包,在網絡擁堵等情況下:

  • 一個「舊 SYN 封包」比「最新的 SYN 」 封包早到達了服務端;
  • 那麼此時服務端就會回一個

    SYN + ACK

    封包給用戶端;
  • 用戶端收到後可以根據自身的上下文,判斷這是一個曆史連接配接(序列号過期或逾時),那麼用戶端就會發送

    RST

    封包給服務端,表示中止這一次連接配接。

如果是兩次握手連接配接,就不能判斷目前連接配接是否是曆史連接配接,三次握手則可以在用戶端(發送方)準備發送第三次封包時,用戶端因有足夠的上下文來判斷目前連接配接是否是曆史連接配接:

  • 如果是曆史連接配接(序列号過期或逾時),則第三次握手發送的封包是

    RST

    封包,以此中止曆史連接配接;
  • 如果不是曆史連接配接,則第三次發送的封包是

    ACK

    封包,通信雙方就會成功建立連接配接;

是以, TCP 使用三次握手建立連接配接的最主要原因是防止曆史連接配接初始化了連接配接。

原因二:同步雙方初始序列号

TCP 協定的通信雙方, 都必須維護一個「序列号」, 序列号是可靠傳輸的一個關鍵因素,它的作用:

  • 接收方可以去除重複的資料;
  • 接收方可以根據資料包的序列号按序接收;
  • 可以辨別發送出去的資料包中, 哪些是已經被對方收到的;

可見,序列号在 TCP 連接配接中占據着非常重要的作用,是以當用戶端發送攜帶「初始序列号」的

SYN

封包的時候,需要服務端回一個

ACK

應答封包,表示用戶端的 SYN 封包已被服務端成功接收,那當服務端發送「初始序列号」給用戶端的時候,依然也要得到用戶端的應答回應,這樣一來一回,才能確定雙方的初始序列号能被可靠的同步。

djano 字段不重複_硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題...

四次握手與三次握手

四次握手其實也能夠可靠的同步雙方的初始化序号,但由于第二步和第三步可以優化成一步,是以就成了「三次握手」。

而兩次握手隻保證了一方的初始序列号能被對方成功接收,沒辦法保證雙方的初始序列号都能被确認接收。

原因三:避免資源浪費

如果隻有「兩次握手」,當用戶端的

SYN

請求連接配接在網絡中阻塞,用戶端沒有接收到

ACK

封包,就會重新發送

SYN

,由于沒有第三次握手,伺服器不清楚用戶端是否收到了自己發送的建立連接配接的

ACK

确認信号,是以每收到一個

SYN

就隻能先主動建立一個連接配接,這會造成什麼情況呢?

如果用戶端的

SYN

阻塞了,重複發送多次

SYN

封包,那麼伺服器在收到請求後就會建立多個備援的無效連結,造成不必要的資源浪費。

djano 字段不重複_硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題...

兩次握手會造成資源浪費

即兩次握手會造成消息滞留情況下,伺服器重複接受無用的連接配接請求

SYN

封包,而造成重複配置設定資源。

小結

TCP 建立連接配接時,通過三次握手能防止曆史連接配接的建立,能減少雙方不必要的資源開銷,能幫助雙方同步初始化序列号。序列号能夠保證資料包不重複、不丢棄和按序傳輸。

不使用「兩次握手」和「四次握手」的原因:

  • 「兩次握手」:無法防止曆史連接配接的建立,會造成雙方資源的浪費,也無法可靠的同步雙方序列号;
  • 「四次握手」:三次握手就已經理論上最少可靠連接配接建立,是以不需要使用更多的通信次數。
為什麼用戶端和服務端的初始序列号 ISN 是不相同的?

因為網絡中的封包會延遲、會複制重發、也有可能丢失,這樣會造成的不同連接配接之間産生互相影響,是以為了避免互相影響,用戶端和服務端的初始序列号是随機且不同的。

初始序列号 ISN 是如何随機産生的?

起始

ISN

是基于時鐘的,每 4 毫秒 + 1,轉一圈要 4.55 個小時。

RFC1948 中提出了一個較好的初始化序列号 ISN 随機生成算法。

ISN = M + F (localhost, localport, remotehost, remoteport)

  • M

    是一個計時器,這個計時器每隔 4 毫秒加 1。
  • F

    是一個 Hash 算法,根據源 IP、目的 IP、源端口、目的端口生成一個随機數值。要保證 Hash 算法不能被外部輕易推算得出,用 MD5 算法是一個比較好的選擇。
既然 IP 層會分片,為什麼 TCP 層還需要 MSS 呢?

我們先來認識下 MTU 和 MSS

djano 字段不重複_硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題...

MTU 與 MSS

  • MTU

    :一個網絡包的最大長度,以太網中一般為

    1500

    位元組;
  • MSS

    :除去 IP 和 TCP 頭部之後,一個網絡包所能容納的 TCP 資料的最大長度;

如果TCP 的整個封包(頭部 + 資料)交給 IP 層進行分片,會有什麼異常呢?

當 IP 層有一個超過

MTU

大小的資料(TCP 頭部 + TCP 資料)要發送,那麼 IP 層就要進行分片,把資料分片成若幹片,保證每一個分片都小于 MTU。把一份 IP 資料報進行分片以後,由目标主機的 IP 層來進行重新組裝後,在交給上一層 TCP 傳輸層。

這看起來井然有序,但這存在隐患的,那麼當如果一個 IP 分片丢失,整個 IP 封包的所有分片都得重傳。

因為 IP 層本身沒有逾時重傳機制,它由傳輸層的 TCP 來負責逾時和重傳。

當接收方發現 TCP 封包(頭部 + 資料)的某一片丢失後,則不會響應 ACK 給對方,那麼發送方的 TCP 在逾時後,就會重發「整個 TCP 封包(頭部 + 資料)」。

是以,可以得知由 IP 層進行分片傳輸,是非常沒有效率的。

是以,為了達到最佳的傳輸效能 TCP 協定在建立連接配接的時候通常要協商雙方的 MSS 值,當 TCP 層發現資料超過 MSS 時,則就先會進行分片,當然由它形成的 IP 包的長度也就不會大于 MTU ,自然也就不用 IP 分片了。

djano 字段不重複_硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題...

握手階段協商 MSS

經過 TCP 層分片後,如果一個 TCP 分片丢失後,進行重發時也是以 MSS 為機關,而不用重傳所有的分片,大大增加了重傳的效率。

什麼是 SYN 攻擊?如何避免 SYN 攻擊?

SYN 攻擊

我們都知道 TCP 連接配接建立是需要三次握手,假設攻擊者短時間僞造不同 IP 位址的

SYN

封包,服務端每接收到一個

SYN

封包,就進入

SYN_RCVD

狀态,但服務端發送出去的

ACK + SYN

封包,無法得到未知 IP 主機的

ACK

應答,久而久之就會占滿服務端的 SYN 接收隊列(未連接配接隊列),使得伺服器不能為正常使用者服務。

djano 字段不重複_硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題...

SYN 攻擊

避免 SYN 攻擊方式一

其中一種解決方式是通過修改 Linux 核心參數,控制隊列大小和當隊列滿時應做什麼處理。

  • 當網卡接收資料包的速度大于核心處理的速度時,會有一個隊列儲存這些資料包。控制該隊列的最大值如下參數:
net
           
  • SYN_RCVD 狀态連接配接的最大個數:
net
           
  • 超出處理能時,對新的 SYN 直接回 RST,丢棄連接配接:
net
           

避免 SYN 攻擊方式二

我們先來看下Linux 核心的

SYN

(未完成連接配接建立)隊列與

Accpet

(已完成連接配接建立)隊列是如何工作的?

djano 字段不重複_硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題...

正常流程

正常流程:

  • 當服務端接收到用戶端的 SYN 封包時,會将其加入到核心的「 SYN 隊列」;
  • 接着發送 SYN + ACK 給用戶端,等待用戶端回應 ACK 封包;
  • 服務端接收到 ACK 封包後,從「 SYN 隊列」移除放入到「 Accept 隊列」;
  • 應用通過調用

    accpet()

    socket 接口,從「 Accept 隊列」取出的連接配接。
djano 字段不重複_硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題...

應用程式過慢

應用程式過慢:

  • 如果應用程式過慢時,就會導緻「 Accept 隊列」被占滿。
djano 字段不重複_硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題...

受到 SYN 攻擊

受到 SYN 攻擊:

  • 如果不斷受到 SYN 攻擊,就會導緻「 SYN 隊列」被占滿。

tcp_syncookies

的方式可以應對 SYN 攻擊的方法:

net.ipv4.tcp_syncookies = 1
           
djano 字段不重複_硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題...

tcp_syncookies 應對 SYN 攻擊

  • 當 「 SYN 隊列」滿之後,後續伺服器收到 SYN 包,不進入「 SYN 隊列」;
  • 計算出一個

    cookie

    值,再以 SYN + ACK 中的「序列号」傳回用戶端,
  • 服務端接收到用戶端的應答封包時,伺服器會檢查這個 ACK 包的合法性。如果合法,直接放入到「 Accept 隊列」。
  • 最後應用通過調用

    accpet()

    socket 接口,從「 Accept 隊列」取出的連接配接。

03 TCP 連接配接斷開

TCP 四次揮手過程和狀态變遷

天下沒有不散的宴席,對于 TCP 連接配接也是這樣, TCP 斷開連接配接是通過四次揮手方式。

雙方都可以主動斷開連接配接,斷開連接配接後主機中的「資源」将被釋放。

djano 字段不重複_硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題...

用戶端主動關閉連接配接 —— TCP 四次揮手

  • 用戶端打算關閉連接配接,此時會發送一個 TCP 首部

    FIN

    标志位被置為

    1

    的封包,也即

    FIN

    封包,之後用戶端進入

    FIN_WAIT_1

    狀态。
  • 服務端收到該封包後,就向用戶端發送

    ACK

    應答封包,接着服務端進入

    CLOSED_WAIT

    狀态。
  • 用戶端收到服務端的

    ACK

    應答封包後,之後進入

    FIN_WAIT_2

    狀态。
  • 等待服務端處理完資料後,也向用戶端發送

    FIN

    封包,之後服務端進入

    LAST_ACK

    狀态。
  • 用戶端收到服務端的

    FIN

    封包後,回一個

    ACK

    應答封包,之後進入

    TIME_WAIT

    狀态
  • 伺服器收到了

    ACK

    應答封包後,就進入了

    CLOSE

    狀态,至此服務端已經完成連接配接的關閉。
  • 用戶端在經過

    2MSL

    一段時間後,自動進入

    CLOSE

    狀态,至此用戶端也完成連接配接的關閉。

你可以看到,每個方向都需要一個 FIN 和一個 ACK,是以通常被稱為四次揮手。

這裡一點需要注意是:主動關閉連接配接的,才有 TIME_WAIT 狀态。

為什麼揮手需要四次?

再來回顧下四次揮手雙方發

FIN

包的過程,就能了解為什麼需要四次了。

  • 關閉連接配接時,用戶端向服務端發送

    FIN

    時,僅僅表示用戶端不再發送資料了但是還能接收資料。
  • 伺服器收到用戶端的

    FIN

    封包時,先回一個

    ACK

    應答封包,而服務端可能還有資料需要處理和發送,等服務端不再發送資料時,才發送

    FIN

    封包給用戶端來表示同意現在關閉連接配接。

從上面過程可知,服務端通常需要等待完成資料的發送和處理,是以服務端的

ACK

FIN

一般都會分開發送,進而比三次握手導緻多了一次。

為什麼 TIME_WAIT 等待的時間是 2MSL?

MSL

是 Maximum Segment Lifetime,封包最大生存時間,它是任何封包在網絡上存在的最長時間,超過這個時間封包将被丢棄。因為 TCP 封包基于是 IP 協定的,而 IP 頭中有一個

TTL

字段,是 IP 資料報可以經過的最大路由數,每經過一個處理他的路由器此值就減 1,當此值為 0 則資料報将被丢棄,同時發送 ICMP 封包通知源主機。

MSL 與 TTL 的差別:MSL 的機關是時間,而 TTL 是經過路由跳數。是以MSL 應該要大于等于 TTL 消耗為 0 的時間,以確定封包已被自然消亡。

TIME_WAIT 等待 2 倍的 MSL,比較合理的解釋是:網絡中可能存在來自發送方的資料包,當這些發送方的資料包被接收方處理後又會向對方發送響應,是以一來一回需要等待 2 倍的時間。

比如,如果被動關閉方沒有收到斷開連接配接的最後的 ACK 封包,就會觸發逾時重發 Fin 封包,另一方接收到 FIN 後,會重發 ACK 給被動關閉方, 一來一去正好 2 個 MSL。

2MSL

的時間是從用戶端接收到 FIN 後發送 ACK 開始計時的。如果在 TIME-WAIT 時間内,因為用戶端的 ACK 沒有傳輸到服務端,用戶端又接收到了服務端重發的 FIN 封包,那麼2MSL 時間将重新計時。

在 Linux 系統裡

2MSL

預設是

60

秒,那麼一個

MSL

也就是

30

秒。Linux 系統停留在 TIME_WAIT 的時間為固定的 60 秒。

其定義在 Linux 核心代碼裡的名稱為 TCP_TIMEWAIT_LEN:

#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT 
                                    state, about 60 seconds  */
           

如果要修改 TIME_WAIT 的時間長度,隻能修改 Linux 核心代碼裡 TCP_TIMEWAIT_LEN 的值,并重新編譯 Linux 核心。

為什麼需要 TIME_WAIT 狀态?

主動發起關閉連接配接的一方,才會有

TIME-WAIT

狀态。

需要 TIME-WAIT 狀态,主要是兩個原因:

  • 防止具有相同「四元組」的「舊」資料包被收到;
  • 保證「被動關閉連接配接」的一方能被正确的關閉,即保證最後的 ACK 能讓被動關閉方接收,進而幫助其正常關閉;

原因一:防止舊連接配接的資料包

假設 TIME-WAIT 沒有等待時間或時間過短,被延遲的資料包抵達後會發生什麼呢?

djano 字段不重複_硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題...

接收到曆史資料的異常

  • 如上圖黃色框框服務端在關閉連接配接之前發送的

    SEQ = 301

    封包,被網絡延遲了。
  • 這時有相同端口的 TCP 連接配接被複用後,被延遲的

    SEQ = 301

    抵達了用戶端,那麼用戶端是有可能正常接收這個過期的封包,這就會産生資料錯亂等嚴重的問題。

是以,TCP 就設計出了這麼一個機制,經過

2MSL

這個時間,足以讓兩個方向上的資料包都被丢棄,使得原來連接配接的資料包在網絡中都自然消失,再出現的資料包一定都是建立立連接配接所産生的。

原因二:保證連接配接正确關閉

在 RFC 793 指出 TIME-WAIT 另一個重要的作用是:

TIME-WAIT - represents waiting for enough time to pass to be sure the remote TCP received the acknowledgment of its connection termination request.

也就是說,TIME-WAIT 作用是等待足夠的時間以確定最後的 ACK 能讓被動關閉方接收,進而幫助其正常關閉。

假設 TIME-WAIT 沒有等待時間或時間過短,斷開連接配接會造成什麼問題呢?

djano 字段不重複_硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題...

沒有確定正常斷開的異常

  • 如上圖紅色框框用戶端四次揮手的最後一個

    ACK

    封包如果在網絡中被丢失了,此時如果用戶端

    TIME-WAIT

    過短或沒有,則就直接進入了

    CLOSE

    狀态了,那麼服務端則會一直處在

    LASE-ACK

    狀态。
  • 當用戶端發起建立連接配接的

    SYN

    請求封包後,服務端會發送

    RST

    封包給用戶端,連接配接建立的過程就會被終止。

如果 TIME-WAIT 等待足夠長的情況就會遇到兩種情況:

  • 服務端正常收到四次揮手的最後一個

    ACK

    封包,則服務端正常關閉連接配接。
  • 服務端沒有收到四次揮手的最後一個

    ACK

    封包時,則會重發

    FIN

    關閉連接配接封包并等待新的

    ACK

    封包。

是以用戶端在

TIME-WAIT

狀态等待

2MSL

時間後,就可以保證雙方的連接配接都可以正常的關閉。

TIME_WAIT 過多有什麼危害?

如果伺服器有處于 TIME-WAIT 狀态的 TCP,則說明是由伺服器方主動發起的斷開請求。

過多的 TIME-WAIT 狀态主要的危害有兩種:

  • 第一是記憶體資源占用;
  • 第二是對端口資源的占用,一個 TCP 連接配接至少消耗一個本地端口;

第二個危害是會造成嚴重的後果的,要知道,端口資源也是有限的,一般可以開啟的端口為

32768~61000

,也可以通過如下參數設定指定

net
           

如果服務端 TIME_WAIT 狀态過多,占滿了所有端口資源,則會導緻無法建立新連接配接。

如何優化 TIME_WAIT?

這裡給出優化 TIME-WAIT 的幾個方式,都是有利有弊:

  • 打開 net.ipv4.tcp_tw_reuse 和 net.ipv4.tcp_timestamps 選項;
  • net.ipv4.tcp_max_tw_buckets
  • 程式中使用 SO_LINGER ,應用強制使用 RST 關閉。

方式一:net.ipv4.tcp_tw_reuse 和 tcp_timestamps

如下的 Linux 核心參數開啟後,則可以複用處于 TIME_WAIT 的 socket 為新的連接配接所用。

使用這個選項,還有一個前提,需要打開對 TCP 時間戳的支援,即

這個時間戳的字段是在 TCP 頭部的「選項」裡,用于記錄 TCP 發送方的目前時間戳和從對端接收到的最新時間戳。

由于引入了時間戳,我們在前面提到的

2MSL

問題就不複存在了,因為重複的資料包會因為時間戳過期被自然丢棄。

溫馨提醒:

net.ipv4.tcp_tw_reuse

要慎用,因為使用了它就必然要打開時間戳的支援

net.ipv4.tcp_timestamps

,當用戶端與服務端主機時間不同步時,用戶端的發送的消息會被直接拒絕掉。小林在工作中就遇到過。。。排查了非常的久

方式二:net.ipv4.tcp_max_tw_buckets

這個值預設為 18000,當系統中處于 TIME_WAIT 的連接配接一旦超過這個值時,系統就會将所有的 TIME_WAIT 連接配接狀态重置。

這個方法過于暴力,而且治标不治本,帶來的問題遠比解決的問題多,不推薦使用。

方式三:程式中使用 SO_LINGER

我們可以通過設定 socket 選項,來設定調用 close 關閉連接配接行為。

struct linger so_linger;
           

如果

l_onoff

為非 0, 且

l_linger

值為 0,那麼調用

close

後,會立該發送一個

RST

标志給對端,該 TCP 連接配接将跳過四次揮手,也就跳過了

TIME_WAIT

狀态,直接關閉。

但這為跨越

TIME_WAIT

狀态提供了一個可能,不過是一個非常危險的行為,不值得提倡。

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

TCP 有一個機制是保活機制。這個機制的原理是這樣的:

定義一個時間段,在這個時間段内,如果沒有任何連接配接相關的活動,TCP 保活機制會開始作用,每隔一個時間間隔,發送一個探測封包,該探測封包包含的資料非常少,如果連續幾個探測封包都沒有得到響應,則認為目前的 TCP 連接配接已經死亡,系統核心将錯誤資訊通知給上層應用程式。

在 Linux 核心可以有對應的參數可以設定保活時間、保活探測的次數、保活探測的時間間隔,以下都為預設值:

net.ipv4.tcp_keepalive_time=7200
net.ipv4.tcp_keepalive_intvl=75  
net.ipv4.tcp_keepalive_probes=9
           
  • tcp_keepalive_time=7200:表示保活時間是 7200 秒(2小時),也就 2 小時内如果沒有任何連接配接相關的活動,則會啟動保活機制
  • tcp_keepalive_intvl=75:表示每次檢測間隔 75 秒;
  • tcp_keepalive_probes=9:表示檢測 9 次無響應,認為對方是不可達的,進而中斷本次的連接配接。

也就是說在 Linux 系統中,最少需要經過 2 小時 11 分 15 秒才可以發現一個「死亡」連接配接。

djano 字段不重複_硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題...

這個時間是有點長的,我們也可以根據實際的需求,對以上的保活相關的參數進行設定。

如果開啟了 TCP 保活,需要考慮以下幾種情況:

第一種,對端程式是正常工作的。當 TCP 保活的探測封包發送給對端, 對端會正常響應,這樣TCP 保活時間會被重置,等待下一個 TCP 保活時間的到來。

第二種,對端程式崩潰并重新開機。當 TCP 保活的探測封包發送給對端後,對端是可以響應的,但由于沒有該連接配接的有效資訊,會産生一個 RST 封包,這樣很快就會發現 TCP 連接配接已經被重置。

第三種,是對端程式崩潰,或對端由于其他原因導緻封包不可達。當 TCP 保活的探測封包發送給對端後,石沉大海,沒有響應,連續幾次,達到保活探測次數後,TCP 會報告該 TCP 連接配接已經死亡。

03 Socket 程式設計

針對 TCP 應該如何 Socket 程式設計?
djano 字段不重複_硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題...

基于 TCP 協定的用戶端和伺服器工作

  • 服務端和用戶端初始化

    socket

    ,得到檔案描述符;
  • 服務端調用

    bind

    ,将綁定在 IP 位址和端口;
  • 服務端調用

    listen

    ,進行監聽;
  • 服務端調用

    accept

    ,等待用戶端連接配接;
  • 用戶端調用

    connect

    ,向伺服器端的位址和端口發起連接配接請求;
  • 服務端

    accept

    傳回用于傳輸的

    socket

    的檔案描述符;
  • 用戶端調用

    write

    寫入資料;服務端調用

    read

    讀取資料;
  • 用戶端斷開連接配接時,會調用

    close

    ,那麼服務端

    read

    讀取資料的時候,就會讀取到了

    EOF

    ,待處理完資料後,服務端調用

    close

    ,表示連接配接關閉。

這裡需要注意的是,服務端調用

accept

時,連接配接成功了會傳回一個已完成連接配接的 socket,後續用來傳輸資料。

是以,監聽的 socket 和真正用來傳送資料的 socket,是「兩個」 socket,一個叫作監聽 socket,一個叫作已完成連接配接 socket。

成功連接配接建立之後,雙方開始通過 read 和 write 函數來讀寫資料,就像往一個檔案流裡面寫東西一樣。

listen 時候參數 backlog 的意義?

Linux核心中會維護兩個隊列:

  • 未完成連接配接隊列(SYN 隊列):接收到一個 SYN 建立連接配接請求,處于 SYN_RCVD 狀态;
  • 已完成連接配接隊列(Accpet 隊列):已完成 TCP 三次握手過程,處于 ESTABLISHED 狀态;
djano 字段不重複_硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題...

SYN 隊列 與 Accpet 隊列

int listen (int socketfd, int backlog)
           
  • 參數一 socketfd 為 socketfd 檔案描述符
  • 參數二 backlog,這參數在曆史有一定的變化

在早期 Linux 核心 backlog 是 SYN 隊列大小,也就是未完成的隊列大小。

在 Linux 核心 2.2 之後,backlog 變成 accept 隊列,也就是已完成連接配接建立的隊列長度,是以現在通常認為 backlog 是 accept 隊列。

accept 發送在三次握手的哪一步?

我們先看看用戶端連接配接服務端時,發送了什麼?

djano 字段不重複_硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題...

用戶端連接配接服務端

  • 用戶端的協定棧向伺服器端發送了 SYN 包,并告訴伺服器端目前發送序列号 client_isn,用戶端進入 SYNC_SENT 狀态;
  • 伺服器端的協定棧收到這個包之後,和用戶端進行 ACK 應答,應答的值為 client_isn+1,表示對 SYN 包 client_isn 的确認,同時伺服器也發送一個 SYN 包,告訴用戶端目前我的發送序列号為 server_isn,伺服器端進入 SYNC_RCVD 狀态;
  • 用戶端協定棧收到 ACK 之後,使得應用程式從

    connect

    調用傳回,表示用戶端到伺服器端的單向連接配接建立成功,用戶端的狀态為 ESTABLISHED,同時用戶端協定棧也會對伺服器端的 SYN 包進行應答,應答資料為 server_isn+1;
  • 應答包到達伺服器端後,伺服器端協定棧使得

    accept

    阻塞調用傳回,這個時候伺服器端到用戶端的單向連接配接也建立成功,伺服器端也進入 ESTABLISHED 狀态。

從上面的描述過程,我們可以得知用戶端 connect 成功傳回是在第二次握手,服務端 accept 成功傳回是在三次握手成功之後。

用戶端調用 close 了,連接配接是斷開的流程是什麼?

我們看看用戶端主動調用了

close

,會發生什麼?

djano 字段不重複_硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題...

用戶端調用 close 過程

  • 用戶端調用

    close

    ,表明用戶端沒有資料需要發送了,則此時會向服務端發送 FIN 封包,進入 FIN_WAIT_1 狀态;
  • 服務端接收到了 FIN 封包,TCP 協定棧會為 FIN 包插入一個檔案結束符

    EOF

    到接收緩沖區中,應用程式可以通過

    read

    調用來感覺這個 FIN 包。這個

    EOF

    會被放在已排隊等候的其他已接收的資料之後,這就意味着服務端需要處理這種異常情況,因為 EOF 表示在該連接配接上再無額外資料到達。此時,服務端進入 CLOSE_WAIT 狀态;
  • 接着,當處理完資料後,自然就會讀到

    EOF

    ,于是也調用

    close

    關閉它的套接字,這會使得會發出一個 FIN 包,之後處于 LAST_ACK 狀态;
  • 用戶端接收到服務端的 FIN 包,并發送 ACK 确認包給服務端,此時用戶端将進入 TIME_WAIT 狀态;
  • 服務端收到 ACK 确認包後,就進入了最後的 CLOSE 狀态;
  • 用戶端進過

    2MSL

    時間之後,也進入 CLOSED 狀态;

巨人的肩膀 [1] 趣談網絡協定專欄.劉超.極客時間. [2] 網絡程式設計實戰專欄.盛延敏.極客時間. [3] 計算機網絡-自頂向下方法.陳鳴 譯.機械工業出版社 [4] TCP/IP詳解 卷1:協定.範建華 譯.機械工業出版社 [5] 圖解TCP/IP.竹下隆史.人民郵電出版社 [6] https://www.rfc-editor.org/rfc/rfc793.html [7] https://draveness.me/whys-the-design-tcp-three-way-handshake [8] https://draveness.me/whys-the-design-tcp-time-wait - END - 點選卡片進入小程式,簽到赢禮品 👇🏻👇🏻👇🏻

djano 字段不重複_硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題...

(更多精彩值得期待……)

djano 字段不重複_硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題...

更多精彩

在公衆号背景對話框輸入以下關鍵詞

檢視更多優質内容!

女朋友 | 大資料 | 運維 | 書單 | 算法

大資料 | JavaScript | Python | 黑客

AI | 人工智能 | 5G | 區塊鍊

機器學習 | 數學 | 送書

djano 字段不重複_硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題...
djano 字段不重複_硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題...

● 

djano 字段不重複_硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題...

 為什麼 TCP 會被 UDP 取代

● 

djano 字段不重複_硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題...

 腳本之家粉絲福利,請檢視!

● 

djano 字段不重複_硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題...

 人人都欠微軟一個正版?

● TCP為啥要3次握手和4次揮手?握兩次手不行嗎?

● 四張動圖看懂 TCP 的三次握手和四次揮手

djano 字段不重複_硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題...