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

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

前言

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

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

遲到的緣由,主要是 TCP 巨複雜,它爲了保證可靠性,用了巨多的機制來保證,真是個「偉大」的協議,寫着寫着發現這水太深了。。。面試

本文的所有圖片都是小林繪畫的,很是的辛苦且累,不廢話了,直接進入正文,Go!算法


正文

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

爲了實現可靠性傳輸,須要考慮不少事情,例如數據的破壞、丟包、重複以及分片順序混亂等問題。如不能解決這些問題,也就無從談起可靠傳輸。緩存

那麼,TCP 是經過序列號、確認應答、重發控制、鏈接管理以及窗口控制等機制實現可靠性傳輸的。網絡

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

提綱提綱

重傳機制

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

在 TCP 中,當發送端的數據到達接收主機時,接收端主機會返回一個確認應答消息,表示已收到消息。tcp

正常的數據傳輸正常的數據傳輸

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

因此 TCP 針對數據包丟失的狀況,會用重傳機制解決。

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

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

超時重傳

重傳機制的其中一個方式,就是在發送數據時,設定一個定時器,當超過指定的時間後,沒有收到對方的 ACK 確認應答報文,就會重發該數據,也就是咱們常說的超時重傳

TCP 會在如下兩種狀況發生超時重傳:

  • 數據包丟失
  • 確認應答丟失
超時重傳的兩種狀況超時重傳的兩種狀況

超時時間應該設置爲多少呢?

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

RTTRTT

RTT 就是數據從網絡一端傳送到另外一端所需的時間,也就是包的往返時間。

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

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

超時時間較長與較短超時時間較長與較短

上圖中有兩種超時時間不一樣的狀況:

  • 當超時時間 RTO 較大時,重發就慢,丟了老半天才重發,沒有效率,性能差;
  • 當超時時間 RTO 較小時,會致使可能並無丟就重發,因而重發的就快,會增長網絡擁塞,致使更多的超時,更多的超時致使更多的重發。

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

根據上述的兩種狀況,咱們能夠得知,超時重傳時間 RTO 的值應該略大於報文往返 RTT 的值

RTO 應略大於 RTTRTO 應略大於 RTT

至此,可能你們以爲超時重傳時間 RTO 的值計算,也不是很複雜嘛。

好像就是在發送端發包時記下 t0 ,而後接收端再把這個 ack 回來時再記一個 t1,因而 RTT = t1 – t0。沒那麼簡單,這只是一個採樣,不能表明廣泛狀況

實際上「報文往返 RTT 的值」是常常變化的,由於咱們的網絡也是時常變化的。也就由於「報文往返 RTT 的值」 是常常波動變化的,因此「超時重傳時間 RTO 的值」應該是一個動態變化的值

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

估計往返時間,一般須要採樣如下兩個:

  • 須要 TCP 經過採樣 RTT 的時間,而後進行加權平均,算出一個平滑 RTT 的值,並且這個值仍是要不斷變化的,由於網絡情況不斷地變化。
  • 除了採樣 RTT,還要採樣 RTT 的波動範圍,這樣就避免若是 RTT 有一個大的波動的話,很難被發現的狀況。

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

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 呢?仍是重傳 Seq二、Seq三、Seq四、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 丟包
  • 「接收方」發給「發送方」的兩個 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 字節已確認32 ~ 36 字節已確認

程序是如何表示發送方的四個部分的呢?

TCP 滑動窗口方案使用三個指針來跟蹤在四個傳輸類別中的每個類別中的字節。其中兩個指針是絕對指針(指特定的序列號),一個是相對指針(須要作偏移)。

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

  • SND.UNA:是一個絕對指針,它指向的是已發送但未收到確認的第一個字節的序列號,也就是 #2 的第一個字節。

  • SND.NXT:也是一個絕對指針,它指向未發送但可發送範圍的第一個字節的序列號,也就是 #3 的第一個字節。

  • 指向 #4 的第一個字節是個相對指針,它須要 SND.UNA 指針加上 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,此時可用窗口的大小就會出現詭異的負值。

因此,若是發生了先減小緩存,再收縮窗口,就會出現丟包的現象。

爲了防止這種狀況發生,TCP 規定是不容許同時減小緩存又收縮窗口的,而是採用先收縮窗口,過段時間在減小緩存,這樣就能夠避免了丟包狀況。

窗口關閉

在前面咱們都看到了,TCP 經過讓接收方指明但願從發送方接收的數據大小(窗口大小)來進行流量控制。

若是窗口大小爲 0 時,就會阻止發送方給接收方傳遞數據,直到窗口變爲非 0 爲止,這就是窗口關閉。

窗口關閉潛在的危險

接收方向發送方通告窗口大小時,是經過 ACK 報文來通告的。

那麼,當發生窗口關閉時,接收方處理完數據後,會向發送方通告一個窗口非 0 的 ACK 報文,若是這個通告窗口的 ACK 報文在網絡中丟失了,那麻煩就大了。

窗口關閉潛在的危險窗口關閉潛在的危險

這會致使發送方一直等待接收方的非 0 窗口通知,接收方也一直等待發送方的數據,如不不採起措施,這種相互等待的過程,會形成了死鎖的現象。

TCP 是如何解決窗口關閉時,潛在的死鎖現象呢?

爲了解決這個問題,TCP 爲每一個鏈接設有一個持續定時器,只要 TCP 鏈接一方收到對方的零窗口通知,就啓動持續計時器。

若是持續計時器超時,就會發送窗口探測 ( Window probe ) 報文,而對方在確認這個探測報文時,給出本身如今的接收窗口大小。

窗口探測窗口探測
  • 若是接收窗口仍然爲 0,那麼收到這個報文的一方就會從新啓動持續計時器;
  • 若是接收窗口不是 0,那麼死鎖的局面就能夠被打破了。

窗口探查探測的次數通常爲 3 這次,每次次大約 30-60 秒(不一樣的實現可能會不同)。若是 3 次事後接收窗口仍是 0 的話,有的 TCP 實現就會發 RST 報文來中斷鏈接。

糊塗窗口綜合症

若是接收方太忙了,來不及取走接收窗口裏的數據,那麼就會致使發送方的發送窗口愈來愈小。

到最後,若是接收方騰出幾個字節並告訴發送方如今有幾個字節的窗口,而發送方會義無反顧地發送這幾個字節,這就是糊塗窗口綜合症

要知道,咱們的 TCP + IP 頭有 40 個字節,爲了傳輸那幾個字節的數據,要達上這麼大的開銷,這太不經濟了。

就好像一個能夠承載 50 人的大巴車,每次來了一兩我的,就直接發車。除非家裏有礦的大巴司機,纔敢這樣玩,否則早晚破產。要解決這個問題也不難,大巴司機等乘客數量超過了 25 個,才認定能夠發車。

現舉個糊塗窗口綜合症的栗子,考慮如下場景:

接收方的窗口大小是 360 字節,但接收方因爲某些緣由陷入困境,假設接收方的應用層讀取的能力以下:

  • 接收方每接收 3 個字節,應用程序就只能從緩衝區中讀取 1 個字節的數據;
  • 在下一個發送方的 TCP 段到達以前,應用程序
    還從緩衝區中讀取了 40 個額外的字節;
糊塗窗口綜合症糊塗窗口綜合症

每一個過程的窗口大小的變化,在圖中都描述的很清楚了,能夠發現窗口不斷減小了,而且發送的數據都是比較小的了。

因此,糊塗窗口綜合症的現象是能夠發生在發送方和接收方:

  • 接收方能夠通告一個小的窗口
  • 而發送方能夠發送小數據

因而,要解決糊塗窗口綜合症,就解決上面兩個問題就能夠了

  • 讓接收方不通告小窗口給發送方
  • 讓發送方避免發送小數據

怎麼讓接收方不通告小窗口呢?

接收方一般的策略以下:

當「窗口大小」小於 min( MSS,緩存空間/2 ) ,也就是小於 MSS 與 1/2 緩存大小中的最小值時,就會向發送方通告窗口爲 0,也就阻止了發送方再發數據過來。

等到接收方處理了一些數據後,窗口大小 >= MSS,或者接收方緩存空間有一半可使用,就能夠把窗口打開讓發送方發送數據過來。

怎麼讓發送方避免發送小數據呢?

發送方一般的策略:

使用 Nagle 算法,該算法的思路是延時處理,它知足如下兩個條件中的一條才能夠發送數據:

  • 要等到窗口大小 >= MSS 或是 數據大小 >= MSS
  • 收到以前發送數據的 ack 回包

只要沒知足上面條件中的一條,發送方一直在囤積數據,直到知足上面的發送條件。

另外,Nagle 算法默認是打開的,若是對於一些須要小數據包交互的場景的程序,好比,telnet 或 ssh 這樣的交互性比較強的程序,則須要關閉 Nagle 算法。

能夠在 Socket 設置 TCP_NODELAY 選項來關閉這個算法(關閉 Nagle 算法沒有全局參數,須要根據每一個應用本身的特色來關閉)

setsockopt(sock_fd, IPPROTO_TCP, TCP_NODELAY, (char *)&value, sizeof(int));

擁塞控制

爲何要有擁塞控制呀,不是有流量控制了嗎?

前面的流量控制是避免「發送方」的數據填滿「接收方」的緩存,可是並不知道網絡的中發生了什麼。

通常來講,計算機網絡都處在一個共享的環境。所以也有可能會由於其餘主機之間的通訊使得網絡擁堵。

在網絡出現擁堵時,若是繼續發送大量數據包,可能會致使數據包時延、丟失等,這時 TCP 就會重傳數據,可是一重傳就會致使網絡的負擔更重,因而會致使更大的延遲以及更多的丟包,這個狀況就會進入惡性循環被不斷地放大….

因此,TCP 不能忽略網絡上發生的事,它被設計成一個無私的協議,當網絡發送擁塞時,TCP 會自我犧牲,下降發送的數據量。

因而,就有了擁塞控制,控制的目的就是避免「發送方」的數據填滿整個網絡。

爲了在「發送方」調節所要發送數據的量,定義了一個叫作「擁塞窗口」的概念。

什麼是擁塞窗口?和發送窗口有什麼關係呢?

擁塞窗口 cwnd是發送方維護的一個 的狀態變量,它會根據網絡的擁塞程度動態變化的

咱們在前面提到過發送窗口 swnd 和接收窗口 rwnd 是約等於的關係,那麼因爲入了擁塞窗口的概念後,此時發送窗口的值是swnd = min(cwnd, rwnd),也就是擁塞窗口和接收窗口中的最小值。

擁塞窗口 cwnd 變化的規則:

  • 只要網絡中沒有出現擁塞,cwnd 就會增大;
  • 但網絡中出現了擁塞,cwnd 就減小;

那麼怎麼知道當前網絡是否出現了擁塞呢?

其實只要「發送方」沒有在規定時間內接收到 ACK 應答報文,也就是發生了超時重傳,就會認爲網絡出現了用擁塞。

擁塞控制有哪些控制算法?

擁塞控制主要是四個算法:

  • 慢啓動
  • 擁塞避免
  • 擁塞發生
  • 快速恢復

慢啓動

TCP 在剛創建鏈接完成後,首先是有個慢啓動的過程,這個慢啓動的意思就是一點一點的提升發送數據包的數量,若是一上來就發大量的數據,這不是給網絡添堵嗎?

慢啓動的算法記住一個規則就行:當發送方每收到一個 ACK,就擁塞窗口 cwnd 的大小就會加 1。

這裏假定擁塞窗口 cwnd 和發送窗口 swnd 相等,下面舉個栗子:

  • 鏈接創建完成後,一開始初始化 cwnd = 1,表示能夠傳一個 MSS 大小的數據。
  • 當收到一個 ACK 確認應答後,cwnd 增長 1,因而一次可以發送 2 個
  • 當收到 2 個的 ACK 確認應答後, cwnd 增長 2,因而就能夠比以前多發2 個,因此這一次可以發送 4 個
  • 當這 4 個的 ACK 確認到來的時候,每一個確認 cwnd 增長 1, 4 個確認 cwnd 增長 4,因而就能夠比以前多發 4 個,因此這一次可以發送 8 個。
慢啓動算法慢啓動算法

能夠看出慢啓動算法,發包的個數是指數性的增加

那慢啓動漲到何時是個頭呢?

有一個叫慢啓動門限 ssthresh (slow start threshold)狀態變量。

  • cwnd < ssthresh 時,使用慢啓動算法。
  • cwnd >= ssthresh 時,就會使用「擁塞避免算法」。

擁塞避免算法

前面說道,當擁塞窗口 cwnd 「超過」慢啓動門限 ssthresh 就會進入擁塞避免算法。

通常來講 ssthresh 的大小是 65535 字節。

那麼進入擁塞避免算法後,它的規則是:每當收到一個 ACK 時,cwnd 增長 1/cwnd。

接上前面的慢啓動的栗子,現假定 ssthresh8

  • 當 8 個 ACK 應答確認到來時,每一個確認增長 1/8,8 個 ACK 確認 cwnd 一共增長 1,因而這一次可以發送 9 個 MSS 大小的數據,變成了線性增加。
擁塞避免擁塞避免

因此,咱們能夠發現,擁塞避免算法就是將本來慢啓動算法的指數增加變成了線性增加,仍是增加階段,可是增加速度緩慢了一些。

就這麼一直增加着後,網絡就會慢慢進入了擁塞的情況了,因而就會出現丟包現象,這時就須要對丟失的數據包進行重傳。

當觸發了重傳機制,也就進入了「擁塞發生算法」。

擁塞發生

當網絡出現擁塞,也就是會發生數據包重傳,重傳機制主要有兩種:

  • 超時重傳
  • 快速重傳

這兩種使用的擁塞發送算法是不一樣的,接下來分別來講說。

發生超時重傳的擁塞發生算法

當發生了「超時重傳」,則就會使用擁塞發生算法。

這個時候,sshresh 和 cwnd 的值會發生變化:

  • ssthresh 設爲 cwnd/2
  • cwnd 重置爲 1
擁塞發送 —— 超時重傳擁塞發送 —— 超時重傳

接着,就從新開始慢啓動,慢啓動是會忽然減小數據流的。這真是一旦「超時重傳」,立刻回到解放前。可是這種方式太激進了,反應也很強烈,會形成網絡卡頓。

就好像原本在秋名山高速漂移着,忽然來個緊急剎車,輪胎受得了嗎。。。

發生快速重傳的擁塞發生算法

還有更好的方式,前面咱們講過「快速重傳算法」。當接收方發現丟了一箇中間包的時候,發送三次前一個包的 ACK,因而發送端就會快速地重傳,沒必要等待超時再重傳。

TCP 認爲這種狀況不嚴重,由於大部分沒丟,只丟了一小部分,則 ssthreshcwnd 變化以下:

  • cwnd = cwnd/2 ,也就是設置爲原來的一半;
  • ssthresh = cwnd;
  • 進入快速恢復算法

快速恢復

快速重傳和快速恢復算法通常同時使用,快速恢復算法是認爲,你還能收到 3 個重複 ACK 說明網絡也不那麼糟糕,因此沒有必要像 RTO 超時那麼強烈。

正如前面所說,進入快速恢復以前,cwndssthresh 已被更新了:

  • cwnd = cwnd/2 ,也就是設置爲原來的一半;
  • ssthresh = cwnd;

而後,進入快速恢復算法以下:

  • 擁塞窗口 cwnd = ssthresh + 3 ( 3 的意思是確認有 3 個數據包被收到了)
  • 重傳丟失的數據包
  • 若是再收到重複的 ACK,那麼 cwnd 增長 1
  • 若是收到新數據的 ACK 後,設置 cwnd 爲 ssthresh,接着就進入了擁塞避免算法
快速重傳和快速恢復快速重傳和快速恢復

也就是沒有像「超時重傳」一晚上回到解放前,而是還在比較高的值,後續呈線性增加。


巨人的肩膀

[1] 趣談網絡協議專欄.劉超.極客時間

[2] Web協議詳解與抓包實戰專欄.陶輝.極客時間

[3] TCP/IP詳解 卷1:協議.範建華 譯.機械工業出版社

[4] 圖解TCP/IP.竹下隆史.人民郵電出版社

[5]The TCP/IP Guide.Charles M. Kozierok.

[6] TCP那些事(上).陳皓.酷殼博客.
https://coolshell.cn/articles/11564.html

[7] TCP那些事(下).陳皓.酷殼博客.https://coolshell.cn/articles/11609.html


往期好文

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

硬核!30 張圖解 HTTP 常見的面試題


嘮叨嘮叨

是吧? TCP 巨複雜吧?看完很累吧?

但這還只是 TCP 冰山一腳,它的更深處就由大家本身去探索啦。

本文只是拋磚引玉,若你有更好的想法或文章有誤的地方,歡迎留言討論!

小林是專爲你們圖解的工具人,Goodbye,咱們下次見!

相關文章
相關標籤/搜索