TCP要點有四,一曰有鏈接,二曰可靠傳輸,三曰數據按照到達,四曰端到端流量控制。注意,TCP被設計時只保證這四點,此時它雖然也有些問題,然而很簡單,然而更大的問題很快呈現出來,使之不得不考慮和IP網絡相關的東西,好比公平性,效率,所以增長了擁塞控制,這樣TCP就成了如今這個樣子。
要回答這個問題,首先必須知道何時TCP
會出現擁塞。TCP
做爲一個端到端的傳輸層協議,它並不關心鏈接雙方在物理鏈路上會通過多少路由器交換機以及報文傳輸的路徑和下一條,這是IP
層該考慮的事。然而,在現實網絡應用中,TCP
鏈接的兩端可能相隔千山萬水,報文也須要由多個路由器交換機進行轉發。交換設備的性能不是無限的!, 當多個入接口的報文都要從相同的出接口轉發時,若是出接口轉發速率達到極限,報文就會開始在交換設備的入接口緩存隊列堆積。但這個隊列長度也是有限的,當隊列塞滿後,後續輸入的報文就只能被丟棄掉了。對於TCP
的發送端來講,看到的就是發送超時丟包了。html
網絡資源是各個鏈接共享的,爲了你們都能完成數據傳輸。因此,TCP
須要當它感知到傳輸發生擁塞時,須要下降本身的發送速率,等待擁塞解除。算法
首先須要明確的是,TCP
是在發送端
進行擁塞控制的。TCP爲每條鏈接準備了一個記錄擁塞窗口大小的變量cwnd
1
,它限制了本端TCP
能夠發送到網絡中的最大報文數量2
。顯然,這個值越大,鏈接的吞吐量越高,但也更容易致使網絡擁塞。因此,TCP的擁塞控制本質上就是根據丟包狀況調整cwnd
,使得傳輸的吞吐率儘量地大!而不一樣的擁塞控制算法就是調整cwnd
的方式不一樣!shell
注1
: 本文中的cwnd
以發送端的最大報文段長度SMSS
爲單位的
注2
: 這個數量也受對端通告的窗口大小限制緩存
Linux 用戶可使用
ss --tcp --info 查看連接的
cwnd
值
TCP從誕生至今,已經有了多種的擁塞控制算法,直到如今還有新的在被提出!其中TCP Tahoe
(1988)和TCP Reno
(1990)是最初的兩個算法。雖然看上去年代很就遠了,但 Reno
算法直到如今還在普遍地使用。網絡
Tahoe
提出了1)慢啓動,2)擁塞避免,3)快速重傳Reno
在Tahoe
的基礎上增長了4)快速恢復Tahoe
算法的基本思想是ssh
當咱們在理解擁塞控制算法時,能夠假想發送端是一會兒將整個擁塞窗口大小的報文發送出去,而後等待迴應。
Tahoe
採用的是加性增乘性減(Additive Increase, Multiplicative Decrease, AIMD)方式來完成緩慢增長和快速減少擁塞窗口:tcp
發送端發送整窗
的數據:性能
cwnd = cwnd + 1
cwnd = cwnd / 2
爲何丟包後是除以2
呢, 這裏的2
其實是一個折中值!用下面的例子來解釋! spa
物理傳輸路徑都會有延時,這個延時也讓傳輸鏈路有了傳輸容量(transit capacity
)這樣一個概念,同時咱們把交換設備的隊列緩存稱爲隊列容量(queue capacity
).好比下面這樣一個鏈接,傳輸容量是M
,隊列容量是N
..net
當cwnd
小於M
時,不會使用R
的隊列,此時不會有擁塞發生;當cwnd
繼續增大時,開始使用R
的隊列,此時實際上已經有阻塞了!可是A
感知不到,由於沒有丟包! 當cwnd
繼續增大到M + N
時,若是再增大cwnd
,就會出現丟包。此時擁塞控制算法須要減少cwnd
,那麼減少到多少合適呢? 固然是減小到不使用R
的緩存,或者說使得cwnd = M
,這樣能夠快速解除阻塞。
這是理想的狀況! 實際狀況是A
並不知道M
和N
有多大(或者說有什麼關係),它只知道當cwnd
超過M + N
時會出丟包!因而咱們折中地假定M = N
,因此當cwnd = 2N
時是丟包的臨界點,爲了解除阻塞,讓cwnd = cwnd / 2 = N
就能夠解除阻塞3
!
因此,cwnd
的變化趨勢就像上面這樣,圖中上方的紅色曲線表示出現丟包。這樣的穩定狀態也稱爲擁塞避免階段
(congestion-avoidance phase
)
現實算法實現中,擁塞避免階段的cwnd
是在收到每一個ACK時更新的:cwnd += 1/cwnd
,若是認真算,會發現這比整窗更新cwnd += 1
要稍微少一點!
注3
:後面會提到,Tahoe
在此時會將cwnd
先設置爲1
,而後再迅速恢復到cwnd / 2
Tahoe
須要爲選定一個cwnd
初始值,可是發送端並不知道多大的cwnd
才合適。因此只能從1開始4
,若是這個時候就開始加性增,那就太慢了,好比假設一個鏈接會發送5050個MSS
大小的報文,按照加性增長,須要100個RTT
才能傳輸完成(1+2+3+...+100=5050)。所以,Tahoe
和Reno
使用一種稱爲慢啓動的算法迅速提升cwnd
。也就是隻要沒有丟包,每發送一個整窗的數據,cwnd = 2 X cwnd
。換句話說,在慢啓動階段
(slow-start phase
),當發送端每收到一個ACK
時,就讓cwnd = cwnd + 1
注4
RFC 2581 已經容許cwnd
的初始值最大爲2, RFC 3390 已經容許cwnd
的初始值最大爲4, RFC 6928已經容許cwnd
的初始值最大爲10
那麼,慢啓動階段什麼時候中止?或者說何時進入前面的擁塞避免階段 ? Tahoe
算法定義了一個慢啓動閾值
(slow-start threshold
)變量,在cwnd < ssthresh
時,TCP
處於慢啓動階段,在cwnd > ssthresh
後,TCP
處於擁塞避免階段。
ssthreshold
的初始值一個很是大的值。鏈接創建後cwnd
以指數增長,直到出現丟包後, 慢啓動閾值將被設置爲 cwnd / 2
。同時cwnd
被設置爲1
,從新開始慢啓動過程。這個過程以下圖所示, 能夠看到,慢啓動但是一點也不慢。
現實的網絡網絡環境拓撲可能十分複雜,即便是同一個TCP
鏈接的報文,也有可能因爲諸如等價路由等因素被路由器轉發到不一樣的路徑,因而,在接收端就可能出現報文的亂序到達,甚至丟包!舉個例子,發送端發送了數據DATA[1]、DATA[2]、……、DATA[8],但因爲某些因素,DATA[2]在傳輸過程當中被丟了,接收端只收到另外7個報文,它會連續回覆屢次 ACK[1](請求發送端發送DATA[2])。這個時候,發送端還須要等待DATA[2]的回覆超時(2個RTT)嗎?
快速重傳的策略是,不等了!擋發送端收到第3個重複的ACK[1]時(也就是第4個ACK[1]),它要立刻重傳DATA[2],而後進入慢啓動階段,設置ssthresh = cwnd / 2
, cwnd = 1
.
如上圖所示,其中cwnd
的初始值爲8,當發送端收到第3個重複的ACK[1]時,迅速進入慢啓動階段,以後當再收到ACK[1]時,因爲cwnd = 1
只有1,所以並不會發送新的報文
在快速重傳中,當出現報文亂序丟包後,擁塞窗口cwnd
變爲1,因爲該限制,在丟失的數據包被應答以前,沒有辦法發送新的數據包。這樣大大下降了網絡的吞吐量。針對這個問題,TCP Reno
在TCP Tahoe
的基礎上增長了快速恢復(Fast Recovery
)。
快速恢復的策略是當收到第3個重複的ACK後,快速重傳丟失的包,而後
sshthresh = cwnd / 2
cwnd = cwnd /2
仍是以上面的例子爲例
與快速重傳中不一樣的是,發送端在收到第3個重複的ACK後,cwnd
變爲5,EFS
設置爲7
這裏EFS
表示發送端認爲的正在向對端發送的包(Estimated FlightSize),或者說正在鏈路上(in flight)的包。通常狀況下,EFS
是與cwnd
相等的。但在快速恢復的時候,就不一樣了。假設擁塞避免階段時cwnd = EFS = N
,在啓動快速恢復時,收到了3個重複的ACK,注意,這3個ACK是不會佔用網絡資源的(由於它們已經被對端收到了),因此EFS = N - 3
,而既然是出發了快速恢復,那麼必定是有一個包沒有到達,因此EFS = N - 4
,而後,本端會快速重傳一個報文,EFS = N - 3
,這就是上面EFS
設置爲7的來源。
其餘部分沒什麼好說的,發送端會在EFS < cwnd
時發送信的數據,而同時,這又會使得EFS = cwnd
根據Reno
的描述,TCP
發送端會在收到3個重複的ACK時進行快速重傳和快速恢復,但還有有一個問題,重複的ACK背後可能不只僅是一個包丟了!若是是多個包丟了,即便發送端快速重傳了丟失的第一個包,進入快速恢復,那麼後面也會收到接收端發出的多個請求其餘丟失的包的重複ACK!這個時候?發送端須要再累計到3個重複的ACK才能重傳!
問題出在哪裏?發送端不能重收到的重複ACK中得到更多的丟包信息!它只知道第一個被丟棄的報文,後面還有多少被丟棄了?徹底不知道!也許使用SACK
(參考RFC6675)這就不是問題,但這須要兩端都支持SACK
。對於不支持SACK
的場景,TCP
須要更靈活!
RFC6582中描述的New Reno
算法,在Reno
中的基礎上,引入了一個新的變量recover
,當進入快速恢復狀態時(收到3個重複的ACK[a]),將recover
設置爲已經發送的最後的報文的序號。若是以後收到的新的ACK[b]序號b不超過recover
,就說明這仍是一個丟包引發的ACK !這種ACK在標準中也稱之爲部分應答
(partial acknowledgments
), 這時發送端就不等了,當即重傳丟失的報文。
還有後文!