TCP要點有四,一曰有連接,二曰可靠傳輸,三曰數據按照到達,四曰端到端流量控制。注意,TCP被設計時只保證這四點,此時它雖然也有些問題,然而很簡單,然而更大的問題很快呈現出來,使之不得不考慮和IP網絡相關的東西,比如公平性,效率,因此增加了擁塞控制,這樣TCP就成了現在這個樣子。
為什么要進行擁塞控制
要回答這個問題,首先必須知道什么時候TCP會出現擁塞。TCP作為一個端到端的傳輸層協議,它并不關心連接雙方在物理鏈路上會經過多少路由器交換機以及報文傳輸的路徑和下一條,這是IP層該考慮的事。然而,在現實網絡應用中,TCP連接的兩端可能相隔千山萬水,報文也需要由多個路由器交換機進行轉發。交換設備的性能不是無限的!, 當多個入接口的報文都要從相同的出接口轉發時,如果出接口轉發速率達到極限,報文就會開始在交換設備的入接口緩存隊列堆積。但這個隊列長度也是有限的,當隊列塞滿后,后續輸入的報文就只能被丟棄掉了。對于TCP的發送端來說,看到的就是發送超時丟包了。
如何進行擁塞控制
擁塞窗口 cwnd
首先需要明確的是,TCP是在發送端進行擁塞控制的。TCP為每條連接準備了一個記錄擁塞窗口大小的變量cwnd1,它限制了本端TCP可以發送到網絡中的最大報文數量2。顯然,這個值越大,連接的吞吐量越高,但也更容易導致網絡擁塞。所以,TCP的擁塞控制本質上就是根據丟包情況調整cwnd,使得傳輸的吞吐率盡可能地大!而不同的擁塞控制算法就是調整cwnd的方式不同!
注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算法的基本思想是
- 首選設置一個符合情理的初始窗口值
- 當沒有出現丟包時,慢慢地增加窗口大小,逐漸逼近吞吐量的上界
- 當出現丟包時,快速地減小窗口大小,等待阻塞消除
相關視頻推薦
LinuxC++零拷貝的實現 用戶態協議棧 ntytcp
90分鐘搞定tcp/ip協議棧
LinuxC++后臺服務器開發架構師免費學習地址:C/C++Linux服務器開發/后臺架構師【零聲教育】-學習視頻教程-騰訊課堂
【文章福利】:小編整理了一些個人覺得比較好的學習書籍、視頻資料共享在qun文件里面,有需要的可以自行添加哦!(需要視頻資料后臺私信“1”自取)
Tahoe 擁塞避免 (congestion-avoidance)
當我們在理解擁塞控制算法時,可以假想發送端是一下子將整個擁塞窗口大小的報文發送出去,然后等待回應。
Tahoe采用的是加性增乘性減(Additive Increase, Multiplicative Decrease, AIMD)方式來完成緩慢增加和快速減小擁塞窗口:
發送端發送整窗的數據:
- 如果沒有丟包,則 cwnd = cwnd + 1
- 如果出現丟包了,則 cwnd = cwnd / 2
為什么丟包后是除以2呢, 這里的2實際上是一個折中值!用下面的例子來解釋!
?
物理傳輸路徑都會有延時,這個延時也讓傳輸鏈路有了傳輸容量(transit capacity)這樣一個概念,同時我們把交換設備的隊列緩存稱為隊列容量(queue capacity).比如下面這樣一個連接,傳輸容量是M,隊列容量是N.
當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 慢啟動 (Slow Start)
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
?
注4RFC 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,重新開始慢啟動過程。這個過程如下圖所示, 可以看到,慢啟動可是一點也不慢。
?
Tahoe 快速重傳 (Fast Retransmit)
現實的網絡網絡環境拓撲可能十分復雜,即使是同一個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,因此并不會發送新的報文
Reno 快速恢復(Fast Recovery)
在快速重傳中,當出現報文亂序丟包后,擁塞窗口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
TCP New Reno
根據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), 這時發送端就不等了,立即重傳丟失的報文。
?
編輯
TCP在發送報文后,如果沒有收到對端應答,那么在重傳定時器超時后會觸發重傳,超時時間遵循二進制退避原則,也就是{1,2,4,8,16}這樣成倍地擴大超時時間。退避是因為TCP認為丟包意味著網絡有擁塞,為了不加重網絡的擁塞,TCP選擇等待更長的時間再進行重傳。這和CSMA/CD中的二進制退避算法如出一轍。
網絡中的網絡設備(路由器、交換機)在收到了超過隊列限制的報文后,后續的報文會被丟棄。從TCP采用的二進制退避算來看,TCP絕對算得上是網絡里的謙謙君子了,它信守的規則是:既然已經堵了,我就等一會兒再發,如果還堵,我就再多等翻倍的時間!
對整個網絡來說,這的確是減輕負擔的好辦法。要知道,在發送窗口已滿的情況下,指數退避一次,意味著單位時間內發送的報文變成了原來的1/2,再退避一次,就只有原來的1/4!就像是汽車限號出行,單雙號限行不好用,我就規定一輛車只能在4天中開1天...
But, TCP真的需要如此克制自己嗎?,換個說法,為什么TCP一定要x2退避?難道重傳定時器超時時間不能線性增大(每次增加X秒),或者乘以一個更小的系數(比如x1.5) ? 我們可以從CSMA/CD找到靈感。
CSMA/CD使用x2的原因很好理解,共享介質中的每個節點并不知道其他還有多少節點,使用x2退避就是利用二分法快速找到讓整個系統穩定運行的時隙分配方案!
舉個例子,假設系統中有4個節點發包速率相同,那么最終的穩定分配方案自然就是將一段時間分為4份,每個節點占用1個時隙。如果此時又加入4個相同的節點。那么顯然,所有的8個節點都會發包沖突。如何才能不沖突呢?當然是分配給每個節點的時間再減小一半!當然這里舉的例子節點都是2的整數冪。如果不是呢?此時2進制退避依然能很快地達到穩定,也許這個時候的時隙分配方案不是最合理的,但是正如前面說的,每個節點并不知道其他還有多少節點,對單個節點來說,二進制退避是最快速找到讓每個節點都正常工作的方案!
?
編輯
二進制退避方案中隱含了對公平性的考慮,它是站在整個網絡的角度,而不是其中某一臺主機!
但對一臺特定的主機,不遵守這個退避規則顯然好處更多...比如使用固定的重傳定時器時間。在這種網絡中,沒有擁塞時大家相安無事,一旦出現了擁塞,那么不退避的主機理論上就能發出更多的報文!
?
編輯
當然,這似乎犧牲了其他遵守規則主機的利益,它們的重傳次數會增加。那么,如果大家都不遵守呢?結果就是大家的重傳次數都增加了,擁塞甚至比大家都遵守還要糟糕,因為網絡上的設備丟的包更多了!
這就有點囚徒困境的味道了,如果別人退避你不退避,你能獲利,如果別人比退避而你退避,那么你就吃虧了,如果大家都不遵守,那么比都遵守還要糟糕......
當然,現實中的網絡并不同于CSMA/CD中的共享介質,一個簡單的區別就是,在CSMA/CD中,如果一個節點監聽到信道繁忙,它是不是發送數據的,它需要等待;而在網絡中,并不存在這樣的共享介質,并且路由器是又緩沖隊列的,因此兩個主機還是可以都發送報文。
話雖如此,很多游戲為了保證實時性不會選擇TCP !畢竟,TCP太無私了,它是為了整個網絡考慮的。而實時游戲需要的是什么!快速!那么如果它們有需要可靠傳輸怎么辦? 很簡單,使用UDP,然后在用戶態自己做ARQ就好了,想怎么折騰怎么折騰,比如KCP就是這么個東西。






