前言
前一篇「硬不硬你說了算!近 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
有這麼幾個好處:
- 可以讓「發送方」知道,是發出去的包丢了,還是接收方回應的 ACK 包丢了;
- 可以知道是不是「發送方」的資料包被網絡延遲了;
- 可以知道網絡中是不是把「發送方」的資料包給複制了;
在 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
-
:是一個絕對指針,它指向的是已發送但未收到确認的第一個位元組的序列号,也就是 #2 的第一個位元組。SND.UNA
-
:也是一個絕對指針,它指向未發送但可發送範圍的第一個位元組的序列号,也就是 #3 的第一個位元組。SND.NXT
- 指向 #4 的第一個位元組是個相對指針,它需要
指針加上SND.UNA
大小的偏移量,就可以指向 #4 的第一個位元組了。SND.WND
那麼可用視窗大小的計算就可以是:
可用視窗大 = SND.WND -(SND.NXT - SND.UNA)
接收方的滑動視窗
接下來我們看看接收方的視窗,接收視窗相對簡單一些,根據處理的情況劃分成三個部分:
- #1 + #2 是已成功接收并确認的資料(等待應用程序讀取);
- #3 是未收到資料但可以接收的資料;
- #4 未收到資料并不可以接收的資料;
接收視窗
其中三個接收部分,使用兩個指針進行劃分:
-
:表示接收視窗的大小,它會通告給發送方。RCV.WND
-
:是一個指針,它指向期望從發送方發送來的下一個資料位元組的序列号,也就是 #3 的第一個位元組。RCV.NXT
-
RCV.NXT
RCV.WND
接收視窗和發送視窗的大小是相等的嗎?
并不是完全相等,接收視窗的大小是約等于發送視窗的大小的。
因為滑動視窗并不是一成不變的。比如,當接收方的應用程序讀取資料的速度非常快的話,這樣的話接收視窗可以很快的就空缺出來。那麼新的接收視窗大小,是通過 TCP 封包中的 Windows 字段來告訴發送方。那麼這個傳輸過程是存在時延的,是以接收視窗和發送視窗是約等于的關系。
流量控制
發送方不能無腦的發資料給接收方,要考慮接收方處理能力。
如果一直無腦的發資料給對方,但對方處理不過來,那麼就會導緻觸發重發機制,進而導緻網絡流量的無端的浪費。
為了解決這種現象發生,TCP 提供一種機制可以讓「發送方」根據「接收方」的實際接收能力控制發送的資料量,這就是所謂的流量控制。
下面舉個栗子,為了簡單起見,假設以下場景:
- 用戶端是接收方,服務端是發送方
- 假設接收視窗和發送視窗相同,都為
200
- 假設兩個裝置在整個傳輸過程中都保持相同的視窗大小,不受外界影響
根據上圖的流量控制,說明下每個過程:
- 用戶端向服務端發送請求資料封包。這裡要說明下,本次例子是把服務端作為發送方,是以沒有畫出服務端的接收視窗。
- 服務端收到請求封包後,發送确認封包和 80 位元組的資料,于是可用視窗
減少為 120 位元組,同時Usable
指針也向右偏移 80 位元組後,指向 321,這意味着下次發送資料的時候,序列号是 321。SND.NXT
- 用戶端收到 80 位元組資料後,于是接收視窗往右移動 80 位元組,
也就指向 321,這意味着用戶端期望的下一個封包的序列号是 321,接着發送确認封包給服務端。RCV.NXT
- 服務端再次發送了 120 位元組資料,于是可用視窗耗盡為 0,服務端無法再繼續發送資料。
- 用戶端收到 120 位元組的資料後,于是接收視窗往右移動 120 位元組,
也就指向 441,接着發送确認封包給服務端。RCV.NXT
- 服務端收到對 80 位元組資料的确認封包後,
指針往右偏移後指向 321,于是可用視窗SND.UNA
增大到 80。Usable
- 服務端收到對 120 位元組資料的确認封包後,
指針往右偏移後指向 441,于是可用視窗SND.UNA
增大到 200。Usable
- 服務端可以繼續發送了,于是發送了 160 位元組的資料後,
指向 601,于是可用視窗SND.NXT
減少到 40。Usable
- 用戶端收到 160 位元組後,接收視窗往右移動了 160 位元組,
也就是指向了 601,接着發送确認封包給服務端。RCV.NXT
- 服務端收到對 160 位元組資料的确認封包後,發送視窗往右移動了 160 位元組,于是
指針偏移了 160 後指向 601,可用視窗SND.UNA
也就增大至了 200。Usable
作業系統緩沖區與滑動視窗的關系
前面的流量控制例子,我們假定了發送視窗和接收視窗是不變的,但是實際上,發送視窗和接收視窗中所存放的位元組數,都是放在作業系統記憶體緩沖區中的,而作業系統的緩沖區,會被作業系統調整。
當應用程序沒辦法及時讀取緩沖區的内容時,也會對我們的緩沖區造成影響。
那操心系統的緩沖區,是如何影響發送視窗和接收視窗的呢?
我們先來看看第一個例子。
當應用程式沒有及時讀取緩存時,發送視窗和接收視窗的變化。
考慮以下場景:
- 用戶端作為發送方,服務端作為接收方,發送視窗和接收視窗初始大小為
;360
- 服務端非常的繁忙,當收到用戶端的資料時,應用層不能及時讀取資料。
- 用戶端發送 140 位元組資料後,可用視窗變為 220 (360 - 140)。
- 服務端收到 140 位元組資料,但是服務端非常繁忙,應用程序隻讀取了 40 個位元組,還有 100 位元組占用着緩沖區,于是接收視窗收縮到了 260 (360 - 100),最後發送确認資訊時,将視窗大小通告給用戶端。
- 用戶端收到确認和視窗通告封包後,發送視窗減少為 260。
- 用戶端發送 180 位元組資料,此時可用視窗減少到 80。
- 服務端收到 180 位元組資料,但是應用程式沒有讀取任何資料,這 180 位元組直接就留在了緩沖區,于是接收視窗收縮到了 80 (260 - 180),并在發送确認資訊時,通過視窗大小給用戶端。
- 用戶端收到确認和視窗通告封包後,發送視窗減少為 80。
- 用戶端發送 80 位元組資料後,可用視窗耗盡。
- 服務端收到 80 位元組資料,但是應用程式依然沒有讀取任何資料,這 80 位元組留在了緩沖區,于是接收視窗收縮到了 0,并在發送确認資訊時,通過視窗大小給用戶端。
- 用戶端收到确認和視窗通告封包後,發送視窗減少為 0。
可見最後視窗都收縮為 0 了,也就是發生了視窗關閉。當發送方可用視窗變為 0 時,發送方實際上會定時發送視窗探測封包,以便知道接收方的視窗是否發生了改變,這個内容後面會說,這裡先簡單提一下。
我們先來看看第二個例子。
當服務端系統資源非常緊張的時候,操心系統可能會直接減少了接收緩沖區大小,這時應用程式又無法及時讀取緩存資料,那麼這時候就有嚴重的事情發生了,會出現資料包丢失的現象。
說明下每個過程:
- 用戶端發送 140 位元組的資料,于是可用視窗減少到了 220。
- 服務端因為現在非常的繁忙,作業系統于是就把接收緩存減少了 120 位元組,當收到 140 位元組資料後,又因為應用程式沒有讀取任何資料,是以 140 位元組留在了緩沖區中,于是接收視窗大小從 360 收縮成了 100,最後發送确認資訊時,通告視窗大小給對方。
- 此時用戶端因為還沒有收到服務端的通告視窗封包,是以不知道此時接收視窗收縮成了 100,用戶端隻會看自己的可用視窗還有 220,是以用戶端就發送了 180 位元組資料,于是可用視窗減少到 40。
- 服務端收到了 180 位元組資料時,發現資料大小超過了接收視窗的大小,于是就把資料包丢失了。
- 用戶端收到第 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 緩存大小中的最小值時,就會向發送方通告視窗為
,也就阻止了發送方再發資料過來。
等到接收方處理了一些資料後,視窗大小 >= 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。
接上前面的慢啟動的栗子,現假定
ssthresh
為
8
:
- 當 8 個 ACK 應答确認到來時,每個确認增加 1/8,8 個 ACK 确認 cwnd 一共增加 1,于是這一次能夠發送 9 個
大小的資料,變成了線性增長。MSS
是以,我們可以發現,擁塞避免算法就是将原本慢啟動算法的指數增長變成了線性增長,還是增長階段,但是增長速度緩慢了一些。
就這麼一直增長着後,網絡就會慢慢進入了擁塞的狀況了,于是就會出現丢包現象,這時就需要對丢失的資料包進行重傳。
當觸發了重傳機制,也就進入了「擁塞發生算法」。
當網絡出現擁塞,也就是會發生資料包重傳,重傳機制主要有兩種:
這兩種使用的擁塞發送算法是不同的,接下來分别來說說。
發生逾時重傳的擁塞發生算法
當發生了「逾時重傳」,則就會使用擁塞發生算法。
這個時候,ssthresh 和 cwnd 的值會發生變化:
-
設為ssthresh
,cwnd/2
-
重置為cwnd
1
擁塞發送 —— 逾時重傳
接着,就重新開始慢啟動,慢啟動是會突然減少資料流的。這真是一旦「逾時重傳」,馬上回到解放前。但是這種方式太激進了,反應也很強烈,會造成網絡卡頓。
就好像本來在秋名山高速漂移着,突然來個緊急刹車,輪胎受得了嗎。。。
發生快速重傳的擁塞發生算法
還有更好的方式,前面我們講過「快速重傳算法」。當接收方發現丢了一個中間包的時候,發送三次前一個包的 ACK,于是發送端就會快速地重傳,不必等待逾時再重傳。
TCP 認為這種情況不嚴重,因為大部分沒丢,隻丢了一小部分,則
ssthresh
和
cwnd
變化如下:
-
,也就是設定為原來的一半;cwnd = cwnd/2
-
;ssthresh = cwnd
- 進入快速恢複算法
快速重傳和快速恢複算法一般同時使用,快速恢複算法是認為,你還能收到 3 個重複 ACK 說明網絡也不那麼糟糕,是以沒有必要像
RTO
逾時那麼強烈。
正如前面所說,進入快速恢複之前,
cwnd
ssthresh
已被更新了:
-
cwnd = cwnd/2
-
ssthresh = cwnd
然後,進入快速恢複算法如下:
-
( 3 的意思是确認有 3 個資料包被收到了);cwnd = ssthresh + 3
- 重傳丢失的資料包;
- 如果再收到重複的 ACK,那麼 cwnd 增加 1;
- 如果收到新資料的 ACK 後,把 cwnd 設定為第一步中的 ssthresh 的值,原因是該 ACK 确認了新的資料,說明從 duplicated ACK 時的資料都已收到,該恢複過程已經結束,可以回到恢複之前的狀态了,也即再次進入擁塞避免狀态;
快速重傳和快速恢複
也就是沒有像「逾時重傳」一夜回到解放前,而是還在比較高的值,後續呈線性增長。
擁塞算法示意圖
好了,以上就是擁塞控制的全部内容了,看完後,你再來看下面這張圖檔,每個過程我相信你都能明白:
TCP 擁塞控制
巨人的肩膀
[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
唠叨唠叨
是吧? TCP 巨複雜吧?看完很累吧?
但這還隻是 TCP 冰山一腳,它的更深處就由你們自己去探索啦。
本文隻是抛磚引玉,若你有更好的想法或文章有誤的地方,歡迎留言讨論!
小林是專為大家圖解的工具人,Goodbye,我們下次見!
讀者問答
讀者問:“整個看完收獲很大,下面是我的一些疑問(稍後
會去确認):
1.擁塞避免這一段,藍色字型:每當收到一個
ACK時,cwnd增加1/cwnd。是否應該是
1/ssthresh?否則不符合線性增長。
2.快速重傳的擁塞發生算法,步驟一和步驟2是
否寫反了?否則快速恢複算法中最後一步【如果
收到新資料的ACK後,設定cwnd為
ssthresh,接看就進入了擁塞避免算法】沒什麼
意義。
3.對ssthresh的變化介紹的比較含糊。”
- 是 1/cwnd,你可以在 RFC2581 第 3 頁找到答案
- 沒有寫反,同樣你可以在 RFC2581 第 5 頁找到答案
- ssthresh 就是慢啟動門限,我覺得 ssthresh 我已經說的很清楚了,當然你可以找其他資料補充你的疑惑
關注公衆号:「小林coding」 ,回複「我要學習」即可免費獲得「伺服器 Linux C/C++ 」成長路程(書籍資料 + 思維導圖)