默默的一块石头
分类: LINUX
2020-08-10 14:32:54
相信大家都知道 TCP 是一個可靠傳輸的協議,那它是如何保證可靠的呢?爲了實現可靠性傳輸,需要考慮很多事情,例如數據的破壞、丟包、重複以及分片順序混亂等問題。如不能解決這些問題,也就無從談起可靠傳輸。那麼,TCP 是通過序列號、確認應答、重發控制、連接管理以及窗口控制等機制實現可靠性傳輸的。今天,將重點介紹 TCP 的重傳機制、滑動窗口、流量控制、擁塞控制。提綱
TCP 實現可靠傳輸的方式之一,是通過序列號與確認應答。在 TCP 中,當發送端的數據到達接收主機時,接收端主機會返回一個確認應答消息,表示已收到消息。正常的數據傳輸但在錯綜複雜的網絡,並不一定能如上圖那麼順利能正常的數據傳輸,萬一數據在傳輸過程中丟失了呢?所以 TCP 針對數據包丟失的情況,會用重傳機制解決。接下來說說常見的重傳機制:
重傳機制的其中一個方式,就是在發送數據時,設定一個定時器,當超過指定的時間後,沒有收到對方的 ACK 確認應答報文,就會重發該數據,也就是我們常說的超時重傳。TCP 會在以下兩種情況發生超時重傳:
超時重傳的兩種情況
超時時間應該設置爲多少呢?
我們先來了解一下什麼是 RTT (Round-Trip Time 往返時延),從下圖我們就可以知道:RTTRTT 就是數據從網絡一端傳送到另一端所需的時間,也就是包的往返時間。超時重傳時間是以 RTO (Retransmission Timeout 超時重傳時間)表示。假設在重傳的情況下,超時時間 RTO 「較長或較短」時,會發生什麼事情呢?超時時間較長與較短上圖中有兩種超時時間不同的情況:
精確的測量超時時間 RTO 的值是非常重要的,這可讓我們的重傳機制更高效。根據上述的兩種情況,我們可以得知,超時重傳時間 RTO 的值應該略大於報文往返 RTT 的值。RTO 應略大於 RTT 至此,可能大家覺得超時重傳時間 RTO 的值計算,也不是很複雜嘛。好像就是在發送端發包時記下 t0 ,然後接收端再把這個 ack 回來時再記一個 t1,於是 RTT = t1 – t0。沒那麼簡單,這只是一個採樣,不能代表普遍情況。實際上「報文往返 RTT 的值」是經常變化的,因爲我們的網絡也是時常變化的。也就因爲「報文往返 RTT 的值」 是經常波動變化的,所以「超時重傳時間 RTO 的值」應該是一個動態變化的值。我們來看看 Linux 是如何計算 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 份數據:
所以,快速重傳的工作方式是當收到三個相同的 ACK 報文時,會在定時器過期之前,重傳丟失的報文段。快速重傳機制只解決了一個問題,就是超時時間的問題,但是它依然面臨着另外一個問題。就是重傳的時候,是重傳之前的一個,還是重傳所有的問題。比如對於上面的例子,是重傳 Seq2 呢?還是重傳 Seq2、Seq3、Seq4、Seq5 呢?因爲發送端並不清楚這連續的三個 Ack 2 是誰傳回來的。根據 TCP 不同的實現,以上兩種情況都是有可能的。可見,這是一把雙刃劍。爲了解決不知道該重傳哪些 TCP 報文,於是就有 SACK 方法。
還有一種實現重傳機制的方式叫:SACK ( Selective Acknowledgment 選擇性確認)。這種方式需要在 TCP 頭部「選項」字段里加一個 SACK 的東西,它可以將緩存的地圖發送給發送方,這樣發送方就可以知道哪些數據收到了,哪些數據沒收到,知道了這些信息,就可以只重傳丟失的數據。如下圖,發送方收到了三次同樣的 ACK 確認報文,於是就會觸發快速重發機制,通過 SACK 信息發現只有 200~299 這段數據丟失,則重發時,就只選擇了這個 TCP 段進行重複。選擇性確認如果要支持 SACK,必須雙方都要支持。在 Linux 下,可以通過 net.ipv4.tcp_sack 參數打開這個功能(Linux 2.4 後默認打開)。
Duplicate SACK 又稱 D-SACK,其主要使用了 SACK 來告訴「發送方」有哪些數據被重複接收了。下面舉例兩個栗子,來說明 D-SACK 的作用。栗子一號:ACK 丟包ACK 丟包
栗子二號:網絡延時網絡延時
可見,D-SACK 有這麼幾個好處:
在 Linux 下可以通過 net.ipv4.tcp_dsack 參數開啓 / 關閉這個功能(Linux 2.4 後默認打開)。
**
**
引入窗口概念的原因
我們都知道 TCP 是每發送一個數據,都要進行一次確認應答。當上一個數據包收到了應答了, 再發送下一個。這個模式就有點像我和你面對面聊天,你一句我一句。但這種方式的缺點是效率比較低的。如果你說完一句話,我在處理其他事情,沒有及時回覆你,那你不是要乾等着我做完其他事情後,我回復你,你才能說下一句話,很顯然這不現實。按數據包進行確認應答所以,這樣的傳輸方式有一個缺點:數據包的往返時間越長,通信的效率就越低。爲解決這個問題,TCP 引入了窗口這個概念。即使在往返時間較長的情況下,它也不會降低網絡通信的效率。那麼有了窗口,就可以指定窗口大小,窗口大小就是指無需等待確認應答,而可以繼續發送數據的最大值。窗口的實現實際上是操作系統開闢的一個緩存空間,發送方主機在等到確認應答返回之前,必須在緩衝區中保留已發送的數據。如果按期收到確認應答,此時數據就可以從緩存區清除。假設窗口大小爲 3 個 TCP 段,那麼發送方就可以「連續發送」 3 個 TCP 段,並且中途若有 ACK 丟失,可以通過「下一個確認應答進行確認」。如下圖:用滑動窗口方式並行處理圖中的 ACK 600 確認應答報文丟失,也沒關係,因爲可以通話下一個確認應答進行確認,只要發送方收到了 ACK 700 確認應答,就意味着 700 之前的所有數據「接收方」都收到了。這個模式就叫累計確認或者累計應答。
窗口大小由哪一方決定?
TCP 頭裏有一個字段叫 Window,也就是窗口大小。這個字段是接收端告訴發送端自己還有多少緩衝區可以接收數據。於是發送端就可以根據這個接收端的處理能力來發送數據,而不會導致接收端處理不過來。所以,通常窗口的大小是由接收方的決定的。發送方發送的數據大小不能超過接收方的窗口大小,否則接收方就無法正常接收到數據。
發送方的滑動窗口
我們先來看看發送方的窗口,下圖就是發送方緩存的數據,根據處理的情況分成四個部分,其中深藍色方框是發送窗口,紫色方框是可用窗口:
在下圖,當發送方把數據「全部」都一下發送出去後,可用窗口的大小就爲 0 了,表明可用窗口耗盡,在沒收到 ACK 確認之前是無法繼續發送數據了。可用窗口耗盡在下圖,當收到之前發送的數據 32~36 字節的 ACK 確認應答後,如果發送窗口的大小沒有變化,則滑動窗口往右邊移動 5 個字節,因爲有 5 個字節的數據被應答確認,接下來 52~56 字節又變成了可用窗口,那麼後續也就可以發送 52~56 這 5 個字節的數據了。32 ~ 36 字節已確認
程序是如何表示發送方的四個部分的呢?
TCP 滑動窗口方案使用三個指針來跟蹤在四個傳輸類別中的每一個類別中的字節。其中兩個指針是絕對指針(指特定的序列號),一個是相對指針(需要做偏移)。SND.WND、SND.UN、SND.NXT
那麼可用窗口大小的計算就可以是:可用窗口大 = SND.WND -(SND.NXT - SND.UNA)
接收方的滑動窗口
接下來我們看看接收方的窗口,接收窗口相對簡單一些,根據處理的情況劃分成三個部分:
接收窗口其中三個接收部分,使用兩個指針進行劃分 :
接收窗口和發送窗口的大小是相等的嗎?
並不是完全相等,接收窗口的大小是約等於發送窗口的大小的。因爲滑動窗口並不是一成不變的。比如,當接收方的應用進程讀取數據的速度非常快的話,這樣的話接收窗口可以很快的就空缺出來。那麼新的接收窗口大小,是通過 TCP 報文中的 Windows 字段來告訴發送方。那麼這個傳輸過程是存在時延的,所以接收窗口和發送窗口是約等於的關係。
發送方不能無腦的發數據給接收方,要考慮接收方處理能力。如果一直無腦的發數據給對方,但對方處理不過來,那麼就會導致觸發重發機制,從而導致網絡流量的無端的浪費。爲了解決這種現象發生,TCP 提供一種機制可以讓「發送方」根據「接收方」的實際接收能力控制發送的數據量,這就是所謂的流量控制。下面舉個栗子,爲了簡單起見,假設以下場景:
流量控制根據上圖的流量控制,說明下每個過程:
前面的流量控制例子,我們假定了發送窗口和接收窗口是不變的,但是實際上,發送窗口和接收窗口中所存放的字節數,都是放在操作系統內存緩衝區中的,而操作系統的緩衝區,會被操作系統調整。當應用進程沒辦法及時讀取緩衝區的內容時,也會對我們的緩衝區造成影響。
那操心繫統的緩衝區,是如何影響發送窗口和接收窗口的呢?
我們先來看看第一個例子。當應用程序沒有及時讀取緩存時,發送窗口和接收窗口的變化。考慮以下場景:
根據上圖的流量控制,說明下每個過程:
可見最後窗口都收縮爲 0 了,也就是發生了窗口關閉。當發送方可用窗口變爲 0 時,發送方實際上會定時發送窗口探測報文,以便知道接收方的窗口是否發生了改變,這個內容後面會說,這裏先簡單提一下。我們先來看看第二個例子。當服務端系統資源非常緊張的時候,操心繫統可能會直接減少了接收緩衝區大小,這時應用程序又無法及時讀取緩存數據,那麼這時候就有嚴重的事情發生了,會出現數據包丟失的現象。說明下每個過程:
所以,如果發生了先減少緩存,再收縮窗口,就會出現丟包的現象。爲了防止這種情況發生,TCP 規定是不允許同時減少緩存又收縮窗口的,而是採用先收縮窗口,過段時間在減少緩存,這樣就可以避免了丟包情況。
在前面我們都看到了,TCP 通過讓接收方指明希望從發送方接收的數據大小(窗口大小)來進行流量控制。如果窗口大小爲 0 時,就會阻止發送方給接收方傳遞數據,直到窗口變爲非 0 爲止,這就是窗口關閉。
窗口關閉潛在的危險
接收方向發送方通告窗口大小時,是通過 ACK 報文來通告的。那麼,當發生窗口關閉時,接收方處理完數據後,會向發送方通告一個窗口非 0 的 ACK 報文,如果這個通告窗口的 ACK 報文在網絡中丟失了,那麻煩就大了。窗口關閉潛在的危險這會導致發送方一直等待接收方的非 0 窗口通知,接收方也一直等待發送方的數據,如不不採取措施,這種相互等待的過程,會造成了死鎖的現象。
TCP 是如何解決窗口關閉時,潛在的死鎖現象呢?
爲了解決這個問題,TCP 爲每個連接設有一個持續定時器,只要 TCP 連接一方收到對方的零窗口通知,就啓動持續計時器。如果持續計時器超時,就會發送窗口探測 ( Windowprobe ) 報文,而對方在確認這個探測報文時,給出自己現在的接收窗口大小。窗口探測
窗口探查探測的次數一般爲 3 此次,每次次大約 30-60 秒(不同的實現可能會不一樣)。如果 3 次過後接收窗口還是 0 的話,有的 TCP 實現就會發 RST 報文來中斷連接。
如果接收方太忙了,來不及取走接收窗口裏的數據,那麼就會導致發送方的發送窗口越來越小。到最後,如果接收方騰出幾個字節並告訴發送方現在有幾個字節的窗口,而發送方會義無反顧地發送這幾個字節,這就是糊塗窗口綜合症。要知道,我們的 TCP + IP 頭有 40 個字節,爲了傳輸那幾個字節的數據,要達上這麼大的開銷,這太不經濟了。就好像一個可以承載 50 人的大巴車,每次來了一兩個人,就直接發車。除非家裏有礦的大巴司機,纔敢這樣玩,不然遲早破產。要解決這個問題也不難,大巴司機等乘客數量超過了 25 個,才認定可以發車。現舉個糊塗窗口綜合症的栗子,考慮以下場景:接收方的窗口大小是 360 字節,但接收方由於某些原因陷入困境,假設接收方的應用層讀取的能力如下:
糊塗窗口綜合症每個過程的窗口大小的變化,在圖中都描述的很清楚了,可以發現窗口不斷減少了,並且發送的數據都是比較小的了。所以,糊塗窗口綜合症的現象是可以發生在發送方和接收方:
於是,要解決糊塗窗口綜合症,就解決上面兩個問題就可以了
怎麼讓接收方不通告小窗口呢?
接收方通常的策略如下 : 當「窗口大小」小於 min( MSS,緩存空間 /2 ) ,也就是小於 MSS 與 1/2 緩存大小中的最小值時,就會向發送方通告窗口爲 0,也就阻止了發送方再發數據過來。等到接收方處理了一些數據後,窗口大小 >= MSS,或者接收方緩存空間有一半可以使用,就可以把窗口打開讓發送方發送數據過來。
怎麼讓發送方避免發送小數據呢?
發送方通常的策略 : 使用 Nagle 算法,該算法的思路是延時處理,它滿足以下兩個條件中的一條纔可以發送數據:
只要沒滿足上面條件中的一條,發送方一直在囤積數據,直到滿足上面的發送條件。另外,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 變化的規則:
那麼怎麼知道當前網絡是否出現了擁塞呢?
其實只要「發送方」沒有在規定時間內接收到 ACK 應答報文,也就是發生了超時重傳,就會認爲網絡出現了用擁塞。
擁塞控制有哪些控制算法?
擁塞控制主要是四個算法:
TCP 在剛建立連接完成後,首先是有個慢啓動的過程,這個慢啓動的意思就是一點一點的提高發送數據包的數量,如果一上來就發大量的數據,這不是給網絡添堵嗎?慢啓動的算法記住一個規則就行:當發送方每收到一個 ACK,就擁塞窗口 cwnd 的大小就會加 1。這裏假定擁塞窗口 cwnd 和發送窗口 swnd 相等,下面舉個栗子:
慢啓動算法可以看出慢啓動算法,發包的個數是指數性的增長。
那慢啓動漲到什麼時候是個頭呢?
有一個叫慢啓動門限 ssthresh (slow start threshold)狀態變量。
前面說道,當擁塞窗口 cwnd 「超過」慢啓動門限 ssthresh 就會進入擁塞避免算法。一般來說 ssthresh 的大小是 65535 字節。那麼進入擁塞避免算法後,它的規則是:每當收到一個 ACK 時,cwnd 增加 1/cwnd。接上前面的慢啓動的栗子,現假定 ssthresh 爲 8:
擁塞避免所以,我們可以發現,擁塞避免算法就是將原本慢啓動算法的指數增長變成了線性增長,還是增長階段,但是增長速度緩慢了一些。就這麼一直增長着後,網絡就會慢慢進入了擁塞的狀況了,於是就會出現丟包現象,這時就需要對丟失的數據包進行重傳。當觸發了重傳機制,也就進入了「擁塞發生算法」。
當網絡出現擁塞,也就是會發生數據包重傳,重傳機制主要有兩種:
這兩種使用的擁塞發送算法是不同的,接下來分別來說說。
發生超時重傳的擁塞發生算法
當發生了「超時重傳」,則就會使用擁塞發生算法。這個時候,sshresh 和 cwnd 的值會發生變化:
擁塞發送 —— 超時重傳接着,就重新開始慢啓動,慢啓動是會突然減少數據流的。這真是一旦「超時重傳」,馬上回到解放前。但是這種方式太激進了,反應也很強烈,會造成網絡卡頓。就好像本來在秋名山高速漂移着,突然來個緊急剎車,輪胎受得了嗎。。。
發生快速重傳的擁塞發生算法
還有更好的方式,前面我們講過「快速重傳算法」。當接收方發現丟了一箇中間包的時候,發送三次前一個包的 ACK,於是發送端就會快速地重傳,不必等待超時再重傳。TCP 認爲這種情況不嚴重,因爲大部分沒丟,只丟了一小部分,則 ssthresh 和 cwnd 變化如下:
快速重傳和快速恢復算法一般同時使用,快速恢復算法是認爲,你還能收到 3 個重複 ACK 說明網絡也不那麼糟糕,所以沒有必要像 RTO 超時那麼強烈。正如前面所說,進入快速恢復之前,cwnd 和 ssthresh 已被更新了:
然後,進入快速恢復算法如下:
快速重傳和快速恢復也就是沒有像「超時重傳」一夜回到解放前,而是還在比較高的值,後續呈線性增長。
資料:
[1] 趣談網絡協議專欄 . 劉超 . 極客時間
[2] Web 協議詳解與抓包實戰專欄 . 陶輝 . 極客時間
[3] TCP/IP 詳解 卷 1:協議 . 範建華 譯 . 機械工業出版社
[4] 圖解 TCP/IP. 竹下隆史 . 人民郵電出版社
[5] The TCP/IP Guide.Charles M. Kozierok.
[6] TCP 那些事(上). 陳皓 . 酷殼博客 .
[7] TCP 那些事(下). 陳皓 . 酷殼博客 .