TCP協(xié)議如何保證可靠傳輸

image.png

(轉(zhuǎn)載: https://xiaolincoding.com/network/3_tcp/tcp_feature.html)

重傳機(jī)制

所以 TCP 針對數(shù)據(jù)包丟失的情況,會用重傳機(jī)制解決。

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

超時重傳

重傳機(jī)制的其中一個方式,就是在發(fā)送數(shù)據(jù)時,設(shè)定一個定時器,當(dāng)超過指定的時間后,沒有收到對方的 ACK 確認(rèn)應(yīng)答報文,就會重發(fā)該數(shù)據(jù),也就是我們常說的超時重傳。

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

  • 數(shù)據(jù)包丟失
  • 確認(rèn)應(yīng)答丟失


    image.png

超時時間應(yīng)該設(shè)置為多少呢?

我們先來了解一下什么是 RTT(Round-Trip Time 往返時延),從下圖我們就可以知道:
RTT 指的是數(shù)據(jù)發(fā)送時刻到接收到確認(rèn)的時刻的差值,也就是包的往返時間。

image.png

image.png

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

  • 當(dāng)超時時間RTO 較大時,重發(fā)就慢,丟了老半天才重發(fā),沒有效率,性能差;
  • 當(dāng)超時時間RTO 較小時,會導(dǎo)致可能并沒有丟就重發(fā),于是重發(fā)的就快,會增加網(wǎng)絡(luò)擁塞,導(dǎo)致更多的超時,更多的超時導(dǎo)致更多的重發(fā)。

精確的測量超時時間 RTO 的值是非常重要的,這可讓我們的重傳機(jī)制更高效。
根據(jù)上述的兩種情況,我們可以得知,超時重傳時間 RTO 的值應(yīng)該略大于報文往返 RTT 的值。

image.png
image.png

如果超時重發(fā)的數(shù)據(jù),再次超時的時候,又需要重傳的時候,TCP 的策略是超時間隔加倍。

也就是每當(dāng)遇到一次超時重傳的時候,都會將下一次超時時間間隔設(shè)為先前值的兩倍。兩次超時,就說明網(wǎng)絡(luò)環(huán)境差,不宜頻繁反復(fù)發(fā)送。

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

于是就可以用「快速重傳」機(jī)制來解決超時重發(fā)的時間等待。

快速重傳

TCP 還有另外一種快速重傳(Fast Retransmit)機(jī)制,它不以時間為驅(qū)動,而是以數(shù)據(jù)驅(qū)動重傳。

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

image.png

在上圖,發(fā)送方發(fā)出了 1,2,3,4,5 份數(shù)據(jù):

  • 第一份 Seq1 先送到了,于是就 Ack 回 2;
  • 結(jié)果 Seq2 因為某些原因沒收到,Seq3 到達(dá)了,于是還是 Ack 回 2;
  • 后面的 Seq4 和 Seq5 都到了,但還是 Ack 回 2,因為 Seq2 還是沒有收到;
  • 發(fā)送端收到了三個 Ack = 2 的確認(rèn),知道了 Seq2 還沒有收到,就會在定時器過期之前,重傳丟失的 Seq2
  • 最后,收到了 Seq2,此時因為 Seq3,Seq4,Seq5 都收到了,于是 Ack 回 6 。
    所以,快速重傳的工作方式是當(dāng)收到三個相同的 ACK 報文時,會在定時器過期之前,重傳丟失的報文段。

SACK

還有一種實現(xiàn)重傳機(jī)制的方式叫:SACK( Selective Acknowledgment 選擇性確認(rèn))。

這種方式需要在 TCP 頭部「選項」字段里加一個 SACK 的東西,它可以將緩存的地圖發(fā)送給發(fā)送方,這樣發(fā)送方就可以知道哪些數(shù)據(jù)收到了,哪些數(shù)據(jù)沒收到,知道了這些信息,就可以只重傳丟失的數(shù)據(jù)。

如下圖,發(fā)送方收到了三次同樣的 ACK 確認(rèn)報文,于是就會觸發(fā)快速重發(fā)機(jī)制,通過 SACK 信息發(fā)現(xiàn)只有 200~299 這段數(shù)據(jù)丟失,則重發(fā)時,就只選擇了這個 TCP 段進(jìn)行重復(fù)。

image.png

Duplicate SACK

Duplicate SACK 又稱 D-SACK,其主要使用了 SACK 來告訴「發(fā)送方」有哪些數(shù)據(jù)被重復(fù)接收了。

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


image.png
  • 「接收方」發(fā)給「發(fā)送方」的兩個 ACK 確認(rèn)應(yīng)答都丟失了,所以發(fā)送方超時后,重傳第一個數(shù)據(jù)包(3000 ~ 3499)
  • 于是「接收方」發(fā)現(xiàn)數(shù)據(jù)是重復(fù)收到的,于是回了一個 SACK = 3000~3500,告訴「發(fā)送方」 3000~3500 的數(shù)據(jù)早已被接收了,因為 ACK 都到了 4000 了,已經(jīng)意味著 4000 之前的所有數(shù)據(jù)都已收到,所以這個 SACK 就代表著 D-SACK。
  • 這樣「發(fā)送方」就知道了,數(shù)據(jù)沒有丟,是「接收方」的 ACK 確認(rèn)報文丟了。


    image.png
  • 數(shù)據(jù)包(1000~1499) 被網(wǎng)絡(luò)延遲了,導(dǎo)致「發(fā)送方」沒有收到 Ack 1500 的確認(rèn)報文。
  • 而后面報文到達(dá)的三個相同的 ACK 確認(rèn)報文,就觸發(fā)了快速重傳機(jī)制,但是在重傳后,被延遲的數(shù)據(jù)包(1000~1499)又到了「接收方」;
  • 所以「接收方」回了一個 SACK=1000~1500,因為 ACK 已經(jīng)到了 3000,所以這個 SACK 是 D-SACK,表示收到了重復(fù)的包。
  • 這樣發(fā)送方就知道快速重傳觸發(fā)的原因不是發(fā)出去的包丟了,也不是因為回應(yīng)的 ACK 包丟了,而是因為網(wǎng)絡(luò)延遲了。
    可見,D-SACK 有這么幾個好處:
  1. 可以讓「發(fā)送方」知道,是發(fā)出去的包丟了,還是接收方回應(yīng)的 ACK 包丟了;
  2. 可以知道是不是「發(fā)送方」的數(shù)據(jù)包被網(wǎng)絡(luò)延遲了;
  3. 可以知道網(wǎng)絡(luò)中是不是把「發(fā)送方」的數(shù)據(jù)包給復(fù)制了;
    在 Linux 下可以通過 net.ipv4.tcp_dsack 參數(shù)開啟/關(guān)閉這個功能(Linux 2.4 后默認(rèn)打開)

滑動窗口

  • 發(fā)送窗口
  • 接收窗口
    為什么要引入滑動窗口
    我們都知道 TCP 是每發(fā)送一個數(shù)據(jù),都要進(jìn)行一次確認(rèn)應(yīng)答。當(dāng)上一個數(shù)據(jù)包收到了應(yīng)答了, 再發(fā)送下一個。

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

如果你說完一句話,我在處理其他事情,沒有及時回復(fù)你,那你不是要干等著我做完其他事情后,我回復(fù)你,你才能說下一句話,很顯然這不現(xiàn)實。


image.png

所以,這樣的傳輸方式有一個缺點:數(shù)據(jù)包的往返時間越長,通信的效率就越低。

為解決這個問題,TCP 引入了窗口這個概念。即使在往返時間較長的情況下,它也不會降低網(wǎng)絡(luò)通信的效率。

那么有了窗口,就可以指定窗口大小,窗口大小就是指無需等待確認(rèn)應(yīng)答,而可以繼續(xù)發(fā)送數(shù)據(jù)的最大值。

窗口的實現(xiàn)實際上是操作系統(tǒng)開辟的一個緩存空間,發(fā)送方主機(jī)在等到確認(rèn)應(yīng)答返回之前,必須在緩沖區(qū)中保留已發(fā)送的數(shù)據(jù)。如果按期收到確認(rèn)應(yīng)答,此時數(shù)據(jù)就可以從緩存區(qū)清除。

假設(shè)窗口大小為 3 個 TCP 段,那么發(fā)送方就可以「連續(xù)發(fā)送」 3 個 TCP 段,并且中途若有 ACK 丟失,可以通過「下一個確認(rèn)應(yīng)答進(jìn)行確認(rèn)」。

TCP 頭里有一個字段叫 Window,也就是窗口大小。

這個字段是接收端告訴發(fā)送端自己還有多少緩沖區(qū)可以接收數(shù)據(jù)。于是發(fā)送端就可以根據(jù)這個接收端的處理能力來發(fā)送數(shù)據(jù),而不會導(dǎo)致接收端處理不過來。

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

發(fā)送方發(fā)送的數(shù)據(jù)大小不能超過接收方的窗口大小,否則接收方就無法正常接收到數(shù)據(jù)。

image.png

并不是完全相等,接收窗口的大小是約等于發(fā)送窗口的大小的。

因為滑動窗口并不是一成不變的。比如,當(dāng)接收方的應(yīng)用進(jìn)程讀取數(shù)據(jù)的速度非??斓脑挘@樣的話接收窗口可以很快的就空缺出來。那么新的接收窗口大小,是通過 TCP 報文中的 Windows 字段來告訴發(fā)送方。那么這個傳輸過程是存在時延的,所以接收窗口和發(fā)送窗口是約等于的關(guān)系。


#流量

流量控制

  • 操作系統(tǒng)緩沖區(qū)與滑動窗口關(guān)系
  • 窗口關(guān)閉
  • 糊度窗口綜合征
    發(fā)送方不能無腦的發(fā)數(shù)據(jù)給接收方,要考慮接收方處理能力。

如果一直無腦的發(fā)數(shù)據(jù)給對方,但對方處理不過來,那么就會導(dǎo)致觸發(fā)重發(fā)機(jī)制,從而導(dǎo)致網(wǎng)絡(luò)流量的無端的浪費(fèi)。

為了解決這種現(xiàn)象發(fā)生,TCP 提供一種機(jī)制可以讓「發(fā)送方」根據(jù)「接收方」的實際接收能力控制發(fā)送的數(shù)據(jù)量,這就是所謂的流量控制。

前面的流量控制例子,我們假定了發(fā)送窗口和接收窗口是不變的,但是實際上,發(fā)送窗口和接收窗口中所存放的字節(jié)數(shù),都是放在操作系統(tǒng)內(nèi)存緩沖區(qū)中的,而操作系統(tǒng)的緩沖區(qū),會被操作系統(tǒng)調(diào)整。

image.png

可見最后窗口都收縮為 0 了,也就是發(fā)生了窗口關(guān)閉。當(dāng)發(fā)送方可用窗口變?yōu)?0 時,發(fā)送方實際上會定時發(fā)送窗口探測報文,以便知道接收方的窗口是否發(fā)生了改變,這個內(nèi)容后面會說,這里先簡單提一下。

image.png

所以,如果發(fā)生了先減少緩存,再收縮窗口,就會出現(xiàn)丟包的現(xiàn)象。

為了防止這種情況發(fā)生,TCP 規(guī)定是不允許同時減少緩存又收縮窗口的,而是采用先收縮窗口,過段時間再減少緩存,這樣就可以避免了丟包情況。

窗口關(guān)閉

在前面我們都看到了,TCP 通過讓接收方指明希望從發(fā)送方接收的數(shù)據(jù)大?。ù翱诖笮。﹣磉M(jìn)行流量控制。

image.png

這會導(dǎo)致發(fā)送方一直等待接收方的非 0 窗口通知,接收方也一直等待發(fā)送方的數(shù)據(jù),如不采取措施,這種相互等待的過程,會造成了死鎖的現(xiàn)象。

TCP 是如何解決窗口關(guān)閉時,潛在的死鎖現(xiàn)象呢?

image.png
  • 如果接收窗口仍然為 0,那么收到這個報文的一方就會重新啟動持續(xù)計時器;
  • 如果接收窗口不是 0,那么死鎖的局面就可以被打破了。

窗口探測的次數(shù)一般為 3 次,每次大約 30-60 秒(不同的實現(xiàn)可能會不一樣)。如果 3 次過后接收窗口還是 0 的話,有的 TCP 實現(xiàn)就會發(fā) RST 報文來中斷連接。

擁塞控制

  • 慢啟動
  • 擁塞避免
  • 擁塞發(fā)生
  • 快速恢復(fù)

為什么要有擁塞控制呀,不是有流量控制了嗎?
前面的流量控制是避免「發(fā)送方」的數(shù)據(jù)填滿「接收方」的緩存,但是并不知道網(wǎng)絡(luò)的中發(fā)生了什么。

一般來說,計算機(jī)網(wǎng)絡(luò)都處在一個共享的環(huán)境。因此也有可能會因為其他主機(jī)之間的通信使得網(wǎng)絡(luò)擁堵。

在網(wǎng)絡(luò)出現(xiàn)擁堵時,如果繼續(xù)發(fā)送大量數(shù)據(jù)包,可能會導(dǎo)致數(shù)據(jù)包時延、丟失等,這時 TCP 就會重傳數(shù)據(jù),但是一重傳就會導(dǎo)致網(wǎng)絡(luò)的負(fù)擔(dān)更重,于是會導(dǎo)致更大的延遲以及更多的丟包,這個情況就會進(jìn)入惡性循環(huán)被不斷地放大....

所以,TCP 不能忽略網(wǎng)絡(luò)上發(fā)生的事,它被設(shè)計成一個無私的協(xié)議,當(dāng)網(wǎng)絡(luò)發(fā)送擁塞時,TCP 會自我犧牲,降低發(fā)送的數(shù)據(jù)量。

于是,就有了擁塞控制,控制的目的就是避免「發(fā)送方」的數(shù)據(jù)填滿整個網(wǎng)絡(luò)。

為了在「發(fā)送方」調(diào)節(jié)所要發(fā)送數(shù)據(jù)的量,定義了一個叫做「擁塞窗口」的概念。

那么怎么知道當(dāng)前網(wǎng)絡(luò)是否出現(xiàn)了擁塞呢?
其實只要「發(fā)送方」沒有在規(guī)定時間內(nèi)接收到 ACK 應(yīng)答報文,也就是發(fā)生了超時重傳,就會認(rèn)為網(wǎng)絡(luò)出現(xiàn)了用擁塞。

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

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

  • 慢啟動
  • 擁塞避免
  • 擁塞發(fā)生
  • 快速恢復(fù)

慢啟動

TCP 在剛建立連接完成后,首先是有個慢啟動的過程,這個慢啟動的意思就是一點一點的提高發(fā)送數(shù)據(jù)包的數(shù)量,如果一上來就發(fā)大量的數(shù)據(jù),這不是給網(wǎng)絡(luò)添堵嗎?

慢啟動的算法記住一個規(guī)則就行:當(dāng)發(fā)送方每收到一個 ACK,擁塞窗口 cwnd 的大小就會加 1。

這里假定擁塞窗口 cwnd 和發(fā)送窗口 swnd 相等,下面舉個栗子:

連接建立完成后,一開始初始化 cwnd = 1,表示可以傳一個 MSS 大小的數(shù)據(jù)。
當(dāng)收到一個 ACK 確認(rèn)應(yīng)答后,cwnd 增加 1,于是一次能夠發(fā)送 2 個
當(dāng)收到 2 個的 ACK 確認(rèn)應(yīng)答后, cwnd 增加 2,于是就可以比之前多發(fā)2 個,所以這一次能夠發(fā)送 4 個
當(dāng)這 4 個的 ACK 確認(rèn)到來的時候,每個確認(rèn) cwnd 增加 1, 4 個確認(rèn) cwnd 增加 4,于是就可以比之前多發(fā) 4 個,所以這一次能夠發(fā)送 8 個。


image.png

可以看出慢啟動算法,發(fā)包的個數(shù)是指數(shù)性的增長

那慢啟動漲到什么時候是個頭呢?

有一個叫慢啟動門限 ssthresh (slow start threshold)狀態(tài)變量。

  • 當(dāng) cwnd < ssthresh 時,使用慢啟動算法。
  • 當(dāng) cwnd >= ssthresh 時,就會使用「擁塞避免算法」。

擁塞避免算法

前面說道,當(dāng)擁塞窗口 cwnd 「超過」慢啟動門限 ssthresh 就會進(jìn)入擁塞避免算法。

一般來說 ssthresh 的大小是 65535 字節(jié)。

那么進(jìn)入擁塞避免算法后,它的規(guī)則是:每當(dāng)收到一個 ACK 時,cwnd 增加 1/cwnd。

接上前面的慢啟動的栗子,現(xiàn)假定 ssthresh 為 8:

當(dāng) 8 個 ACK 應(yīng)答確認(rèn)到來時,每個確認(rèn)增加 1/8,8 個 ACK 確認(rèn) cwnd 一共增加 1,于是這一次能夠發(fā)送 9 個 MSS 大小的數(shù)據(jù),變成了線性增長。


image.png

所以,我們可以發(fā)現(xiàn),擁塞避免算法就是將原本慢啟動算法的指數(shù)增長變成了線性增長,還是增長階段,但是增長速度緩慢了一些。

就這么一直增長著后,網(wǎng)絡(luò)就會慢慢進(jìn)入了擁塞的狀況了,于是就會出現(xiàn)丟包現(xiàn)象,這時就需要對丟失的數(shù)據(jù)包進(jìn)行重傳。

當(dāng)觸發(fā)了重傳機(jī)制,也就進(jìn)入了「擁塞發(fā)生算法」。

擁塞發(fā)生

當(dāng)網(wǎng)絡(luò)出現(xiàn)擁塞,也就是會發(fā)生數(shù)據(jù)包重傳,重傳機(jī)制主要有兩種:

  • 超時重傳
  • 快速重傳
    這兩種使用的擁塞發(fā)送算法是不同的,接下來分別來說說。

發(fā)生超時重傳的擁塞發(fā)生算法

當(dāng)發(fā)生了「超時重傳」,則就會使用擁塞發(fā)生算法。

這個時候,ssthresh 和 cwnd 的值會發(fā)生變化:

  • ssthresh 設(shè)為 cwnd/2,
  • cwnd 重置為 1


    image.png

還有更好的方式,前面我們講過「快速重傳算法」。當(dāng)接收方發(fā)現(xiàn)丟了一個中間包的時候,發(fā)送三次前一個包的 ACK,于是發(fā)送端就會快速地重傳,不必等待超時再重傳。

TCP 認(rèn)為這種情況不嚴(yán)重,因為大部分沒丟,只丟了一小部分,則 ssthreshcwnd 變化如下:

  • cwnd = cwnd/2 ,也就是設(shè)置為原來的一半;
  • ssthresh = cwnd;
  • 進(jìn)入快速恢復(fù)算法

快速恢復(fù)

快速重傳和快速恢復(fù)算法一般同時使用,快速恢復(fù)算法是認(rèn)為,你還能收到 3 個重復(fù) ACK 說明網(wǎng)絡(luò)也不那么糟糕,所以沒有必要像 RTO 超時那么強(qiáng)烈。

正如前面所說,進(jìn)入快速恢復(fù)之前,cwnd 和 ssthresh 已被更新了:

  • cwnd = cwnd/2 ,也就是設(shè)置為原來的一半;
  • ssthresh = cwnd;
    然后,進(jìn)入快速恢復(fù)算法如下:
  • 擁塞窗口 cwnd = ssthresh + 3 ( 3 的意思是確認(rèn)有 3 個數(shù)據(jù)包被收到了);
  • 重傳丟失的數(shù)據(jù)包;
  • 如果再收到重復(fù)的 ACK,那么 cwnd 增加 1;
  • 如果收到新數(shù)據(jù)的 ACK 后,把 cwnd 設(shè)置為第一步中的 ssthresh 的值,原因是該 ACK 確認(rèn)了新的數(shù)據(jù),說明從 duplicated ACK 時的數(shù)據(jù)都已收到,該恢復(fù)過程已經(jīng)結(jié)束,可以回到恢復(fù)之前的狀態(tài)了,也即再次進(jìn)入擁塞避免狀態(tài);


    image.png
image.png
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容