玩轉TCP

超大隻烏龜發表於2021-10-13

前言

最近在拉勾教育學習計算機網路相關知識,今天先來學習下TCP。

正文

什麼是TCP

TCP(Transport Control Protocol)是一個傳輸層協議,提供 Host-To-Host 資料的可靠傳輸,支援全雙工,是一個連線導向的協議。

TCP 的握手和揮手

TCP 是一個連線導向的協議,設計有建立連線(握手)和斷開連線(揮手)的過程。

TCP 協議有這樣幾個基本操作:

  • 一個 Host 主動向另一個 Host 發起連線,稱為 SYN(Synchronization),請求同步;
  • 一個 Host 主動斷開請求,稱為 FIN(Finish),請求完成;
  • 一個 Host 給另一個 Host 傳送資料,稱為 PSH(Push),資料推送。

以上 3 種情況,接收方收到資料後,都需要給傳送方一個 ACK(Acknowledgement)響應。請求/響應的模型是可靠性的要求,如果一個請求沒有響應,傳送方可能會認為自己需要重發這個請求。

建立連線的過程(三次握手)

因為要保持連線和可靠性約束,TCP 協議要保證每一條發出的資料必須給返回,返回資料叫作 ACK(也就是響應)。

按照這個思路,你可以看看建立連線是不是需要 3 次握手:

image.png

  1. 客戶端發訊息給服務端(SYN)
  2. 服務端準備好進行連線
  3. 服務端針對客戶端的 SYN 給一個 ACK

你可以能會問,到這裡不就可以了嗎?2 次握手就足夠了。但其實不是,因為服務端還沒有確定客戶端是否準備好了。比如步驟 3 之後,服務端馬上給客戶端傳送資料,這個時候客戶端可能還沒有準備好接收資料。因此還需要增加一個過程。

接下來還會發生以下操作:

  1. 服務端傳送一個 SYN 給客戶端
  2. 客戶端準備就緒
  3. 客戶端給服務端傳送一個 ACK

你可能會問,上面不是 6 個步驟嗎? 怎麼是 3 次握手呢?下面我們一起分析一下其中緣由。

步驟 1 是 1 次握手;

步驟 2 是服務端的準備,不是資料傳輸,因此不算握手;

步驟 3 和步驟 4,因為是同時發生的,可以合併成一個 SYN-ACK 響應,作為一條資料傳遞給客戶端,因此是第 2 次握手;

步驟 5 不算握手;

步驟 6 是第 3 次握手。

為了方便你理解步驟 3 和步驟 4,這裡我畫了一張圖。可以看到下圖中 SYN 和 ACK 被合併了,因此建立連線一共需要 3 次握手,過程如下圖所示:

image.png

從上面的例子中,你可以進一步思考 SYN、ACK、PSH 這些常見的標識位(Flag)在傳輸中如何表示。

一種思路是為 TCP 協議增加協議頭。在協議頭中取多個位(bit),其中 SYN、ACK、PSH 都佔有 1 個位。比如 SYN 位,1 表示 SYN 開啟,0 表示關閉。因此,SYN-ACK 就是 SYN 位和 ACK 位都置 1。這種設計,我們也稱為標識(Flag)。

斷開連線的過程(4 次揮手)

繼續上面的思路,如果斷開連線需要幾次握手?給你一些提示,你可以在腦海中這樣構思。

  1. 客戶端要求斷開連線,傳送一個斷開的請求,這個叫作(FIN)。
  1. 服務端收到請求,然後給客戶端一個 ACK,作為 FIN 的響應。
  1. 這裡你需要思考一個問題,可不可以像握手那樣馬上傳 FIN 回去?
    其實這個時候服務端不能馬上傳 FIN,因為斷開連線要處理的問題比較多,比如說服務端可能還有傳送出去的訊息沒有得到 ACK;也有可能服務端自己有資源要釋放。因此斷開連線不能像握手那樣操作——將兩條訊息合併。所以,服務端經過一個等待,確定可以關閉連線了,再發一條 FIN 給客戶端。
  1. 客戶端收到服務端的 FIN,同時客戶端也可能有自己的事情需要處理完,比如客戶端有傳送給服務端沒有收到 ACK 的請求,客戶端自己處理完成後,再給服務端傳送一個 ACK。

image.png

TIME_WAIT

相信大家都知道,TCP 主動關閉連線的那一方會最後進入TIME_WAIT 狀態。

其實就是客戶端發出確認報文後不是立馬釋放TCP連線,而是要經過2MSL(最長報文段壽命的2倍時長)後才釋放TCP連線。

那麼TIME_WAIT 狀態是用來解決或避免什麼問題呢?

這裡同樣是要考慮丟包的問題,如果第四次揮手的報文丟失,服務端沒收到確認ack報文就會重發第三次揮手的報文,這樣報文一去一回最長時間就是2MSL,所以需要等這麼長時間來確認服務端確實已經收到了。

TCP的粘包和拆包

TCP 是一個傳輸層協議。TCP 傳送資料的時候,往往不會將資料一次性傳送,像下圖這樣:

image.png

而是將資料拆分成很多個部分,然後再逐個傳送。像下圖這樣:

image.png

同樣的,在目的地,TCP 協議又需要逐個接收資料。請你思考,TCP 為什麼不一次傳送完所有的資料?比如我們要傳一個大小為 10M 的檔案,對於應用層而言,就是一次傳送完成的。而傳輸層的協議為什麼不選擇將這個檔案一次傳送完呢?

這裡有很多原因,比如為了穩定性,一次傳送的資料越多,出錯的概率越大。再比如說為了效率,網路中有時候存在著並行的路徑,拆分資料包就能更好地利用這些並行的路徑。再有,比如傳送和接收資料的時候,都存在著緩衝區。如下圖所示:

image.png

緩衝區是在記憶體中開闢的一塊區域,目的是緩衝。因為大量的應用頻繁地通過網路卡收發資料,這個時候,網路卡只能一個一個處理應用的請求。當網路卡忙不過來的時候,資料就需要排隊,也就是將資料放入緩衝區。如果每個應用都隨意傳送很大的資料,可能導致其他應用實時性遭到破壞。

總之,方方面面的原因:在傳輸層封包不能太大。這種限制,往往是以緩衝區大小為單位的。也就是 TCP 協議,會將資料拆分成不超過緩衝區大小的一個個部分。每個部分有一個獨特的名詞,叫作 TCP 段(TCP Segment)。

在接收資料的時候,一個個 TCP 段又被重組成原來的資料

像這樣,資料經過拆分,然後傳輸,然後在目的地重組,俗稱拆包。所以拆包是將資料拆分成多個 TCP 段傳輸。那麼粘包是什麼呢?有時候,如果發往一個目的地的多個資料太小了,為了防止多次傳送佔用資源,TCP 協議有可能將它們合併成一個 TCP 段傳送,在目的地再還原成多個資料,這個過程俗稱粘包。所以粘包是將多個資料合併成一個 TCP 段傳送

Sequence Number 和 Acknowledgement Number

在 TCP 協議的設計當中,資料被拆分成很多個部分,部分增加了協議頭。合併成為一個 TCP 段,進行傳輸。這個過程,我們俗稱拆包。這些 TCP 段經過複雜的網路結構,由底層的 IP 協議,負責傳輸到目的地,然後再進行重組。

這裡請你思考一個問題:穩定性要求資料無損地傳輸,也就是說拆包獲得資料,又需要恢復到原來的樣子。而在複雜的網路環境當中,即便所有的段是順序發出的,也不能保證它們順序到達,因此,發出的每一個 TCP 段都需要有序號。這個序號,就是 Sequence Number(Seq)。

image.png

如上圖所示。傳送資料的時候,為每一個 TCP 段分配一個自增的 Sequence Number。接收資料的時候,雖然得到的是亂序的 TCP 段,但是可以通過 Seq 進行排序。

但是這樣又會產生一個新的問題——接收方如果要回復傳送方,也需要這個 Seq。而網路的兩個終端,去同步一個自增的序號是非常困難的。因為任何兩個網路主體間,時間都不能做到完全同步,又沒有公共的儲存空間,無法共享資料,更別說實現一個分散式的自增序號了。

其實這個問題的本質就好像兩個人在說話一樣,我們要確保他們說出去的話,和回答之間的順序。因為 TCP 是一個雙工的協議,兩邊可能會同時說話。所以聰明的科學家想到了確定一句話的順序,需要兩個值去描述——也就是傳送的位元組數和接收的位元組數。

image.png

我們重新定義一下 Seq(如上圖所示),對於任何一個接收方,如果知道了傳送者傳送某個 TCP 段時,已經傳送了多少位元組的資料,那麼就可以確定傳送者傳送資料的順序。

但是這裡有一個問題。如果接收方也向傳送者傳送了資料請求(或者說雙方在對話),接收方就不知道傳送者傳送的資料到底對應哪一條自己傳送的資料了。

因此我們還需要另一個資料,就是每個 TCP 段傳送時,傳送方已經接收了多少資料。用 Acknowledgement Number 表示,下面簡寫為 ACK。

下圖中,終端傳送了三條資料,並且接收到四條資料,通過觀察,根據接收到的資料中的 Seq 和 ACK,將傳送和接收的資料進行排序。

image.png

例如上圖中,傳送方傳送了 100 位元組的資料,而接收到的(Seq = 0 和 Seq =100)的兩個封包,都是針對傳送方(Seq = 0)這個封包的。傳送 100 個位元組,所以接收到的 ACK 剛好是 100。說明(Seq= 0 和 Seq= 100)這兩個封包是針對接收到第 100 個位元組資料後,傳送回來的。這樣就確定了整體的順序。

滑動視窗和流速控制

TCP 作為一個傳輸層協議,最核心的能力是傳輸。傳輸需要保證可靠性,還需要控制流速,這兩個核心能力均由滑動視窗提供。

請求/響應模型

TCP 中每個傳送的請求都需要響應。如果一個請求沒有收到響應,傳送方就會認為這次傳送出現了故障,會觸發重發。

大體的模型,和下圖很像。但是如果完全和下圖一樣,每一個請求收到響應之後,再傳送下一個請求,吞吐量會很低。因為這樣的設計,會產生網路的空閒時間,說白了,就是浪費頻寬。頻寬沒有用滿,意味著可以同時傳送更多的請求,接收更多的響應。

image.png

一種改進的方式,就是讓傳送方有請求就傳送出去,而不是等待響應。通過這樣的處理方式,傳送的資料連在了一起,響應的資料也連在了一起,吞吐量就提升了。

image.png

但是如果可以同時傳送的資料真的非常多呢?比如成百上千個 TCP 段都需要傳送,這個時候頻寬可能會不足。

滑動視窗(Sliding Window)

那麼這時候就需要我們的滑動視窗來實現了,如下圖:

image.png

如上圖所示:

  • 深綠色代表已經收到 ACK 的段
  • 淺綠色代表傳送了,但是沒有收到 ACK 的段
  • 白色代表沒有傳送的段
  • 紫色代表暫時不能傳送的段

下面我們重新設計一下不同型別封包的順序,將已傳送的資料放到最左邊,傳送中的資料放到中間,未傳送的資料放到右邊。假設我們最多同時傳送 5 個封包,也就是視窗大小 = 5。視窗中的資料被同時傳送出去,然後等待 ACK。如果一個封包 ACK 到達,我們就將它標記為已接收(深綠色)。

如下圖所示,有兩個封包的 ACK 到達,因此標記為綠色。

image.png

這個時候滑動視窗可以向右滑動,如下圖所示:

image.png

重傳

如果傳送過程中,部分資料沒能收到 ACK 會怎樣呢?這就可能發生重傳。

如果發生下圖這樣的情況,段 4 遲遲沒有收到 ACK。

image.png

這個時候滑動視窗只能右移一個位置,如下圖所示:

image.png

在這個過程中,如果後來段 4 重傳成功(接收到 ACK),那麼視窗就會繼續右移。如果段 4 傳送失敗,還是沒能收到 ACK,那麼接收方也會拋棄段 5、段 6、段 7。這樣從段 4 開始之後的資料都需要重發。

快速重傳

在 TCP 協議中,如果接收方想丟棄某個段,可以選擇不發 ACK。傳送端超時後,會重發這個 TCP 段。而有時候,接收方希望催促傳送方儘快補發某個 TCP 段,這個時候可以使用快速重傳能力。

例如段 1、段 2、段 4 到了,但是段 3 沒有到。 接收方可以傳送多次段 3 的 ACK。如果傳送方收到多個段 3 的 ACK,就會重發段 3。這個機制稱為快速重傳。這和超時重發不同,是一種催促的機制。

為了不讓傳送方誤以為段 3 已經收到了,在快速重傳的情況下,接收方即便收到發來的段 4,依然會發段 3 的 ACK(不發段 4 的 ACK),直到傳送方把段 3 重傳。

思考:視窗大小的單位是?

請你思考另一個問題,視窗大小的單位是多少呢?在上面所有的圖片中,視窗大小是 TCP 段的數量。實際操作中,每個 TCP 段的大小不同,限制數量會讓接收方的緩衝區不好操作,因此實際操作中視窗大小單位是位元組數

思考:滑動視窗是越大越好嗎?

引用知乎的回答:

image.png

流速控制

傳送、接收視窗的大小可以用來控制 TCP 協議的流速。視窗越大,同時可以傳送、接收的資料就越多,支援的吞吐量也就越大。當然,視窗越大,如果資料發生錯誤,損失也就越大,因為需要重傳越多的資料。

總結

為了提高傳輸速率,TCP 協議選擇將多個段同時傳送,為了讓這些段不至於被接收方拒絕服務,在傳送前,雙方要協商好傳送的速率。但是我們不可能完全確定網速,所以協商的方式,就變成確定視窗大小。

有了視窗,傳送方利用滑動視窗演算法傳送訊息;接收方構造緩衝區接收訊息,並給傳送方 ACK。滑動視窗的實現只需要陣列和少量的指標即可,是一個非常高效的演算法。

那麼,現在你可以嘗試來回答本講關聯的面試題目:滑動視窗和流速控制是怎麼回事

【解析】滑動視窗是 TCP 協議控制可靠性的核心。傳送方將資料拆包,變成多個分組。然後將資料放入一個擁有滑動視窗的陣列,依次發出,仍然遵循先入先出(FIFO)的順序,但是視窗中的分組會一次性傳送。視窗中序號最小的分組如果收到 ACK,視窗就會發生滑動;如果最小序號的分組長時間沒有收到 ACK,就會觸發整個視窗的資料重新傳送。

另一方面,在多次傳輸中,網路的平均延遲往往是相對固定的,這樣 TCP 協議可以通過雙方協商視窗大小控制流速。

TCP和UDP

TCP 和 UDP 是今天應用最廣泛的傳輸層協議,擁有最核心的壟斷地位。TCP 最核心的價值是提供了可靠性,而 UDP 最核心的價值是靈活,你幾乎可以用它來做任何事情。例如:HTTP 協議 1.1 和 2.0 都基於 TCP,而到了 HTTP 3.0 就開始用 UDP 了。

UDP 與 TCP的區別

接下來我們說說 UDP 和 TCP 的區別。

  1. 目的差異
    首先,這兩個協議的目的不同:TCP 協議的核心目標是提供可靠的網路傳輸,而 UDP 的目標是在提供報文交換能力基礎上儘可能地簡化協議輕裝上陣。
  1. 可靠性差異
    TCP 核心是要在保證可靠性提供更好的服務。TCP 會有握手的過程,需要建立連線,保證雙方同時線上。而且TCP 有時間視窗持續收集無序的資料,直到這一批資料都可以合理地排序組成連續的結果。UDP 並不具備以上這些特性,它只管傳送資料封包,而且 UDP 不需要 ACK,這意味著訊息傳送出去成功與否 UDP 是不管的。
  1. 連線 vs 無連線
    TCP 是一個面向連線的協議(Connection-oriented Protocol),傳輸資料必須先建立連線。 UDP 是一個無連線協議(Connection-less Protocol),資料隨時都可以傳送,只提供傳送封包(Datagram)的能力。
  1. 流控技術(Flow Control)
    TCP 使用了流控技術來確保傳送方不會因為一次傳送過多的資料包而使接收方不堪重負。TCP 在傳送緩衝區中儲存資料,並在接收緩衝區中接收資料。當應用程式準備就緒時,它將從接收緩衝區讀取資料。如果接收緩衝區已滿,接收方將無法處理更多資料,並將其丟棄。UDP 沒有提供類似的能力。
  1. 傳輸速度
    UDP 協議簡化,封包小,沒有連線、可靠性檢查等,因此單純從傳輸速度上講,UDP 更快。
  1. 場景差異
    TCP 每個資料封包都需要確認,因此天然不適應高速資料傳輸場景,比如觀看視訊(流媒體應用)、網路遊戲(TCP 有延遲)等。具體來說,如果網路遊戲用 TCP,每個封包都需要確認,可能會造成一定的延遲;再比如音、視訊傳輸天生就允許一定的丟包率;Ping 和 DNSLookup,這型別的操作只需要一次簡單的請求/返回,不需要建立連線,用 UDP 就足夠了。

總結

這次的TCP就學習到這裡。

參考

《 計算機網路通關 29 講》