TCP 重傳郭宝、滑動窗口辞槐、流量控制、擁塞控制
相信大家都知道 TCP 是一個可靠傳輸?shù)膮f(xié)議粘室,那它是如何保證可靠的呢榄檬?
為了實現(xiàn)可靠性傳輸,需要考慮很多事情衔统,例如數(shù)據(jù)的破壞鹿榜、丟包、重復以及分片順序混亂等問題锦爵。如不能解決這些問題舱殿,也就無從談起可靠傳輸。
那么险掀,TCP 是通過序列號沪袭、確認應答、重發(fā)控制迷郑、連接管理以及窗口控制等機制實現(xiàn)可靠性傳輸?shù)摹?/p>
今天枝恋,將重點介紹 TCP 的重傳機制、滑動窗口嗡害、流量控制焚碌、擁塞控制。
重傳機制
TCP 實現(xiàn)可靠傳輸?shù)姆绞街话悦茫峭ㄟ^序列號與確認應答十电。
在 TCP 中,當發(fā)送端的數(shù)據(jù)到達接收主機時叹螟,接收端主機會返回一個確認應答消息鹃骂,表示已收到消息。
正常的數(shù)據(jù)傳輸
但在錯綜復雜的網絡罢绽,并不一定能如上圖那么順利能正常的數(shù)據(jù)傳輸牧挣,萬一數(shù)據(jù)在傳輸過程中丟失了呢轴总?
所以 TCP 針對數(shù)據(jù)包丟失的情況,會用重傳機制解決惫确。
接下來說說常見的重傳機制:
超時重傳
快速重傳
SACK
D-SACK
超時重傳
重傳機制的其中一個方式,就是在發(fā)送數(shù)據(jù)時,設定一個定時器,當超過指定的時間后,沒有收到對方的 ACK 確認應答報文市咽,就會重發(fā)該數(shù)據(jù),也就是我們常說的超時重傳抵蚊。
TCP 會在以下兩種情況發(fā)生超時重傳:
數(shù)據(jù)包丟失
確認應答丟失
超時時間應該設置為多少呢施绎?
我們先來了解一下什么是 RTT
(Round-Trip Time 往返時延),從下圖我們就可以知道:
RTT
就是數(shù)據(jù)從網絡一端傳送到另一端所需的時間贞绳,也就是包的往返時間谷醉。
超時重傳時間是以 RTO
(Retransmission Timeout 超時重傳時間)表示。
假設在重傳的情況下熔酷,超時時間 RTO
「較長或較短」時孤紧,會發(fā)生什么事情呢?
上圖中有兩種超時時間不同的情況:
當超時時間 RTO 較大時拒秘,重發(fā)就慢号显,丟了老半天才重發(fā),沒有效率躺酒,性能差押蚤;
當超時時間 RTO 較小時,會導致可能并沒有丟就重發(fā)羹应,于是重發(fā)的就快揽碘,會增加網絡擁塞,導致更多的超時园匹,更多的超時導致更多的重發(fā)雳刺。
精確的測量超時時間 RTO 的值是非常重要的,這可讓我們的重傳機制更高效裸违。
根據(jù)上述的兩種情況掖桦,我們可以得知,超時重傳時間 RTO 的值應該略大于報文往返 RTT 的值供汛。
至此枪汪,可能大家覺得超時重傳時間 RTO 的值計算,也不是很復雜嘛怔昨。
好像就是在發(fā)送端發(fā)包時記下 t0 雀久,然后接收端再把這個 ack 回來時再記一個 t1,于是 RTT = t1 – t0趁舀。沒那么簡單赖捌,這只是一個采樣,不能代表普遍情況矮烹。
實際上「報文往返 RTT 的值」是經常變化的越庇,因為我們的網絡也是時常變化的奋隶。也就因為「報文往返 RTT 的值」 是經常波動變化的,所以「超時重傳時間 RTO 的值」應該是一個動態(tài)變化的值悦荒。
我們來看看 Linux 是如何計算 RTO 的呢?
估計往返時間嘹吨,通常需要采樣以下兩個:
需要 TCP 通過采樣 RTT 的時間搬味,然后進行加權平均,算出一個平滑 RTT 的值蟀拷,而且這個值還是要不斷變化的碰纬,因為網絡狀況不斷地變化。
除了采樣 RTT问芬,還要采樣 RTT 的波動范圍悦析,這樣就避免如果 RTT 有一個大的波動的話,很難被發(fā)現(xiàn)的情況此衅。
RFC6289 建議使用以下的公式計算 RTO:
其中 SRTT
是計算平滑的RTT 强戴,DevRTR
是計算平滑的RTT 與 最新 RTT 的差距。
在 Linux 下挡鞍,α = 0.125骑歹,β = 0.25, μ = 1墨微,? = 4道媚。別問怎么來的,問就是大量實驗中調出來的翘县。
如果超時重發(fā)的數(shù)據(jù)最域,再次超時的時候,又需要重傳的時候锈麸,TCP 的策略是超時間隔加倍镀脂。
也就是每當遇到一次超時重傳的時候,都會將下一次超時時間間隔設為先前值的兩倍掐隐。兩次超時狗热,就說明網絡環(huán)境差,不宜頻繁反復發(fā)送虑省。
超時觸發(fā)重傳存在的問題是匿刮,超時周期可能相對較長。那是不是可以有更快的方式呢探颈?
于是就可以用「快速重傳」機制來解決超時重發(fā)的時間等待熟丸。
快速重傳
TCP 還有另外一種快速重傳(Fast Retransmit)機制,它不以時間為驅動伪节,而是以數(shù)據(jù)驅動重傳光羞。
快速重傳機制绩鸣,是如何工作的呢?其實很簡單纱兑,一圖勝千言呀闻。
在上圖,發(fā)送方發(fā)出了 1潜慎,2捡多,3,4铐炫,5 份數(shù)據(jù):
第一份 Seq1 先送到了垒手,于是就 Ack 回 2;
結果 Seq2 因為某些原因沒收到倒信,Seq3 到達了科贬,于是還是 Ack 回 2;
后面的 Seq4 和 Seq5 都到了鳖悠,但還是 Ack 回 2榜掌,因為 Seq2 還是沒有收到;
發(fā)送端收到了三個 Ack = 2 的確認乘综,知道了 Seq2 還沒有收到唐责,就會在定時器過期之前,重傳丟失的 Seq2瘾带。
最后鼠哥,接收到收到了 Seq2,此時因為 Seq3看政,Seq4朴恳,Seq5 都收到了,于是 Ack 回 6 允蚣。
所以于颖,快速重傳的工作方式是當收到三個相同的 ACK 報文時,會在定時器過期之前嚷兔,重傳丟失的報文段森渐。
快速重傳機制只解決了一個問題,就是超時時間的問題冒晰,但是它依然面臨著另外一個問題同衣。就是重傳的時候,是重傳之前的一個壶运,還是重傳所有的問題耐齐。
比如對于上面的例子,是重傳 Seq2 呢?還是重傳 Seq2埠况、Seq3耸携、Seq4、Seq5 呢辕翰?因為發(fā)送端并不清楚這連續(xù)的三個 Ack 2 是誰傳回來的夺衍。
根據(jù) TCP 不同的實現(xiàn),以上兩種情況都是有可能的喜命∷⒑螅可見,這是一把雙刃劍渊抄。
為了解決不知道該重傳哪些 TCP 報文,于是就有 SACK 方法丧裁。
SACK 方法
還有一種實現(xiàn)重傳機制的方式叫:SACK( Selective Acknowledgment 選擇性確認)护桦。
這種方式需要在 TCP 頭部「選項」字段里加一個 SACK 的東西,它可以將緩存的地圖發(fā)送給發(fā)送方煎娇,這樣發(fā)送方就可以知道哪些數(shù)據(jù)收到了二庵,哪些數(shù)據(jù)沒收到,知道了這些信息缓呛,就可以只重傳丟失的數(shù)據(jù)催享。
如下圖,發(fā)送方收到了三次同樣的 ACK 確認報文哟绊,于是就會觸發(fā)快速重發(fā)機制因妙,通過 SACK 信息發(fā)現(xiàn)只有 200~299 這段數(shù)據(jù)丟失,則重發(fā)時票髓,就只選擇了這個 TCP 段進行重復攀涵。
如果要支持
SACK
,必須雙方都要支持洽沟。在 Linux 下以故,可以通過 net.ipv4.tcp_sack
參數(shù)打開這個功能(Linux 2.4 后默認打開)。
Duplicate SACK
Duplicate SACK 又稱 D-SACK
裆操,其主要使用了 SACK 來告訴「發(fā)送方」有哪些數(shù)據(jù)被重復接收了怒详。
下面舉例兩個栗子,來說明 D-SACK
的作用踪区。
栗子一號:ACK 丟包
「接收方」發(fā)給「發(fā)送方」的兩個 ACK 確認應答都丟失了昆烁,所以發(fā)送方超時后,重傳第一個數(shù)據(jù)包(3000 ~ 3499)
于是「接收方」發(fā)現(xiàn)數(shù)據(jù)是重復收到的缎岗,于是回了一個 SACK = 3000~3500善玫,告訴「發(fā)送方」 3000~3500 的數(shù)據(jù)早已被接收了,因為 ACK 都到了 4000 了,已經意味著 4000 之前的所有數(shù)據(jù)都已收到茅郎,所以這個 SACK 就代表著
D-SACK
蜗元。這樣「發(fā)送方」就知道了,數(shù)據(jù)沒有丟系冗,是「接收方」的 ACK 確認報文丟了奕扣。
栗子二號:網絡延時
數(shù)據(jù)包(1000~1499) 被網絡延遲了,導致「發(fā)送方」沒有收到 Ack 1500 的確認報文掌敬。
而后面報文到達的三個相同的 ACK 確認報文惯豆,就觸發(fā)了快速重傳機制,但是在重傳后奔害,被延遲的數(shù)據(jù)包(1000~1499)又到了「接收方」楷兽;
所以「接收方」回了一個 SACK=1000~1500,因為 ACK 已經到了 3000华临,所以這個 SACK 是 D-SACK芯杀,表示收到了重復的包。
這樣發(fā)送方就知道快速重傳觸發(fā)的原因不是發(fā)出去的包丟了雅潭,也不是因為回應的 ACK 包丟了揭厚,而是因為網絡延遲了。
可見扶供,D-SACK
有這么幾個好處:
可以讓「發(fā)送方」知道筛圆,是發(fā)出去的包丟了,還是接收方回應的 ACK 包丟了;
可以知道是不是「發(fā)送方」的數(shù)據(jù)包被網絡延遲了;
可以知道網絡中是不是把「發(fā)送方」的數(shù)據(jù)包給復制了;
在 Linux 下可以通過 net.ipv4.tcp_dsack
參數(shù)開啟/關閉這個功能(Linux 2.4 后默認打開)椿浓。
滑動窗口
引入窗口概念的原因
我們都知道 TCP 是每發(fā)送一個數(shù)據(jù)太援,都要進行一次確認應答。當上一個數(shù)據(jù)包收到了應答了扳碍, 再發(fā)送下一個粉寞。
這個模式就有點像我和你面對面聊天,你一句我一句左腔。但這種方式的缺點是效率比較低的唧垦。
如果你說完一句話,我在處理其他事情液样,沒有及時回復你振亮,那你不是要干等著我做完其他事情后,我回復你鞭莽,你才能說下一句話坊秸,很顯然這不現(xiàn)實。
所以澎怒,這樣的傳輸方式有一個缺點:數(shù)據(jù)包的往返時間越長褒搔,通信的效率就越低。
為解決這個問題,TCP 引入了窗口這個概念星瘾。即使在往返時間較長的情況下走孽,它也不會降低網絡通信的效率。
那么有了窗口琳状,就可以指定窗口大小磕瓷,窗口大小就是指無需等待確認應答,而可以繼續(xù)發(fā)送數(shù)據(jù)的最大值念逞。
窗口的實現(xiàn)實際上是操作系統(tǒng)開辟的一個緩存空間困食,發(fā)送方主機在等到確認應答返回之前,必須在緩沖區(qū)中保留已發(fā)送的數(shù)據(jù)翎承。如果按期收到確認應答硕盹,此時數(shù)據(jù)就可以從緩存區(qū)清除。
假設窗口大小為 3 個 TCP 段叨咖,那么發(fā)送方就可以「連續(xù)發(fā)送」 3 個 TCP 段瘩例,并且中途若有 ACK 丟失,可以通過「下一個確認應答進行確認」芒澜。如下圖:
圖中的 ACK 600 確認應答報文丟失,也沒關系创淡,因為可以通話下一個確認應答進行確認痴晦,只要發(fā)送方收到了 ACK 700 確認應答,就意味著 700 之前的所有數(shù)據(jù)「接收方」都收到了琳彩。這個模式就叫累計確認或者累計應答誊酌。
窗口大小由哪一方決定?
TCP 頭里有一個字段叫 Window露乏,也就是窗口大小碧浊。
這個字段是接收端告訴發(fā)送端自己還有多少緩沖區(qū)可以接收數(shù)據(jù)。于是發(fā)送端就可以根據(jù)這個接收端的處理能力來發(fā)送數(shù)據(jù)瘟仿,而不會導致接收端處理不過來箱锐。
所以,通常窗口的大小是由接收方的決定的劳较。
發(fā)送方發(fā)送的數(shù)據(jù)大小不能超過接收方的窗口大小驹止,否則接收方就無法正常接收到數(shù)據(jù)。
我們先來看看發(fā)送方的窗口观蜗,下圖就是發(fā)送方緩存的數(shù)據(jù)臊恋,根據(jù)處理的情況分成四個部分,其中深藍色方框是發(fā)送窗口墓捻,紫色方框是可用窗口:
#1 是已發(fā)送并收到 ACK確認的數(shù)據(jù):1~31 字節(jié)
#2 是已發(fā)送但未收到 ACK確認的數(shù)據(jù):32~45 字節(jié)
#3 是未發(fā)送但總大小在接收方處理范圍內(接收方還有空間):46~51字節(jié)
#4 是未發(fā)送但總大小超過接收方處理范圍(接收方沒有空間):52字節(jié)以后
在下圖抖仅,當發(fā)送方把數(shù)據(jù)「全部」都一下發(fā)送出去后,可用窗口的大小就為 0 了,表明可用窗口耗盡撤卢,在沒收到 ACK 確認之前是無法繼續(xù)發(fā)送數(shù)據(jù)了环凿。
<figcaption style="margin: 10px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; line-height: inherit; text-align: center; color: rgb(153, 153, 153); font-size: 0.7em;">可用窗口耗盡</figcaption>
在下圖,當收到之前發(fā)送的數(shù)據(jù) 32~36
字節(jié)的 ACK 確認應答后凸丸,如果發(fā)送窗口的大小沒有變化拷邢,則滑動窗口往右邊移動 5 個字節(jié),因為有 5 個字節(jié)的數(shù)據(jù)被應答確認屎慢,接下來 52~56
字節(jié)又變成了可用窗口瞭稼,那么后續(xù)也就可以發(fā)送 52~56
這 5 個字節(jié)的數(shù)據(jù)了。
TCP 滑動窗口方案使用三個指針來跟蹤在四個傳輸類別中的每一個類別中的字節(jié)腻惠。其中兩個指針是絕對指針(指特定的序列號)环肘,一個是相對指針(需要做偏移)。
SND.WND
:表示發(fā)送窗口的大屑唷(大小是由接收方指定的)悔雹;SND.UNA
:是一個絕對指針,它指向的是已發(fā)送但未收到確認的第一個字節(jié)的序列號欣喧,也就是 #2 的第一個字節(jié)腌零。SND.NXT
:也是一個絕對指針,它指向未發(fā)送但可發(fā)送范圍的第一個字節(jié)的序列號唆阿,也就是 #3 的第一個字節(jié)益涧。指向 #4 的第一個字節(jié)是個相對指針,它需要
SND.UNA
指針加上SND.WND
大小的偏移量驯鳖,就可以指向 #4 的第一個字節(jié)了闲询。
那么可用窗口大小的計算就可以是:
可用窗口大 = SND.WND -(SND.NXT - SND.UNA)
接收方的滑動窗口
接下來我們看看接收方的窗口,接收窗口相對簡單一些浅辙,根據(jù)處理的情況劃分成三個部分:
* #1 + #2 是已成功接收并確認的數(shù)據(jù)(等待應用進程讀扰せ );
* #3 是未收到數(shù)據(jù)但可以接收的數(shù)據(jù)记舆;
* #4 未收到數(shù)據(jù)并不可以接收的數(shù)據(jù)鸽捻;
其中三個接收部分,使用兩個指針進行劃分:
RCV.WND
:表示接收窗口的大小泽腮,它會通告給發(fā)送方泊愧。RCV.NXT
:是一個指針,它指向期望從發(fā)送方發(fā)送來的下一個數(shù)據(jù)字節(jié)的序列號盛正,也就是 #3 的第一個字節(jié)删咱。指向 #4 的第一個字節(jié)是個相對指針,它需要
RCV.NXT
指針加上RCV.WND
大小的偏移量豪筝,就可以指向 #4 的第一個字節(jié)了痰滋。
接收窗口和發(fā)送窗口的大小是相等的嗎摘能?
并不是完全相等,接收窗口的大小是約等于發(fā)送窗口的大小的敲街。
因為滑動窗口并不是一成不變的团搞。比如,當接收方的應用進程讀取數(shù)據(jù)的速度非扯嗤В快的話逻恐,這樣的話接收窗口可以很快的就空缺出來。那么新的接收窗口大小峻黍,是通過 TCP 報文中的 Windows 字段來告訴發(fā)送方复隆。那么這個傳輸過程是存在時延的,所以接收窗口和發(fā)送窗口是約等于的關系姆涩。
流量控制
發(fā)送方不能無腦的發(fā)數(shù)據(jù)給接收方挽拂,要考慮接收方處理能力。
如果一直無腦的發(fā)數(shù)據(jù)給對方骨饿,但對方處理不過來亏栈,那么就會導致觸發(fā)重發(fā)機制,從而導致網絡流量的無端的浪費宏赘。
為了解決這種現(xiàn)象發(fā)生绒北,TCP 提供一種機制可以讓「發(fā)送方」根據(jù)「接收方」的實際接收能力控制發(fā)送的數(shù)據(jù)量,這就是所謂的流量控制察署。
下面舉個栗子闷游,為了簡單起見,假設以下場景:
客戶端是接收方箕母,服務端是發(fā)送方
假設接收窗口和發(fā)送窗口相同储藐,都為 200
假設兩個設備在整個傳輸過程中都保持相同的窗口大小俱济,不受外界影響
根據(jù)上圖的流量控制嘶是,說明下每個過程:
客戶端向服務端發(fā)送請求數(shù)據(jù)報文。這里要說明下蛛碌,本次例子是把服務端作為發(fā)送方聂喇,所以沒有畫出服務端的接收窗口。
服務端收到請求報文后蔚携,發(fā)送確認報文和 80 字節(jié)的數(shù)據(jù)希太,于是可用窗口 Usable 減少為 120 字節(jié),同時 SND.NXT 指針也向右偏移 80 字節(jié)后酝蜒,指向 321誊辉,這意味著下次發(fā)送數(shù)據(jù)的時候,序列號是 321亡脑。
客戶端收到 80 字節(jié)數(shù)據(jù)后堕澄,于是接收窗口往右移動 80 字節(jié)邀跃,RCV.NXT 也就指向 321,這意味著客戶端期望的下一個報文的序列號是 321蛙紫,接著發(fā)送確認報文給服務端拍屑。
服務端再次發(fā)送了 120 字節(jié)數(shù)據(jù),于是可用窗口耗盡為 0坑傅,服務端無法在繼續(xù)發(fā)送數(shù)據(jù)僵驰。
客戶端收到 120 字節(jié)的數(shù)據(jù)后,于是接收窗口往右移動 120 字節(jié)唁毒,RCV.NXT 也就指向 441蒜茴,接著發(fā)送確認報文給服務端。
服務端收到對 80 字節(jié)數(shù)據(jù)的確認報文后枉证,SND.UNA 指針往右偏移后指向 321矮男,于是可用窗口 Usable 增大到 80。
服務端收到對 120 字節(jié)數(shù)據(jù)的確認報文后室谚,SND.UNA 指針往右偏移后指向 441拔妥,于是可用窗口 Usable 增大到 200。
服務端可以繼續(xù)發(fā)送了蚓庭,于是發(fā)送了 160 字節(jié)的數(shù)據(jù)后痹愚,SND.NXT 指向 601,于是可用窗口 Usable 減少到 40入篮。
客戶端收到 160 字節(jié)后陈瘦,接收窗口往右移動了 160 字節(jié),RCV.NXT 也就是指向了 601潮售,接著發(fā)送確認報文給服務端痊项。
服務端收到對 160 字節(jié)數(shù)據(jù)的確認報文后,發(fā)送窗口往右移動了 160 字節(jié)酥诽,于是 SND.UNA 指針偏移了 160 后指向 601鞍泉,可用窗口 Usable 也就增大至了 200。
操作系統(tǒng)緩沖區(qū)與滑動窗口的關系
前面的流量控制例子肮帐,我們假定了發(fā)送窗口和接收窗口是不變的咖驮,但是實際上,發(fā)送窗口和接收窗口中所存放的字節(jié)數(shù)训枢,都是放在操作系統(tǒng)內存緩沖區(qū)中的托修,而操作系統(tǒng)的緩沖區(qū),會被操作系統(tǒng)調整恒界。
當應用進程沒辦法及時讀取緩沖區(qū)的內容時睦刃,也會對我們的緩沖區(qū)造成影響。
那操心系統(tǒng)的緩沖區(qū)十酣,是如何影響發(fā)送窗口和接收窗口的呢涩拙?
我們先來看看第一個例子枣宫。
當應用程序沒有及時讀取緩存時,發(fā)送窗口和接收窗口的變化吃环。
考慮以下場景:
客戶端作為發(fā)送方也颤,服務端作為接收方,發(fā)送窗口和接收窗口初始大小為
360
郁轻;服務端非常的繁忙翅娶,當收到客戶端的數(shù)據(jù)時,應用層不能及時讀取數(shù)據(jù)好唯。
根據(jù)上圖的流量控制竭沫,說明下每個過程:
客戶端發(fā)送 140 字節(jié)數(shù)據(jù)后,可用窗口變?yōu)?220 (360 - 140)骑篙。
服務端收到 140 字節(jié)數(shù)據(jù)蜕提,但是服務端非常繁忙,應用進程只讀取了 40 個字節(jié)靶端,還有 100 字節(jié)占用著緩沖區(qū)谎势,于是接收窗口收縮到了 260 (360 - 100),最后發(fā)送確認信息時杨名,將窗口大小通過給客戶端脏榆。
客戶端收到確認和窗口通告報文后,發(fā)送窗口減少為 260台谍。
客戶端發(fā)送 180 字節(jié)數(shù)據(jù)须喂,此時可用窗口減少到 80。
服務端收到 180 字節(jié)數(shù)據(jù)趁蕊,但是應用程序沒有讀取任何數(shù)據(jù)坞生,這 180 字節(jié)直接就留在了緩沖區(qū),于是接收窗口收縮到了 80 (260 - 180)掷伙,并在發(fā)送確認信息時是己,通過窗口大小給客戶端。
客戶端收到確認和窗口通告報文后炎咖,發(fā)送窗口減少為 80赃泡。
客戶端發(fā)送 80 字節(jié)數(shù)據(jù)后寒波,可用窗口耗盡乘盼。
服務端收到 80 字節(jié)數(shù)據(jù),但是應用程序依然沒有讀取任何數(shù)據(jù)俄烁,這 80 字節(jié)留在了緩沖區(qū)绸栅,于是接收窗口收縮到了 0,并在發(fā)送確認信息時页屠,通過窗口大小給客戶端粹胯。
客戶端收到確認和窗口通告報文后蓖柔,發(fā)送窗口減少為 0。
可見最后窗口都收縮為 0 了风纠,也就是發(fā)生了窗口關閉况鸣。當發(fā)送方可用窗口變?yōu)?0 時,發(fā)送方實際上會定時發(fā)送窗口探測報文竹观,以便知道接收方的窗口是否發(fā)生了改變镐捧,這個內容后面會說,這里先簡單提一下臭增。
我們先來看看第二個例子懂酱。
當服務端系統(tǒng)資源非常緊張的時候,操心系統(tǒng)可能會直接減少了接收緩沖區(qū)大小誊抛,這時應用程序又無法及時讀取緩存數(shù)據(jù)列牺,那么這時候就有嚴重的事情發(fā)生了,會出現(xiàn)數(shù)據(jù)包丟失的現(xiàn)象拗窃。
說明下每個過程:
客戶端發(fā)送 140 字節(jié)的數(shù)據(jù)瞎领,于是可用窗口減少到了 220。
服務端因為現(xiàn)在非常的繁忙随夸,操作系統(tǒng)于是就把接收緩存減少了 100 字節(jié)默刚,當收到 對 140 數(shù)據(jù)確認報文后,又因為應用程序沒有讀取任何數(shù)據(jù)逃魄,所以 140 字節(jié)留在了緩沖區(qū)中荤西,于是接收窗口大小從 360 收縮成了 100,最后發(fā)送確認信息時伍俘,通告窗口大小給對方邪锌。
此時客戶端因為還沒有收到服務端的通告窗口報文,所以不知道此時接收窗口收縮成了 100癌瘾,客戶端只會看自己的可用窗口還有 220觅丰,所以客戶端就發(fā)送了 180 字節(jié)數(shù)據(jù),于是可用窗口減少到 40妨退。
服務端收到了 180 字節(jié)數(shù)據(jù)時妇萄,發(fā)現(xiàn)數(shù)據(jù)大小超過了接收窗口的大小,于是就把數(shù)據(jù)包丟失了咬荷。
客戶端收到第 2 步時冠句,服務端發(fā)送的確認報文和通告窗口報文,嘗試減少發(fā)送窗口到 100幸乒,把窗口的右端向左收縮了 80懦底,此時可用窗口的大小就會出現(xiàn)詭異的負值。
所以罕扎,如果發(fā)生了先減少緩存聚唐,再收縮窗口丐重,就會出現(xiàn)丟包的現(xiàn)象。
為了防止這種情況發(fā)生杆查,TCP 規(guī)定是不允許同時減少緩存又收縮窗口的扮惦,而是采用先收縮窗口,過段時間在減少緩存亲桦,這樣就可以避免了丟包情況径缅。
窗口關閉
在前面我們都看到了,TCP 通過讓接收方指明希望從發(fā)送方接收的數(shù)據(jù)大欣臃巍(窗口大心芍怼)來進行流量控制。
如果窗口大小為 0 時桃笙,就會阻止發(fā)送方給接收方傳遞數(shù)據(jù)氏堤,直到窗口變?yōu)榉?0 為止,這就是窗口關閉搏明。
窗口關閉潛在的危險
接收方向發(fā)送方通告窗口大小時鼠锈,是通過 ACK
報文來通告的。
那么星著,當發(fā)生窗口關閉時购笆,接收方處理完數(shù)據(jù)后,會向發(fā)送方通告一個窗口非 0 的 ACK 報文虚循,如果這個通告窗口的 ACK 報文在網絡中丟失了同欠,那麻煩就大了。
這會導致發(fā)送方一直等待接收方的非 0 窗口通知横缔,接收方也一直等待發(fā)送方的數(shù)據(jù)铺遂,如不不采取措施,這種相互等待的過程茎刚,會造成了死鎖的現(xiàn)象襟锐。
TCP 是如何解決窗口關閉時,潛在的死鎖現(xiàn)象呢膛锭?
為了解決這個問題粮坞,TCP 為每個連接設有一個持續(xù)定時器,只要 TCP 連接一方收到對方的零窗口通知初狰,就啟動持續(xù)計時器莫杈。
如果持續(xù)計時器超時,就會發(fā)送窗口探測 ( Window probe ) 報文跷究,而對方在確認這個探測報文時姓迅,給出自己現(xiàn)在的接收窗口大小敲霍。
如果接收窗口仍然為 0俊马,那么收到這個報文的一方就會重新啟動持續(xù)計時器丁存;
如果接收窗口不是 0,那么死鎖的局面就可以被打破了柴我。
窗口探查探測的次數(shù)一般為 3 此次解寝,每次次大約 30-60 秒(不同的實現(xiàn)可能會不一樣)。如果 3 次過后接收窗口還是 0 的話艘儒,有的 TCP 實現(xiàn)就會發(fā) RST
報文來中斷連接聋伦。
糊涂窗口綜合癥
如果接收方太忙了,來不及取走接收窗口里的數(shù)據(jù)界睁,那么就會導致發(fā)送方的發(fā)送窗口越來越小觉增。
到最后,如果接收方騰出幾個字節(jié)并告訴發(fā)送方現(xiàn)在有幾個字節(jié)的窗口翻斟,而發(fā)送方會義無反顧地發(fā)送這幾個字節(jié)逾礁,這就是糊涂窗口綜合癥。
要知道访惜,我們的 TCP + IP
頭有 40
個字節(jié)嘹履,為了傳輸那幾個字節(jié)的數(shù)據(jù),要達上這么大的開銷债热,這太不經濟了砾嫉。
就好像一個可以承載 50 人的大巴車,每次來了一兩個人窒篱,就直接發(fā)車焕刮。除非家里有礦的大巴司機,才敢這樣玩墙杯,不然遲早破產济锄。要解決這個問題也不難,大巴司機等乘客數(shù)量超過了 25 個霍转,才認定可以發(fā)車荐绝。
現(xiàn)舉個糊涂窗口綜合癥的栗子,考慮以下場景:
接收方的窗口大小是 360 字節(jié)避消,但接收方由于某些原因陷入困境低滩,假設接收方的應用層讀取的能力如下:
接收方每接收 3 個字節(jié),應用程序就只能從緩沖區(qū)中讀取 1 個字節(jié)的數(shù)據(jù)岩喷;
在下一個發(fā)送方的 TCP 段到達之前恕沫,應用程序
還從緩沖區(qū)中讀取了 40 個額外的字節(jié);
<figcaption style="margin: 10px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; line-height: inherit; text-align: center; color: rgb(153, 153, 153); font-size: 0.7em;">糊涂窗口綜合癥</figcaption>
每個過程的窗口大小的變化纱意,在圖中都描述的很清楚了婶溯,可以發(fā)現(xiàn)窗口不斷減少了,并且發(fā)送的數(shù)據(jù)都是比較小的了。
所以迄委,糊涂窗口綜合癥的現(xiàn)象是可以發(fā)生在發(fā)送方和接收方:
接收方可以通告一個小的窗口
而發(fā)送方可以發(fā)送小數(shù)據(jù)
于是褐筛,要解決糊涂窗口綜合癥,就解決上面兩個問題就可以了
讓接收方不通告小窗口給發(fā)送方
讓發(fā)送方避免發(fā)送小數(shù)據(jù)
怎么讓接收方不通告小窗口呢叙身?
接收方通常的策略如下:
當「窗口大小」小于 min( MSS渔扎,緩存空間/2 ) ,也就是小于 MSS 與 1/2 緩存大小中的最小值時信轿,就會向發(fā)送方通告窗口為 0
晃痴,也就阻止了發(fā)送方再發(fā)數(shù)據(jù)過來。
等到接收方處理了一些數(shù)據(jù)后财忽,窗口大小 >= MSS倘核,或者接收方緩存空間有一半可以使用,就可以把窗口打開讓發(fā)送方發(fā)送數(shù)據(jù)過來即彪。
怎么讓發(fā)送方避免發(fā)送小數(shù)據(jù)呢笤虫?
發(fā)送方通常的策略:
使用 Nagle 算法,該算法的思路是延時處理祖凫,它滿足以下兩個條件中的一條才可以發(fā)送數(shù)據(jù):
要等到窗口大小 >=
MSS
或是 數(shù)據(jù)大小 >=MSS
收到之前發(fā)送數(shù)據(jù)的
ack
回包
只要沒滿足上面條件中的一條琼蚯,發(fā)送方一直在囤積數(shù)據(jù),直到滿足上面的發(fā)送條件惠况。
另外遭庶,Nagle 算法默認是打開的,如果對于一些需要小數(shù)據(jù)包交互的場景的程序稠屠,比如峦睡,telnet 或 ssh 這樣的交互性比較強的程序,則需要關閉 Nagle 算法权埠。
可以在 Socket 設置 TCP_NODELAY
選項來關閉這個算法(關閉 Nagle 算法沒有全局參數(shù)榨了,需要根據(jù)每個應用自己的特點來關閉)
setsockopt(sock_fd, IPPROTO_TCP, TCP_NODELAY, (char *)&value, sizeof(int));
擁塞控制
為什么要有擁塞控制呀,不是有流量控制了嗎攘蔽?
前面的流量控制是避免「發(fā)送方」的數(shù)據(jù)填滿「接收方」的緩存龙屉,但是并不知道網絡的中發(fā)生了什么。
一般來說满俗,計算機網絡都處在一個共享的環(huán)境转捕。因此也有可能會因為其他主機之間的通信使得網絡擁堵。
在網絡出現(xiàn)擁堵時唆垃,如果繼續(xù)發(fā)送大量數(shù)據(jù)包五芝,可能會導致數(shù)據(jù)包時延、丟失等辕万,這時 TCP 就會重傳數(shù)據(jù)枢步,但是一重傳就會導致網絡的負擔更重沉删,于是會導致更大的延遲以及更多的丟包,這個情況就會進入惡性循環(huán)被不斷地放大….
所以醉途,TCP 不能忽略網絡上發(fā)生的事矾瑰,它被設計成一個無私的協(xié)議,當網絡發(fā)送擁塞時结蟋,TCP 會自我犧牲脯倚,降低發(fā)送的數(shù)據(jù)量渔彰。
于是嵌屎,就有了擁塞控制,控制的目的就是避免「發(fā)送方」的數(shù)據(jù)填滿整個網絡恍涂。
為了在「發(fā)送方」調節(jié)所要發(fā)送數(shù)據(jù)的量宝惰,定義了一個叫做「擁塞窗口」的概念。
什么是擁塞窗口再沧?和發(fā)送窗口有什么關系呢尼夺?
擁塞窗口 cwnd是發(fā)送方維護的一個 的狀態(tài)變量,它會根據(jù)網絡的擁塞程度動態(tài)變化的炒瘸。
我們在前面提到過發(fā)送窗口 swnd 和接收窗口 rwnd 是約等于的關系淤堵,那么由于入了擁塞窗口的概念后,此時發(fā)送窗口的值是swnd = min(cwnd, rwnd)顷扩,也就是擁塞窗口和接收窗口中的最小值拐邪。
擁塞窗口 cwnd 變化的規(guī)則:
只要網絡中沒有出現(xiàn)擁塞,cwnd 就會增大隘截;
但網絡中出現(xiàn)了擁塞扎阶,cwnd 就減少;
那么怎么知道當前網絡是否出現(xiàn)了擁塞呢婶芭?
其實只要「發(fā)送方」沒有在規(guī)定時間內接收到 ACK 應答報文东臀,也就是發(fā)生了超時重傳,就會認為網絡出現(xiàn)了用擁塞犀农。
擁塞控制有哪些控制算法惰赋?
擁塞控制主要是四個算法:
慢啟動
擁塞避免
擁塞發(fā)生
快速恢復
慢啟動
TCP 在剛建立連接完成后,首先是有個慢啟動的過程呵哨,這個慢啟動的意思就是一點一點的提高發(fā)送數(shù)據(jù)包的數(shù)量谤逼,如果一上來就發(fā)大量的數(shù)據(jù),這不是給網絡添堵嗎仇穗?
慢啟動的算法記住一個規(guī)則就行:當發(fā)送方每收到一個 ACK流部,就擁塞窗口 cwnd 的大小就會加 1。
這里假定擁塞窗口 cwnd 和發(fā)送窗口 swnd 相等纹坐,下面舉個栗子:
連接建立完成后枝冀,一開始初始化
cwnd = 1
舞丛,表示可以傳一個MSS
大小的數(shù)據(jù)。當收到一個 ACK 確認應答后果漾,cwnd 增加 1球切,于是一次能夠發(fā)送 2 個
當收到 2 個的 ACK 確認應答后, cwnd 增加 2绒障,于是就可以比之前多發(fā)2 個吨凑,所以這一次能夠發(fā)送 4 個
當這 4 個的 ACK 確認到來的時候,每個確認 cwnd 增加 1户辱, 4 個確認 cwnd 增加 4鸵钝,于是就可以比之前多發(fā) 4 個,所以這一次能夠發(fā)送 8 個庐镐。
可以看出慢啟動算法恩商,發(fā)包的個數(shù)是指數(shù)性的增長。
那慢啟動漲到什么時候是個頭呢必逆?
有一個叫慢啟動門限 ssthresh
(slow start threshold)狀態(tài)變量怠堪。
當
cwnd < ssthresh
時,使用慢啟動算法名眉。當
cwnd >= ssthresh
時粟矿,就會使用「擁塞避免算法」。
擁塞避免算法
前面說道损拢,當擁塞窗口 cwnd
「超過」慢啟動門限 ssthresh
就會進入擁塞避免算法陌粹。
一般來說 ssthresh
的大小是 65535
字節(jié)。
那么進入擁塞避免算法后探橱,它的規(guī)則是:每當收到一個 ACK 時申屹,cwnd 增加 1/cwnd。
接上前面的慢啟動的栗子隧膏,現(xiàn)假定 ssthresh
為 8
:
- 當 8 個 ACK 應答確認到來時哗讥,每個確認增加 1/8,8 個 ACK 確認 cwnd 一共增加 1胞枕,于是這一次能夠發(fā)送 9 個
MSS
大小的數(shù)據(jù)杆煞,變成了線性增長。
所以腐泻,我們可以發(fā)現(xiàn)决乎,擁塞避免算法就是將原本慢啟動算法的指數(shù)增長變成了線性增長,還是增長階段派桩,但是增長速度緩慢了一些构诚。
就這么一直增長著后,網絡就會慢慢進入了擁塞的狀況了铆惑,于是就會出現(xiàn)丟包現(xiàn)象范嘱,這時就需要對丟失的數(shù)據(jù)包進行重傳送膳。
當觸發(fā)了重傳機制,也就進入了「擁塞發(fā)生算法」丑蛤。
擁塞發(fā)生
當網絡出現(xiàn)擁塞叠聋,也就是會發(fā)生數(shù)據(jù)包重傳,重傳機制主要有兩種:
超時重傳
快速重傳
這兩種使用的擁塞發(fā)送算法是不同的受裹,接下來分別來說說碌补。
發(fā)生超時重傳的擁塞發(fā)生算法
當發(fā)生了「超時重傳」,則就會使用擁塞發(fā)生算法棉饶。
這個時候厦章,sshresh 和 cwnd 的值會發(fā)生變化:
ssthresh
設為cwnd/2
,cwnd
重置為1
接著砰盐,就重新開始慢啟動闷袒,慢啟動是會突然減少數(shù)據(jù)流的坑律。這真是一旦「超時重傳」岩梳,馬上回到解放前。但是這種方式太激進了晃择,反應也很強烈冀值,會造成網絡卡頓。
就好像本來在秋名山高速漂移著宫屠,突然來個緊急剎車列疗,輪胎受得了嗎。浪蹂。抵栈。
發(fā)生快速重傳的擁塞發(fā)生算法
還有更好的方式,前面我們講過「快速重傳算法」坤次。當接收方發(fā)現(xiàn)丟了一個中間包的時候古劲,發(fā)送三次前一個包的 ACK,于是發(fā)送端就會快速地重傳缰猴,不必等待超時再重傳产艾。
TCP 認為這種情況不嚴重,因為大部分沒丟滑绒,只丟了一小部分闷堡,則 ssthresh
和 cwnd
變化如下:
cwnd = cwnd/2
,也就是設置為原來的一半;ssthresh = cwnd
;進入快速恢復算法
快速恢復
快速重傳和快速恢復算法一般同時使用疑故,快速恢復算法是認為杠览,你還能收到 3 個重復 ACK 說明網絡也不那么糟糕,所以沒有必要像 RTO
超時那么強烈纵势。
正如前面所說踱阿,進入快速恢復之前误续,cwnd
和 ssthresh
已被更新了:
cwnd = cwnd/2
,也就是設置為原來的一半;ssthresh = cwnd
;
然后扫茅,進入快速恢復算法如下:
擁塞窗口
cwnd = ssthresh + 3
( 3 的意思是確認有 3 個數(shù)據(jù)包被收到了)重傳丟失的數(shù)據(jù)包
如果再收到重復的 ACK蹋嵌,那么 cwnd 增加 1
如果收到新數(shù)據(jù)的 ACK 后,設置 cwnd 為 ssthresh葫隙,接著就進入了擁塞避免算法
也就是沒有像「超時重傳」一夜回到解放前栽烂,而是還在比較高的值,后續(xù)呈線性增長恋脚。