
(轉(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)的時刻的差值,也就是包的往返時間。


上圖中有兩種超時時間不同的情況:
- 當(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 的值。


如果超時重發(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ī)制,是如何工作的呢?其實很簡單,一圖勝千言。

在上圖,發(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ù)。

Duplicate SACK
Duplicate SACK 又稱 D-SACK,其主要使用了 SACK 來告訴「發(fā)送方」有哪些數(shù)據(jù)被重復(fù)接收了。
下面舉例兩個栗子,來說明 D-SACK 的作用。

- 「接收方」發(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 有這么幾個好處:
- 可以讓「發(fā)送方」知道,是發(fā)出去的包丟了,還是接收方回應(yīng)的 ACK 包丟了;
- 可以知道是不是「發(fā)送方」的數(shù)據(jù)包被網(wǎng)絡(luò)延遲了;
- 可以知道網(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)實。

所以,這樣的傳輸方式有一個缺點:數(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ù)。

并不是完全相等,接收窗口的大小是約等于發(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)整。

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

所以,如果發(fā)生了先減少緩存,再收縮窗口,就會出現(xiàn)丟包的現(xiàn)象。
為了防止這種情況發(fā)生,TCP 規(guī)定是不允許同時減少緩存又收縮窗口的,而是采用先收縮窗口,過段時間再減少緩存,這樣就可以避免了丟包情況。
窗口關(guān)閉
在前面我們都看到了,TCP 通過讓接收方指明希望從發(fā)送方接收的數(shù)據(jù)大?。ù翱诖笮。﹣磉M(jìn)行流量控制。

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

- 如果接收窗口仍然為 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 個。

可以看出慢啟動算法,發(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ù),變成了線性增長。

所以,我們可以發(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)重,因為大部分沒丟,只丟了一小部分,則 ssthresh 和 cwnd 變化如下:
-
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




