TCP為什么可靠?
目錄
前言
實(shí)現(xiàn)一個(gè)可靠的協(xié)議?
滑動窗口
順序、丟包問題
確認(rèn)和重發(fā)機(jī)制
流量控制
擁塞控制
前言
上一篇記錄了TCP三次握手四次揮手的細(xì)節(jié)以及為什么會在TIME_WAIT狀態(tài)停留時(shí)間為2MSL。
一直說TCP是可靠的協(xié)議,那么它靠什么成為一個(gè)可靠的傳輸協(xié)議?
實(shí)現(xiàn)一個(gè)可靠的協(xié)議?
順序性: 每個(gè)包都有一個(gè)序號,在建立連接時(shí),雙方確定起始序號,然后按需發(fā)送
不丟包: 對于發(fā)送的包都要進(jìn)行應(yīng)答,但不是發(fā)一個(gè)應(yīng)答一個(gè),而是應(yīng)答某一個(gè),而這個(gè)序號之前的包,表示都收到了,這種模式成為累計(jì)確認(rèn)或者累計(jì)應(yīng)答
滑動窗口
TCP使用的被稱為滑動窗口協(xié)議的另一種形式的流量控制方法,該協(xié)議允許發(fā)送方在停止并等待確認(rèn)前可以連續(xù)發(fā)送多個(gè)分組。
由于發(fā)送方不必每發(fā)一個(gè)分組就停下來等待確認(rèn),因此該協(xié)議可以加速數(shù)據(jù)的傳輸。
為記錄所有發(fā)送和接收的包,TCP需要發(fā)送端和接收端分別都有緩存來保存這些記錄
發(fā)送端的緩存里時(shí)按照包的序號一個(gè)個(gè)排列,按照處理情況分為四個(gè)部分:
(1)發(fā)送并已確認(rèn) (2)發(fā)送但未確認(rèn) (3)沒發(fā)送但等發(fā)送 (4)沒發(fā)送但暫不發(fā)送
滑動窗口的大小是由接收端提供的,也即是上述 【 第二點(diǎn) + 第三點(diǎn) 】,超過這個(gè)大小,接收端處理不過來

對接收端來說,緩存里記錄的內(nèi)容如下:
(1)接收并已確認(rèn) (2)已接收但未確認(rèn) (3)等待接收的 (4)不能接收的,超過窗口大小

書中例子:
數(shù)據(jù)傳輸過程中滑動窗口的變化


順序、丟包問題
發(fā)送端:
接收端:
分析如下:
(1)包1、2、3雙方都達(dá)成了一致
(2)包4、5接收方確認(rèn)了,但是發(fā)送方還沒收到接收方的確認(rèn),有可能丟了或者在路上
(3)包6、7、8、9已經(jīng)發(fā)送,但是包8、9已經(jīng)到接收端,包6、7沒到,此時(shí)會出現(xiàn)亂序,所以包8、9只能放在緩存中,不能進(jìn)行ACK。
確認(rèn)和重發(fā)機(jī)制
超時(shí)重傳
對每一個(gè)發(fā)送了但是沒有確認(rèn)的包,都設(shè)定一個(gè)定時(shí)器,超過定時(shí),就重新嘗試。
超時(shí)時(shí)間不宜過短,必須大于往返時(shí)間RTT,否則會有不必要的重傳
估計(jì)往返時(shí)間,需要TCP通過采樣RTT的時(shí)間,然后進(jìn)行加權(quán)平均,算出一個(gè)值,而且這個(gè)值是不斷變化的,因?yàn)榫W(wǎng)絡(luò)狀況在不斷的變化,除了采用RTT,還要采用RTT的波動范圍,計(jì)算出一個(gè)估計(jì)的超時(shí)時(shí)間。由于重傳時(shí)間是不斷變化的,稱為自適應(yīng)重傳算法。
TCP的策略是超時(shí)間隔加倍,每當(dāng)遇到依次超時(shí)重傳的時(shí)候,都會將下一次超時(shí)時(shí)間間隔設(shè)為先前值的兩倍,兩次超時(shí),說明網(wǎng)絡(luò)環(huán)境差,不宜頻繁反復(fù)發(fā)送。
快速重傳:
當(dāng)接收方收到一個(gè)序號大于下一個(gè)所期望的報(bào)文段時(shí),就會檢測到數(shù)據(jù)流中的一個(gè)間隔,于是它就會發(fā)送冗余的 ACK,冗余 ACK 的是期望接收的報(bào)文段。而當(dāng)客戶端收到三個(gè)冗余的 ACK 后,就會在定時(shí)器過期之前,重傳丟失的報(bào)文段。
例如:接收方發(fā)現(xiàn) 6 收到了,8 也收到了,但是 7 還沒來,那肯定是丟了,于是發(fā)送 6 的 ACK,要求下一個(gè)是 7。接下來,收到后續(xù)的包,仍然發(fā)送 6 的 ACK,要求下一個(gè)是 7。當(dāng)發(fā)送端收到 3 個(gè)重復(fù) ACK,就會發(fā)現(xiàn) 7 的確丟了,不等超時(shí),馬上重發(fā)。
流量控制
在對包的確認(rèn)中,同時(shí)會攜帶一個(gè)窗口的大小
發(fā)送端發(fā)送的每一個(gè)數(shù)據(jù)包,服務(wù)端都要給一個(gè)確認(rèn)包(ACK),確認(rèn)它收到了。
服務(wù)端給發(fā)送端發(fā)送的確認(rèn)包(ACK包)中,同時(shí)會攜帶一個(gè)窗口的大小,
此窗口大小就代表目前服務(wù)器端的處理能力(接收端最大緩存量 - 接收端已確認(rèn)但未被應(yīng)用層讀取的部分)
此窗口大小也是時(shí)刻變化的,可能接收方在發(fā)送數(shù)據(jù)包4的ACK時(shí)候,窗口大小為9,此時(shí)應(yīng)用層程序已經(jīng)讀取確認(rèn)了緩存4個(gè)數(shù)據(jù)的,接收方再發(fā)送數(shù)據(jù)包5的ACK的時(shí)候,窗口的大小就變?yōu)?4
例如:窗口不變,始終為9
初始情況:
注意:點(diǎn)點(diǎn)框起來的是窗口
發(fā)送端:

當(dāng)收到ACK4時(shí),窗口右移,此時(shí)第13個(gè)包也可以發(fā)送

此時(shí)發(fā)送端發(fā)送較快,將包10、11、12、13 全部發(fā)送,此時(shí)未發(fā)送可發(fā)送部分為0

當(dāng)收到ACK5時(shí),窗口向右滑動一格,此時(shí)14可發(fā)送

如果接收方處理速度比不上發(fā)送方發(fā)送速度,導(dǎo)致緩存中沒有空間,可通過確認(rèn)信息修改窗口大小,甚至可設(shè)置為0,則發(fā)送方將暫時(shí)停止發(fā)送
假設(shè)接收方應(yīng)用一直不讀取緩存中數(shù)據(jù),當(dāng)ACK6到達(dá)發(fā)送方后,此時(shí)攜帶的窗口大小變成了8

此時(shí)發(fā)送方僅僅是將左邊進(jìn)行右移,窗口大小從9改成了8

接收端應(yīng)用一直不讀取數(shù)據(jù),那么就會一直發(fā)送ACK給發(fā)送端,直到窗口大小為0

當(dāng)ACK14到達(dá)時(shí),窗口為0,停止發(fā)送

此時(shí)發(fā)送方會定時(shí)發(fā)送窗口探測數(shù)據(jù)包,看是否有機(jī)會調(diào)整窗口大小。
擁塞控制
擁塞控制也是通過窗口的大小控制的,流量控制的滑動窗口rwnd是怕發(fā)送方把接收方緩存塞滿,而擁塞窗口cwnd,是怕把網(wǎng)絡(luò)塞滿
rwnd:receiver window,接收方滑動窗口,用于防止接收方緩存占滿 cwnd:congestion window,擁塞窗口,用于控制將帶寬占滿
發(fā)送方怎么判斷網(wǎng)絡(luò)是不是慢?
對于TCP協(xié)議來說,他不知道整個(gè)網(wǎng)絡(luò)路徑都會經(jīng)歷什么,TCP發(fā)送包常被比喻為往一個(gè)水管里面灌水,而TCP的擁塞控制就是在不堵塞、不丟包的情況下,盡量發(fā)揮帶寬。
網(wǎng)絡(luò)有帶寬,端到端有時(shí)延
通道的容量= 帶寬 * 往返延遲
如果設(shè)置發(fā)送窗口,使得發(fā)送但未確認(rèn)的包為通道的容量,就能撐滿整個(gè)管道。

假設(shè)往返時(shí)間為8s,去4s,回4s,每秒發(fā)送一個(gè)包,每個(gè)包1024byte。
8s后8個(gè)包全發(fā)出去了,其中前4個(gè)包已經(jīng)到達(dá)接收端,但是ACK還沒返回,不算發(fā)送成功,5-8個(gè)包還在路上沒被接收,此時(shí)整個(gè)管道正好撐滿。
在發(fā)送端,已發(fā)送未確認(rèn)的有8個(gè)包,正好等于帶寬,也即是每秒發(fā)送一個(gè)包,乘于往返時(shí)間8s。
如果在此基礎(chǔ)上調(diào)大窗口,單位時(shí)間可發(fā)送更多的包,那么單位時(shí)間內(nèi),會有更多的包到達(dá)中間設(shè)備,但設(shè)備在單位時(shí)間的處理速度跟不上,多出來的包就會被丟棄。
此時(shí),可以在中間設(shè)備上添加緩存,此時(shí)包不會丟失,但是會增加時(shí)延,如果時(shí)延到達(dá)一定成都,會進(jìn)行超時(shí)重傳。
所以TCP的擁塞控制主要避免兩種現(xiàn)象:包丟失和超時(shí)重傳。
出現(xiàn)這兩個(gè)現(xiàn)象就說明發(fā)送速度過快,需要慢一點(diǎn)。
如果發(fā)送速度從慢開始,發(fā)現(xiàn)可以加快速度的發(fā),這叫慢啟動。類似于車的加速。
一條TCP連接開始,cwnd設(shè)置為1個(gè)報(bào)文段,一次只能發(fā)1個(gè),當(dāng)收到這一個(gè)確認(rèn)時(shí)候,cwnd加1,此時(shí)可以一次發(fā)2個(gè),當(dāng)這兩個(gè)確認(rèn)到來時(shí),每確認(rèn)一個(gè),cwnd加1,此時(shí)cwnd為4,可以發(fā)4個(gè)。這是指數(shù)型增長。
直到cwnd達(dá)到ssthresh=65535個(gè)字節(jié)的時(shí)候,就需要慢下來。
此時(shí)每收到一個(gè)確認(rèn)后,cwnd增加1/cwnd,一次發(fā)送8個(gè),8個(gè)確認(rèn)一共是8*(1/8)=1,于是此時(shí)可以發(fā)送9個(gè),變成了線性增長。
擁塞的一種表現(xiàn)形式是丟包,需要超時(shí)重傳,此時(shí),將sshresh設(shè)為cwnd/2,將cwnd設(shè)為1,重新開始慢啟動,此時(shí)傳輸速度就會立馬慢下來,造成網(wǎng)絡(luò)卡頓。
快速重傳算法。當(dāng)接收端發(fā)現(xiàn)丟了一個(gè)中間包的時(shí)候,發(fā)送三次前一個(gè)包的 ACK,于是發(fā)送端就會快速地重傳,不必等待超時(shí)再重傳。TCP 認(rèn)為這種情況不嚴(yán)重,因?yàn)榇蟛糠譀]丟,只丟了一小部分,cwnd 減半為 cwnd/2,然后 sshthresh = cwnd,當(dāng)三個(gè)包返回的時(shí)候,cwnd = sshthresh + 3,也就是沒有一夜回到解放前,而是還在比較高的值,呈線性增長。
例如,此時(shí)cwnd為6,有一個(gè)包遲遲沒返回ACK,此時(shí)cwnd變成3,ssthresh=cwnd,當(dāng)收到三個(gè)請求發(fā)送這個(gè)包的ACK時(shí),cwnd = ssthresh + 3 = 6
這樣就不會一下導(dǎo)致網(wǎng)絡(luò)卡頓。
TCP 的擁塞控制主要來避免的兩個(gè)現(xiàn)象都是有問題的
第一個(gè)問題是丟包并不代表著通道滿了,也可能是管子本來就漏水。例如公網(wǎng)上帶寬不滿也會丟包,這個(gè)時(shí)候就認(rèn)為擁塞了,退縮了,其實(shí)是不對的。 第二個(gè)問題是 TCP 的擁塞控制要等到將中間設(shè)備都填充滿了,才發(fā)生丟包,從而降低速度,這時(shí)候已經(jīng)晚了。其實(shí) TCP 只要填滿管道就可以了,不應(yīng)該接著填,直到連緩存也填滿。
為了優(yōu)化這兩個(gè)問題,后來有了 TCP BBR 擁塞算法。它企圖找到一個(gè)平衡點(diǎn),就是通過不斷地加快發(fā)送速度,將管道填滿,但是不要填滿中間設(shè)備的緩存,因?yàn)檫@樣時(shí)延會增加,在這個(gè)平衡點(diǎn)可以很好的達(dá)到高帶寬和低時(shí)延的平衡。


往期推薦:
DNS域名解析系統(tǒng)
推薦一個(gè)生產(chǎn)環(huán)境問題排查利器
