前一篇「硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題」獲得了不少讀者的承認,在此特別感謝大家的承認,你們都暖暖的。html
來了,今天又來圖解 TCP 了,小林可能會遲到,但不會缺席。web
遲到的緣由,主要是 TCP 巨複雜,它爲了保證可靠性,用了巨多的機制來保證,真是個「偉大」的協議,寫着寫着發現這水太深了。。。面試
本文的所有圖片都是小林繪畫的,很是的辛苦且累,不廢話了,直接進入正文,Go!算法
相信你們都知道 TCP 是一個可靠傳輸的協議,那它是如何保證可靠的呢?shell
爲了實現可靠性傳輸,須要考慮不少事情,例如數據的破壞、丟包、重複以及分片順序混亂等問題。如不能解決這些問題,也就無從談起可靠傳輸。緩存
那麼,TCP 是經過序列號、確認應答、重發控制、鏈接管理以及窗口控制等機制實現可靠性傳輸的。網絡
今天,將重點介紹 TCP 的重傳機制、滑動窗口、流量控制、擁塞控制。app
提綱TCP 實現可靠傳輸的方式之一,是經過序列號與確認應答。ssh
在 TCP 中,當發送端的數據到達接收主機時,接收端主機會返回一個確認應答消息,表示已收到消息。tcp
正常的數據傳輸但在錯綜複雜的網絡,並不必定能如上圖那麼順利能正常的數據傳輸,萬一數據在傳輸過程當中丟失了呢?
因此 TCP 針對數據包丟失的狀況,會用重傳機制解決。
接下來講說常見的重傳機制:
重傳機制的其中一個方式,就是在發送數據時,設定一個定時器,當超過指定的時間後,沒有收到對方的 ACK
確認應答報文,就會重發該數據,也就是咱們常說的超時重傳。
TCP 會在如下兩種狀況發生超時重傳:
超時時間應該設置爲多少呢?
咱們先來了解一下什麼是 RTT
(Round-Trip Time 往返時延),從下圖咱們就能夠知道:
RTT
就是數據從網絡一端傳送到另外一端所需的時間,也就是包的往返時間。
超時重傳時間是以 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 呢?仍是重傳 Seq二、Seq三、Seq四、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
。栗子二號:網絡延時
網絡延時可見,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 個字節的數據了。
程序是如何表示發送方的四個部分的呢?
TCP 滑動窗口方案使用三個指針來跟蹤在四個傳輸類別中的每個類別中的字節。其中兩個指針是絕對指針(指特定的序列號),一個是相對指針(須要作偏移)。
SND.WND、SND.UN、SND.NXTSND.WND
:表示發送窗口的大小(大小是由接收方指定的);
SND.UNA
:是一個絕對指針,它指向的是已發送但未收到確認的第一個字節的序列號,也就是 #2 的第一個字節。
SND.NXT
:也是一個絕對指針,它指向未發送但可發送範圍的第一個字節的序列號,也就是 #3 的第一個字節。
指向 #4 的第一個字節是個相對指針,它須要 SND.UNA
指針加上 SND.WND
大小的偏移量,就能夠指向 #4 的第一個字節了。
那麼可用窗口大小的計算就能夠是:
可用窗口大 = SND.WND -(SND.NXT - SND.UNA)
接收方的滑動窗口
接下來咱們看看接收方的窗口,接收窗口相對簡單一些,根據處理的狀況劃分紅三個部分:
其中三個接收部分,使用兩個指針進行劃分:
RCV.WND
:表示接收窗口的大小,它會通告給發送方。RCV.NXT
:是一個指針,它指向指望從發送方發送來的下一個數據字節的序列號,也就是 #3 的第一個字節。RCV.NXT
指針加上 RCV.WND
大小的偏移量,就能夠指向 #4 的第一個字節了。接收窗口和發送窗口的大小是相等的嗎?
並非徹底相等,接收窗口的大小是約等於發送窗口的大小的。
由於滑動窗口並非一成不變的。好比,當接收方的應用進程讀取數據的速度很是快的話,這樣的話接收窗口能夠很快的就空缺出來。那麼新的接收窗口大小,是經過 TCP 報文中的 Windows 字段來告訴發送方。那麼這個傳輸過程是存在時延的,因此接收窗口和發送窗口是約等於的關係。
發送方不能無腦的發數據給接收方,要考慮接收方處理能力。
若是一直無腦的發數據給對方,但對方處理不過來,那麼就會致使觸發重發機制,從而致使網絡流量的無故的浪費。
爲了解決這種現象發生,TCP 提供一種機制可讓「發送方」根據「接收方」的實際接收能力控制發送的數據量,這就是所謂的流量控制。
下面舉個栗子,爲了簡單起見,假設如下場景:
200
根據上圖的流量控制,說明下每一個過程:
Usable
減小爲 120 字節,同時 SND.NXT
指針也向右偏移 80 字節後,指向 321,這意味着下次發送數據的時候,序列號是 321。RCV.NXT
也就指向 321,這意味着客戶端指望的下一個報文的序列號是 321,接着發送確認報文給服務端。RCV.NXT
也就指向 441,接着發送確認報文給服務端。SND.UNA
指針往右偏移後指向 321,因而可用窗口 Usable
增大到 80。SND.UNA
指針往右偏移後指向 441,因而可用窗口 Usable
增大到 200。SND.NXT
指向 601,因而可用窗口 Usable
減小到 40。RCV.NXT
也就是指向了 601,接着發送確認報文給服務端。SND.UNA
指針偏移了 160 後指向 601,可用窗口 Usable
也就增大至了 200。前面的流量控制例子,咱們假定了發送窗口和接收窗口是不變的,可是實際上,發送窗口和接收窗口中所存放的字節數,都是放在操做系統內存緩衝區中的,而操做系統的緩衝區,會被操做系統調整。
當應用進程沒辦法及時讀取緩衝區的內容時,也會對咱們的緩衝區形成影響。
那操心繫統的緩衝區,是如何影響發送窗口和接收窗口的呢?
咱們先來看看第一個例子。
當應用程序沒有及時讀取緩存時,發送窗口和接收窗口的變化。
考慮如下場景:
360
;根據上圖的流量控制,說明下每一個過程:
可見最後窗口都收縮爲 0 了,也就是發生了窗口關閉。當發送方可用窗口變爲 0 時,發送方實際上會定時發送窗口探測報文,以便知道接收方的窗口是否發生了改變,這個內容後面會說,這裏先簡單提一下。
咱們先來看看第二個例子。
當服務端系統資源很是緊張的時候,操心繫統可能會直接減小了接收緩衝區大小,這時應用程序又沒法及時讀取緩存數據,那麼這時候就有嚴重的事情發生了,會出現數據包丟失的現象。
說明下每一個過程:
因此,若是發生了先減小緩存,再收縮窗口,就會出現丟包的現象。
爲了防止這種狀況發生,TCP 規定是不容許同時減小緩存又收縮窗口的,而是採用先收縮窗口,過段時間在減小緩存,這樣就能夠避免了丟包狀況。
在前面咱們都看到了,TCP 經過讓接收方指明但願從發送方接收的數據大小(窗口大小)來進行流量控制。
若是窗口大小爲 0 時,就會阻止發送方給接收方傳遞數據,直到窗口變爲非 0 爲止,這就是窗口關閉。
窗口關閉潛在的危險
接收方向發送方通告窗口大小時,是經過 ACK
報文來通告的。
那麼,當發生窗口關閉時,接收方處理完數據後,會向發送方通告一個窗口非 0 的 ACK 報文,若是這個通告窗口的 ACK 報文在網絡中丟失了,那麻煩就大了。
窗口關閉潛在的危險這會致使發送方一直等待接收方的非 0 窗口通知,接收方也一直等待發送方的數據,如不不採起措施,這種相互等待的過程,會形成了死鎖的現象。
TCP 是如何解決窗口關閉時,潛在的死鎖現象呢?
爲了解決這個問題,TCP 爲每一個鏈接設有一個持續定時器,只要 TCP 鏈接一方收到對方的零窗口通知,就啓動持續計時器。
若是持續計時器超時,就會發送窗口探測 ( Window probe ) 報文,而對方在確認這個探測報文時,給出本身如今的接收窗口大小。
窗口探測窗口探查探測的次數通常爲 3 這次,每次次大約 30-60 秒(不一樣的實現可能會不同)。若是 3 次事後接收窗口仍是 0 的話,有的 TCP 實現就會發 RST
報文來中斷鏈接。
若是接收方太忙了,來不及取走接收窗口裏的數據,那麼就會致使發送方的發送窗口愈來愈小。
到最後,若是接收方騰出幾個字節並告訴發送方如今有幾個字節的窗口,而發送方會義無反顧地發送這幾個字節,這就是糊塗窗口綜合症。
要知道,咱們的 TCP + IP
頭有 40
個字節,爲了傳輸那幾個字節的數據,要達上這麼大的開銷,這太不經濟了。
就好像一個能夠承載 50 人的大巴車,每次來了一兩我的,就直接發車。除非家裏有礦的大巴司機,纔敢這樣玩,否則早晚破產。要解決這個問題也不難,大巴司機等乘客數量超過了 25 個,才認定能夠發車。
現舉個糊塗窗口綜合症的栗子,考慮如下場景:
接收方的窗口大小是 360 字節,但接收方因爲某些緣由陷入困境,假設接收方的應用層讀取的能力以下:
每一個過程的窗口大小的變化,在圖中都描述的很清楚了,能夠發現窗口不斷減小了,而且發送的數據都是比較小的了。
因此,糊塗窗口綜合症的現象是能夠發生在發送方和接收方:
因而,要解決糊塗窗口綜合症,就解決上面兩個問題就能夠了
怎麼讓接收方不通告小窗口呢?
接收方一般的策略以下:
當「窗口大小」小於 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
大小的數據。能夠看出慢啓動算法,發包的個數是指數性的增加。
那慢啓動漲到何時是個頭呢?
有一個叫慢啓動門限 ssthresh
(slow start threshold)狀態變量。
cwnd
< ssthresh
時,使用慢啓動算法。cwnd
>= ssthresh
時,就會使用「擁塞避免算法」。前面說道,當擁塞窗口 cwnd
「超過」慢啓動門限 ssthresh
就會進入擁塞避免算法。
通常來講 ssthresh
的大小是 65535
字節。
那麼進入擁塞避免算法後,它的規則是:每當收到一個 ACK 時,cwnd 增長 1/cwnd。
接上前面的慢啓動的栗子,現假定 ssthresh
爲 8
:
MSS
大小的數據,變成了線性增加。因此,咱們能夠發現,擁塞避免算法就是將本來慢啓動算法的指數增加變成了線性增加,仍是增加階段,可是增加速度緩慢了一些。
就這麼一直增加着後,網絡就會慢慢進入了擁塞的情況了,因而就會出現丟包現象,這時就須要對丟失的數據包進行重傳。
當觸發了重傳機制,也就進入了「擁塞發生算法」。
當網絡出現擁塞,也就是會發生數據包重傳,重傳機制主要有兩種:
這兩種使用的擁塞發送算法是不一樣的,接下來分別來講說。
發生超時重傳的擁塞發生算法
當發生了「超時重傳」,則就會使用擁塞發生算法。
這個時候,sshresh 和 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
;而後,進入快速恢復算法以下:
cwnd = ssthresh + 3
( 3 的意思是確認有 3 個數據包被收到了)也就是沒有像「超時重傳」一晚上回到解放前,而是還在比較高的值,後續呈線性增加。
[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 三次握手和四次揮手面試題
是吧? TCP 巨複雜吧?看完很累吧?
但這還只是 TCP 冰山一腳,它的更深處就由大家本身去探索啦。
本文只是拋磚引玉,若你有更好的想法或文章有誤的地方,歡迎留言討論!
小林是專爲你們圖解的工具人,Goodbye,咱們下次見!