1. 程式人生 > >【圖解】你還在為 TCP 重傳、滑動視窗、流量控制、擁塞控制發愁嗎?看完圖解就不愁了

【圖解】你還在為 TCP 重傳、滑動視窗、流量控制、擁塞控制發愁嗎?看完圖解就不愁了

每日一句英語學習,每天進步一點點:

前言

前一篇「硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題」得到了很多讀者的認可,在此特別感謝你們的認可,大家都暖暖的。

來了,今天又來圖解 TCP 了,小林可能會遲到,但不會缺席。

遲到的原因,主要是 TCP 巨複雜,它為了保證可靠性,用了巨多的機制來保證,真是個「偉大」的協議,寫著寫著發現這水太深了。。。

本文的全部圖片都是小林繪畫的,非常的辛苦且累,不廢話了,直接進入正文,Go!


正文

相信大家都知道 TCP 是一個可靠傳輸的協議,那它是如何保證可靠的呢?

為了實現可靠性傳輸,需要考慮很多事情,例如資料的破壞、丟包、重複以及分片順序混亂等問題。如不能解決這些問題,也就無從談起可靠傳輸。

那麼,TCP 是通過序列號、確認應答、重發控制、連線管理以及視窗控制等機制實現可靠性傳輸的。

今天,將重點介紹 TCP 的重傳機制、滑動視窗、流量控制、擁塞控制。

提綱

重傳機制

TCP 實現可靠傳輸的方式之一,是通過序列號與確認應答。

在 TCP 中,當傳送端的資料到達接收主機時,接收端主機會返回一個確認應答訊息,表示已收到訊息。

正常的資料傳輸

但在錯綜複雜的網路,並不一定能如上圖那麼順利能正常的資料傳輸,萬一資料在傳輸過程中丟失了呢?

所以 TCP 針對資料包丟失的情況,會用重傳機制解決。

接下來說說常見的重傳機制:

  • 超時重傳
  • 快速重傳
  • SACK
  • D-SACK

超時重傳

重傳機制的其中一個方式,就是在傳送資料時,設定一個定時器,當超過指定的時間後,沒有收到對方的 ACK

確認應答報文,就會重發該資料,也就是我們常說的超時重傳。

TCP 會在以下兩種情況發生超時重傳:

  • 資料包丟失
  • 確認應答丟失
超時重傳的兩種情況

超時時間應該設定為多少呢?

我們先來了解一下什麼是 RTT(Round-Trip Time 往返時延),從下圖我們就可以知道:

RTT

RTT 就是資料從網路一端傳送到另一端所需的時間,也就是包的往返時間。

超時重傳時間是以 RTO (Retransmission Timeout 超時重傳時間)表示。

假設在重傳的情況下,超時時間 RTO 「較長或較短」時,會發生什麼事情呢?

超時時間較長與較短

上圖中有兩種超時時間不同的情況:

  • 當超時時間 RTO 較大時,重發就慢,丟了老半天才重發,沒有效率,效能差;
  • 當超時時間 RTO 較小時,會導致可能並沒有丟就重發,於是重發的就快,會增加網路擁塞,導致更多的超時,更多的超時導致更多的重發。

精確的測量超時時間 RTO 的值是非常重要的,這可讓我們的重傳機制更高效。

根據上述的兩種情況,我們可以得知,超時重傳時間 RTO 的值應該略大於報文往返 RTT 的值。

RTO 應略大於 RTT

至此,可能大家覺得超時重傳時間 RTO 的值計算,也不是很複雜嘛。

好像就是在傳送端發包時記下 t0 ,然後接收端再把這個 ack 回來時再記一個 t1,於是 RTT = t1 – t0。沒那麼簡單,這只是一個取樣,不能代表普遍情況。

實際上「報文往返 RTT 的值」是經常變化的,因為我們的網路也是時常變化的。也就因為「報文往返 RTT 的值」 是經常波動變化的,所以「超時重傳時間 RTO 的值」應該是一個動態變化的值。

我們來看看 Linux 是如何計算 RTO 的呢?

估計往返時間,通常需要取樣以下兩個:

  • 需要 TCP 通過取樣 RTT 的時間,然後進行加權平均,算出一個平滑 RTT 的值,而且這個值還是要不斷變化的,因為網路狀況不斷地變化。
  • 除了取樣 RTT,還要取樣 RTT 的波動範圍,這樣就避免如果 RTT 有一個大的波動的話,很難被發現的情況。

RFC6289 建議使用以下的公式計算 RTO:

RFC6289 建議的 RTO 計算

其中 SRTT 是計算平滑的RTT ,DevRTR 是計算平滑的RTT 與 最新 RTT 的差距。

在 Linux 下,α = 0.125,β = 0.25, μ = 1,∂ = 4。別問怎麼來的,問就是大量實驗中調出來的。

如果超時重發的資料,再次超時的時候,又需要重傳的時候,TCP 的策略是超時間隔加倍。

也就是每當遇到一次超時重傳的時候,都會將下一次超時時間間隔設為先前值的兩倍。兩次超時,就說明網路環境差,不宜頻繁反覆傳送。

超時觸發重傳存在的問題是,超時週期可能相對較長。那是不是可以有更快的方式呢?

於是就可以用「快速重傳」機制來解決超時重發的時間等待。

快速重傳

TCP 還有另外一種快速重傳(Fast Retransmit)機制,它不以時間為驅動,而是以資料驅動重傳。

快速重傳機制,是如何工作的呢?其實很簡單,一圖勝千言。

快速重傳機制

在上圖,傳送方發出了 1,2,3,4,5 份資料:

  • 第一份 Seq1 先送到了,於是就 Ack 回 2;
  • 結果 Seq2 因為某些原因沒收到,Seq3 到達了,於是還是 Ack 回 2;
  • 後面的 Seq4 和 Seq5 都到了,但還是 Ack 回 2,因為 Seq2 還是沒有收到;
  • 傳送端收到了三個 Ack = 2 的確認,知道了 Seq2 還沒有收到,就會在定時器過期之前,重傳丟失的 Seq2。
  • 最後,接收到收到了 Seq2,此時因為 Seq3,Seq4,Seq5 都收到了,於是 Ack 回 6 。

所以,快速重傳的工作方式是當收到三個相同的 ACK 報文時,會在定時器過期之前,重傳丟失的報文段。

快速重傳機制只解決了一個問題,就是超時時間的問題,但是它依然面臨著另外一個問題。就是重傳的時候,是重傳之前的一個,還是重傳所有的問題。

比如對於上面的例子,是重傳 Seq2 呢?還是重傳 Seq2、Seq3、Seq4、Seq5 呢?因為傳送端並不清楚這連續的三個 Ack 2 是誰傳回來的。

根據 TCP 不同的實現,以上兩種情況都是有可能的。可見,這是一把雙刃劍。

為了解決不知道該重傳哪些 TCP 報文,於是就有 SACK 方法。

SACK 方法

還有一種實現重傳機制的方式叫:SACK( Selective Acknowledgment 選擇性確認)。

這種方式需要在 TCP 頭部「選項」欄位里加一個 SACK 的東西,它可以將快取的地圖傳送給傳送方,這樣傳送方就可以知道哪些資料收到了,哪些資料沒收到,知道了這些資訊,就可以只重傳丟失的資料。

如下圖,傳送方收到了三次同樣的 ACK 確認報文,於是就會觸發快速重發機制,通過 SACK 資訊發現只有 200~299 這段資料丟失,則重發時,就只選擇了這個 TCP 段進行重複。

選擇性確認

如果要支援 SACK,必須雙方都要支援。在 Linux 下,可以通過 net.ipv4.tcp_sack 引數開啟這個功能(Linux 2.4 後預設開啟)。

Duplicate SACK

Duplicate SACK 又稱 D-SACK,其主要使用了 SACK 來告訴「傳送方」有哪些資料被重複接收了。

下面舉例兩個栗子,來說明 D-SACK 的作用。

栗子一號:ACK 丟包

ACK 丟包
  • 「接收方」發給「傳送方」的兩個 ACK 確認應答都丟失了,所以傳送方超時後,重傳第一個資料包(3000 ~ 3499)
  • 於是「接收方」發現數據是重複收到的,於是回了一個 SACK = 3000~3500,告訴「傳送方」 3000~3500 的資料早已被接收了,因為 ACK 都到了 4000 了,已經意味著 4000 之前的所有資料都已收到,所以這個 SACK 就代表著 D-SACK
  • 這樣「傳送方」就知道了,資料沒有丟,是「接收方」的 ACK 確認報文丟了。

栗子二號:網路延時

網路延時
  • 資料包(1000~1499) 被網路延遲了,導致「傳送方」沒有收到 Ack 1500 的確認報文。
  • 而後面報文到達的三個相同的 ACK 確認報文,就觸發了快速重傳機制,但是在重傳後,被延遲的資料包(1000~1499)又到了「接收方」;
  • 所以「接收方」回了一個 SACK=1000~1500,因為 ACK 已經到了 3000,所以這個 SACK 是 D-SACK,表示收到了重複的包。
  • 這樣傳送方就知道快速重傳觸發的原因不是發出去的包丟了,也不是因為迴應的 ACK 包丟了,而是因為網路延遲了。

可見,D-SACK 有這麼幾個好處:

  1. 可以讓「傳送方」知道,是發出去的包丟了,還是接收方迴應的 ACK 包丟了;
  2. 可以知道是不是「傳送方」的資料包被網路延遲了;
  3. 可以知道網路中是不是把「傳送方」的資料包給複製了;

在 Linux 下可以通過 net.ipv4.tcp_dsack 引數開啟/關閉這個功能(Linux 2.4 後預設開啟)。


滑動視窗

引入視窗概念的原因

我們都知道 TCP 是每傳送一個數據,都要進行一次確認應答。當上一個數據包收到了應答了, 再發送下一個。

這個模式就有點像我和你面對面聊天,你一句我一句。但這種方式的缺點是效率比較低的。

如果你說完一句話,我在處理其他事情,沒有及時回覆你,那你不是要乾等著我做完其他事情後,我回復你,你才能說下一句話,很顯然這不現實。

按資料包進行確認應答

所以,這樣的傳輸方式有一個缺點:資料包的往返時間越長,通訊的效率就越低。

為解決這個問題,TCP 引入了視窗這個概念。即使在往返時間較長的情況下,它也不會降低網路通訊的效率。

那麼有了視窗,就可以指定視窗大小,視窗大小就是指無需等待確認應答,而可以繼續傳送資料的最大值。

視窗的實現實際上是作業系統開闢的一個快取空間,傳送方主機在等到確認應答返回之前,必須在緩衝區中保留已傳送的資料。如果按期收到確認應答,此時資料就可以從快取區清除。

假設視窗大小為 3 個 TCP 段,那麼傳送方就可以「連續傳送」 3 個 TCP 段,並且中途若有 ACK 丟失,可以通過「下一個確認應答進行確認」。如下圖:

用滑動視窗方式並行處理

圖中的 ACK 600 確認應答報文丟失,也沒關係,因為可以通話下一個確認應答進行確認,只要傳送方收到了 ACK 700 確認應答,就意味著 700 之前的所有資料「接收方」都收到了。這個模式就叫累計確認或者累計應答。

視窗大小由哪一方決定?

TCP 頭裡有一個欄位叫 Window,也就是視窗大小。

這個欄位是接收端告訴傳送端自己還有多少緩衝區可以接收資料。於是傳送端就可以根據這個接收端的處理能力來發送資料,而不會導致接收端處理不過來。

所以,通常視窗的大小是由接收方的決定的。

傳送方傳送的資料大小不能超過接收方的視窗大小,否則接收方就無法正常接收到資料。

傳送方的滑動視窗

我們先來看看傳送方的視窗,下圖就是傳送方快取的資料,根據處理的情況分成四個部分,其中深藍色方框是傳送視窗,紫色方框是可用視窗:

  • #1 是已傳送並收到 ACK確認的資料:1~31 位元組
  • #2 是已傳送但未收到 ACK確認的資料:32~45 位元組
  • #3 是未傳送但總大小在接收方處理範圍內(接收方還有空間):46~51位元組
  • #4 是未傳送但總大小超過接收方處理範圍(接收方沒有空間):52位元組以後

在下圖,當傳送方把資料「全部」都一下發送出去後,可用視窗的大小就為 0 了,表明可用視窗耗盡,在沒收到 ACK 確認之前是無法繼續傳送資料了。

可用視窗耗盡

在下圖,當收到之前傳送的資料 32~36 位元組的 ACK 確認應答後,如果傳送視窗的大小沒有變化,則滑動視窗往右邊移動 5 個位元組,因為有 5 個位元組的資料被應答確認,接下來 52~56 位元組又變成了可用視窗,那麼後續也就可以傳送 52~56 這 5 個位元組的資料了。

32 ~ 36 位元組已確認

程式是如何表示傳送方的四個部分的呢?

TCP 滑動視窗方案使用三個指標來跟蹤在四個傳輸類別中的每一個類別中的位元組。其中兩個指標是絕對指標(指特定的序列號),一個是相對指標(需要做偏移)。

SND.WND、SND.UN、SND.NXT
  • SND.WND:表示傳送視窗的大小(大小是由接收方指定的);

  • SND.UNA:是一個絕對指標,它指向的是已傳送但未收到確認的第一個位元組的序列號,也就是 #2 的第一個位元組。

  • SND.NXT:也是一個絕對指標,它指向未傳送但可傳送範圍的第一個位元組的序列號,也就是 #3 的第一個位元組。

  • 指向 #4 的第一個位元組是個相對指標,它需要 SND.NXT 指標加上 SND.WND 大小的偏移量,就可以指向 #4 的第一個位元組了。

那麼可用視窗大小的計算就可以是:

可用視窗大 = SND.WND -(SND.NXT - SND.UNA)

接收方的滑動視窗

接下來我們看看接收方的視窗,接收視窗相對簡單一些,根據處理的情況劃分成三個部分:

  • #1 + #2 是已成功接收並確認的資料(等待應用程序讀取);
  • #3 是未收到資料但可以接收的資料;
  • #4 未收到資料並不可以接收的資料;
接收視窗

其中三個接收部分,使用兩個指標進行劃分:

  • RCV.WND:表示接收視窗的大小,它會通告給傳送方。
  • RCV.NXT:是一個指標,它指向期望從傳送方傳送來的下一個資料位元組的序列號,也就是 #3 的第一個位元組。
  • 指向 #4 的第一個位元組是個相對指標,它需要 RCV.NXT 指標加上 RCV.WND 大小的偏移量,就可以指向 #4 的第一個位元組了。

接收視窗和傳送視窗的大小是相等的嗎?

並不是完全相等,接收視窗的大小是約等於傳送視窗的大小的。

因為滑動視窗並不是一成不變的。比如,當接收方的應用程序讀取資料的速度非常快的話,這樣的話接收視窗可以很快的就空缺出來。那麼新的接收視窗大小,是通過 TCP 報文中的 Windows 欄位來告訴傳送方。那麼這個傳輸過程是存在時延的,所以接收視窗和傳送視窗是約等於的關係。


流量控制

傳送方不能無腦的發資料給接收方,要考慮接收方處理能力。

如果一直無腦的發資料給對方,但對方處理不過來,那麼就會導致觸發重發機制,從而導致網路流量的無端的浪費。

為了解決這種現象發生,TCP 提供一種機制可以讓「傳送方」根據「接收方」的實際接收能力控制傳送的資料量,這就是所謂的流量控制。

下面舉個栗子,為了簡單起見,假設以下場景:

  • 客戶端是接收方,服務端是傳送方
  • 假設接收視窗和傳送視窗相同,都為 200
  • 假設兩個裝置在整個傳輸過程中都保持相同的視窗大小,不受外界影響
流量控制

根據上圖的流量控制,說明下每個過程:

  1. 客戶端向服務端傳送請求資料報文。這裡要說明下,本次例子是把服務端作為傳送方,所以沒有畫出服務端的接收視窗。
  2. 服務端收到請求報文後,傳送確認報文和 80 位元組的資料,於是可用視窗 Usable 減少為 120 位元組,同時 SND.NXT 指標也向右偏移 80 位元組後,指向 321,這意味著下次傳送資料的時候,序列號是 321。
  3. 客戶端收到 80 位元組資料後,於是接收視窗往右移動 80 位元組,RCV.NXT 也就指向 321,這意味著客戶端期望的下一個報文的序列號是 321,接著傳送確認報文給服務端。
  4. 服務端再次傳送了 120 位元組資料,於是可用視窗耗盡為 0,服務端無法在繼續傳送資料。
  5. 客戶端收到 120 位元組的資料後,於是接收視窗往右移動 120 位元組,RCV.NXT 也就指向 441,接著傳送確認報文給服務端。
  6. 服務端收到對 80 位元組資料的確認報文後,SND.UNA 指標往右偏移後指向 321,於是可用視窗 Usable 增大到 80。
  7. 服務端收到對 120 位元組資料的確認報文後,SND.UNA 指標往右偏移後指向 441,於是可用視窗 Usable 增大到 200。
  8. 服務端可以繼續傳送了,於是傳送了 160 位元組的資料後,SND.NXT 指向 601,於是可用視窗 Usable 減少到 40。
  9. 客戶端收到 160 位元組後,接收視窗往右移動了 160 位元組,RCV.NXT 也就是指向了 601,接著傳送確認報文給服務端。
  10. 服務端收到對 160 位元組資料的確認報文後,傳送視窗往右移動了 160 位元組,於是 SND.UNA 指標偏移了 160 後指向 601,可用視窗 Usable 也就增大至了 200。

作業系統緩衝區與滑動視窗的關係

前面的流量控制例子,我們假定了傳送視窗和接收視窗是不變的,但是實際上,傳送視窗和接收視窗中所存放的位元組數,都是放在作業系統記憶體緩衝區中的,而作業系統的緩衝區,會被作業系統調整。

當應用程序沒辦法及時讀取緩衝區的內容時,也會對我們的緩衝區造成影響。

那操心繫統的緩衝區,是如何影響傳送視窗和接收視窗的呢?

我們先來看看第一個例子。

當應用程式沒有及時讀取快取時,傳送視窗和接收視窗的變化。

考慮以下場景:

  • 客戶端作為傳送方,服務端作為接收方,傳送視窗和接收視窗初始大小為 360
  • 服務端非常的繁忙,當收到客戶端的資料時,應用層不能及時讀取資料。

根據上圖的流量控制,說明下每個過程:

  1. 客戶端傳送 140 位元組資料後,可用視窗變為 220 (360 - 140)。
  2. 服務端收到 140 位元組資料,但是服務端非常繁忙,應用程序只讀取了 40 個位元組,還有 100 位元組佔用著緩衝區,於是接收視窗收縮到了 260 (360 - 100),最後傳送確認資訊時,將視窗大小通過給客戶端。
  3. 客戶端收到確認和視窗通告報文後,傳送視窗減少為 260。
  4. 客戶端傳送 180 位元組資料,此時可用視窗減少到 80。
  5. 服務端收到 180 位元組資料,但是應用程式沒有讀取任何資料,這 180 位元組直接就留在了緩衝區,於是接收視窗收縮到了 80 (260 - 180),並在傳送確認資訊時,通過視窗大小給客戶端。
  6. 客戶端收到確認和視窗通告報文後,傳送視窗減少為 80。
  7. 客戶端傳送 80 位元組資料後,可用視窗耗盡。
  8. 服務端收到 80 位元組資料,但是應用程式依然沒有讀取任何資料,這 80 位元組留在了緩衝區,於是接收視窗收縮到了 0,並在傳送確認資訊時,通過視窗大小給客戶端。
  9. 客戶端收到確認和視窗通告報文後,傳送視窗減少為 0。

可見最後視窗都收縮為 0 了,也就是發生了視窗關閉。當傳送方可用視窗變為 0 時,傳送方實際上會定時傳送視窗探測報文,以便知道接收方的視窗是否發生了改變,這個內容後面會說,這裡先簡單提一下。

我們先來看看第二個例子。

當服務端系統資源非常緊張的時候,操心繫統可能會直接減少了接收緩衝區大小,這時應用程式又無法及時讀取快取資料,那麼這時候就有嚴重的事情發生了,會出現資料包丟失的現象。

說明下每個過程:

  1. 客戶端傳送 140 位元組的資料,於是可用視窗減少到了 220。
  2. 服務端因為現在非常的繁忙,作業系統於是就把接收快取減少了 100 位元組,當收到 對 140 資料確認報文後,又因為應用程式沒有讀取任何資料,所以 140 位元組留在了緩衝區中,於是接收視窗大小從 360 收縮成了 100,最後傳送確認資訊時,通告視窗大小給對方。
  3. 此時客戶端因為還沒有收到服務端的通告視窗報文,所以不知道此時接收視窗收縮成了 100,客戶端只會看自己的可用視窗還有 220,所以客戶端就傳送了 180 位元組資料,於是可用視窗減少到 40。
  4. 服務端收到了 180 位元組資料時,發現數據大小超過了接收視窗的大小,於是就把資料包丟失了。
  5. 客戶端收到第 2 步時,服務端傳送的確認報文和通告視窗報文,嘗試減少傳送視窗到 100,把視窗的右端向左收縮了 80,此時可用視窗的大小就會出現詭異的負值