TCP 是面向鏈接的,UDP 是面向無鏈接的。所謂的創建鏈接,是爲了在客戶端和服務端維護鏈接,而創建必定的數據結構來維護雙方交互的狀態,用這樣的數據結構來保證所謂的面向鏈接的特性。算法
TCP 提供可靠交付。 經過 TCP 鏈接傳輸的數據,無差錯、不丟失、不重複、而且按序到達。咱們都知道 IP 包是沒有任何可靠性保證的, 一旦發出去只能聽天由命,而UDP 繼承了 IP 包的特性,不保證不丟失,不保證按順序到達。緩存
TCP 是面向字節流的。發送的時候發的是一個流,沒頭沒尾。 IP 包可不是一個流,而是一個個的 IP 包。之因此變成了流,這也是 TCP 本身的狀態維護作的事情。而UDP 繼承了 IP 的特性,基於數據報的,一個一個地發,一個一個地收。服務器
TCP 是能夠有擁塞控制的。 它意識到包丟棄了或者網絡的環境很差了,就會根據狀況調整本身的行爲,看看是否是發快了,要不要發慢點。UDP 就不會,應用讓我發,我就發,管它洪水滔天。網絡
於是TCP 實際上是一個有狀態服務, 通俗地講就是有腦子的,裏面精確地記着發送了沒有,接收到沒有,發送到哪一個了,應該接收哪一個了,錯一點兒都不行。而 UDP 則是無狀態服務。 通俗地說是沒腦子的,天真無邪的,發出去就發出去了。數據結構
咱們能夠這樣比喻,若是 MAC 層定義了本地局域網的傳輸行爲,IP 層定義了整個網絡端到端的傳輸行爲, 這兩層基本定義了這樣的基因:網絡傳輸是以包爲單位的,二層叫幀,網絡層叫包,傳輸層叫段。 咱們籠統地稱爲包。包單獨傳輸,自行選路,在不一樣的設備封裝解封裝,不保證到達。 基於這個基因,生下來的孩子 UDP 徹底繼承了這些特性,幾乎沒有本身的思想。ssh
發送的時候,我知道我發的是一個 UDP 的包,收到的那臺機器咋知道的呢?因此在 IP 頭裏面有個 8 位協議,這裏會存放,數據裏面究竟是 TCP 仍是 UDP,固然這裏是 UDP。因而,若是咱們知道 UDP 頭的格式,就能從數據裏面,將它解析出來。異步
當我發送的 UDP 包到達目標機器後,發現 MAC 地址匹配,因而就取下來,將剩下的包傳給處理 IP 層的代碼。把 IP 頭取下來,發現目標 IP 匹配,接下來而後就給傳輸層處理,處理完後,內核的事情基本就幹完了,裏面的數據應該交給應用程序本身去處理。
tcp
QUIC(全稱Quick UDP Internet Connections,快速 UDP 互聯網鏈接)是 Google提出的一種基於 UDP 改進的通訊協議,其目的是下降網絡通訊的延遲,提供更好的用戶互動體驗。優化
QUIC 在應用層上,會本身實現快速鏈接創建、減小重傳時延,自適應擁塞控制,ui
直播協議多使用 RTMP。而這個RTMP 協議也是基於 TCP 的。TCP 的嚴格順序傳輸要保證前一個收到了,下一個才能確認,若是前一個收不到,下一個就算包已經收到了,在緩存裏面,也須要等着。對於直播來說,這顯然是不合適的,由於老的視頻幀丟了其實也就丟了,就算再傳過來用戶也不在乎了,他們要看新的了,若是總是沒來就等着,卡頓了,新的也看不了,那就會丟失客戶,因此直播,實時性比較比較重要,寧肯丟包,也不要卡頓的。
實時遊戲中客戶端和服務端要創建長鏈接,來保證明時傳輸。可是遊戲玩家不少,服務器卻很少。因爲維護 TCP 鏈接須要在內核維護一些數據結構,於是一臺機器可以支撐的TCP 鏈接數目是有限的,而後 UDP 因爲是沒有鏈接的,在異步 IO 機制引入以前,經常是應對海量客戶端鏈接的策略。
另外仍是 TCP 的強順序問題,對戰的遊戲,對網絡的要求很簡單,玩家經過客戶端發送給服務器鼠標和鍵盤行走的位置,服務器會處理每一個用戶發送過來的全部場景,處理完再返回給客戶端,客戶端解析響應,渲染最新的場景展現給玩家。
若是出現一個數據包丟失,全部事情都須要停下來等待這個數據包重發。客戶端會出現等待接收數據,然而玩家並不關心過時的數據,激戰中卡 1 秒,等能動了都已經死了。
遊戲對實時要求較爲嚴格的狀況下,採用自定義的可靠 UDP 協議,自定義重傳策略,可以把丟包產生的延遲降到最低,儘可能減小網絡問題對遊戲性形成的影響。
一方面,物聯網領域終端資源少, 極可能只是個內存很是小的嵌入式系統,而維護 TCP 協議代價太大;另外一方面,物聯網對實時性要求也很高, 而 TCP 仍是由於上面的那些緣由致使時延大。Google 旗下的 Nest 創建 Thread Group,推出了物聯網通訊協議 Thread,就是基於 UDP 協議的。
在 4G 網絡裏,移動流量上網的數據面對的協議 GTP-U 是基於 UDP 的。由於移動網絡協議比較複雜,而 GTP 協議自己就包含複雜的手機上線下線的通訊協議。若是基於 TCP,TCP 的機制就顯得很是多餘。
TCP 協議。它之因此這麼複雜,那是由於它秉承的是「性惡論」。它自然認爲網絡環境是惡劣的,丟包、亂序、重傳,擁塞都是常有的事情,一言不合就可能送達不了,於是要從算法層面來保證可靠性。
經過對 TCP 頭的解析,咱們知道要掌握 TCP 協議,重點應該關注如下幾個問題:
順序問題;丟包問題;鏈接維護;流量控制;擁塞控制
TCP 的鏈接創建,咱們經常稱爲三次握手。
A:您好,我是 A。 B:您好 A,我是 B。 A:您好 B。
咱們也常稱爲「請求 -> 應答 -> 應答之應答」的三個回合。
三次握手除了雙方創建鏈接外,主要仍是爲了溝通一件事情,就是 TCP 包的序號的問題。
A 要告訴 B,我這面發起的包的序號起始是從哪一個號開始的,B 一樣也要告訴 A,B 發起的包的序號起始是從哪一個號開始的。爲何序號不能都從 1 開始呢?由於這樣每每會出現衝突。
咱們仍是假設這個通路是很是不可靠的,A 要發起一個鏈接,當發了第一個請求杳無音信的時候,會有不少的可能性,好比第一個請求包丟了,再如沒有丟,可是繞了彎路,超時了,還有 B 沒有響應,不想和我鏈接。A 不能確認結果,因而再發,再發。終於,有一個請求包到了 B,可是請求包到了 B 的這個事情,目前 A 仍是不知道的,A 還有可能再發。B 收到了請求包,就知道了 A 的存在,而且知道 A 要和它創建鏈接。若是 B 不樂意創建鏈接,則 A 會重試一陣後放棄,鏈接創建失敗,沒有問題;若是 B 是樂意創建鏈接的,則會發送應答包給 A。
固然對於 B 來講,這個應答包也是一入網絡深似海,不知道能不能到達 A。這個時候 B 天然不能認爲鏈接是創建好了,由於應答包仍然會丟,會繞彎路,或者 A 已經掛了都有可能。並且這個時候 B 還能碰到一個詭異的現象就是,A 和 B 原來創建了鏈接,作了簡單通訊後,結束了鏈接。還記得嗎?A 創建鏈接的時候,請求包重複發了幾回,有的請求包繞了一大圈又回來了,B 會認爲這也是一個正常的的請求的話,所以創建了鏈接,能夠想象,這個鏈接不會進行下去,也沒有個終結的時候,純屬單相思了。於是兩次握手確定不行。B 發送的應答可能會發送屢次,可是隻要一次到達 A,A 就認爲鏈接已經創建了,由於對於A 來說,他的消息有去有回。A 會給 B 發送應答之應答,而 B 也在等這個消息,才能確認鏈接的創建,只有等到了這個消息,對於 B 來說,纔算它的消息有去有回。固然 A 發給 B 的應答之應答也會丟,也會繞路,甚至 B 掛了。按理來講,還應該有個應答之應答之應答,這樣下去就沒底了。因此四次握手是能夠的,四十次均可以,關鍵四百次也不能保證就真的可靠了。只要雙方的消息都有去有回,就基本能夠了。
好在大部分狀況下,A 和 B 創建了鏈接以後,A 會立刻發送數據的,一旦 A 發送數據,則不少問題都獲得瞭解決。例如 A 發給 B 的應答丟了,當 A 後續發送的數據到達的時候,B能夠認爲這個鏈接已經創建,或者 B 壓根就掛了,A 發送的數據,會報錯,說 B 不可達,A 就知道 B 出事情了。固然你能夠說 A 比較壞,就是不發數據,創建鏈接後空着。咱們在程序設計的時候,能夠要求開啓 keepalive 機制,即便沒有真實的數據包,也有探活包。另外,你做爲服務端 B 的程序設計者,對於 A 這種長時間不發包的客戶端,能夠主動關閉,從而空出資源來給其餘客戶端使用。
三次握手除了雙方創建鏈接外,主要仍是爲了溝通一件事情,就是TCP 包的序號的問題。A 要告訴 B,我這面發起的包的序號起始是從哪一個號開始的,B 一樣也要告訴 A,B 發起的包的序號起始是從哪一個號開始的。 爲何序號不能都從 1 開始呢?由於這樣每每會出現衝突。例如,A 連上 B 以後,發送了 一、二、3 三個包,可是發送 3 的時候,中間丟了,或者繞路了,因而從新發送,後來 A 掉線了,從新連上 B 後,序號又從 1 開始,而後發送 2,可是壓根沒想發送 3,可是上次繞路的那個 3 又回來了,發給了 B,B 天然認爲,這就是下一個包,因而發生了錯誤。於是,每一個鏈接都要有不一樣的序號。這個序號的起始序號是隨着時間變化的,能夠當作一個32 位的計數器,每 4ms 加一,若是計算一下,若是到重複,須要 4 個多小時,那個繞路的包早就死翹翹了,由於咱們都知道 IP 包頭裏面有個 TTL,也即生存時間。好了,雙方終於創建了信任,創建了鏈接。前面也說過,爲了維護這個鏈接,雙方都要維護一個狀態機,在鏈接創建的過程當中,雙方的狀態變化時序圖就像這樣。
一開始,客戶端和服務端都處於 CLOSED 狀態。先是服務端主動監聽某個端口,處於LISTEN 狀態。而後客戶端主動發起鏈接 SYN,以後處於 SYN-SENT 狀態。服務端收到發起的鏈接,返回 SYN,而且 ACK 客戶端的 SYN,以後處於 SYN-RCVD 狀態。客戶端收到服務端發送的 SYN 和 ACK 以後,發送 ACK 的 ACK,以後處於 ESTABLISHED 狀態,由於它一發一收成功了。服務端收到 ACK 的 ACK 以後,處於 ESTABLISHED 狀態,由於它也一發一收了。
A:B 啊,我不想玩了。
B:哦,你不想玩了啊,我知道了。
這個時候,還只是 A 不想玩了,也即 A 不會再發送數據,可是 B 能不能在 ACK 的時候,直接關閉呢?固然不能夠了,頗有可能 A 是發完了最後的數據就準備不玩了,可是 B 還沒作完本身的事情,仍是能夠發送數據的,因此稱爲半關閉的狀態。
這個時候 A 能夠選擇再也不接收數據了,也能夠選擇最後再接收一段數據,等待 B 也主動關閉。
B:A 啊,好吧,我也不玩了,拜拜。
A:好的,拜拜。
這樣整個鏈接就關閉了。可是這個過程有沒有異常狀況呢?固然有,上面是和平分手的場面。
A 開始說「不玩了」,B 說「知道了」,這個回合,是沒什麼問題的,由於在此以前,雙方還處於合做的狀態,若是 A 說「不玩了」,沒有收到回覆,則 A 會從新發送「不玩了」。可是這個回合結束以後,就有可能出現異常狀況了,由於已經有一方率先撕破臉。
一種狀況是,A 說完「不玩了」以後,直接跑路,是會有問題的,由於 B 尚未發起結束,而若是 A 跑路,B 就算髮起結束,也得不到回答,B 就不知道該怎麼辦了。另外一種狀況是,A 說完「不玩了」,B 直接跑路,也是有問題的,由於 A 不知道 B 是還有事情要處理,仍是過一下子會發送結束。
斷開的時候,咱們能夠看到,當 A 說「不玩了」,就進入 FIN_WAIT_1 的狀態,B 收到「A 不玩」的消息後,發送知道了,就進入 CLOSE_WAIT 的狀態。A 收到「B 說知道了」,就進入 FIN_WAIT_2 的狀態,若是這個時候 B 直接跑路,則 A 將永遠在這個狀態。TCP 協議裏面並無對這個狀態的處理,可是 Linux 有,能夠調整tcp_fin_timeout 這個參數,設置一個超時時間。
若是 B 沒有跑路,發送了「B 也不玩了」的請求到達 A 時,A 發送「知道 B 也不玩了」的ACK 後,從 FIN_WAIT_2 狀態結束,按說 A 能夠跑路了,可是最後的這個 ACK 萬一 B 收不到呢?則 B 會從新發一個「B 不玩了」,這個時候 A 已經跑路了的話,B 就再也收不到ACK 了,於是 TCP 協議要求 A 最後等待一段時間 TIME_WAIT,這個時間要足夠長,長到若是 B 沒收到 ACK 的話,「B 說不玩了」會重發的,A 會從新發一個 ACK 而且足夠時間到達 B。A 直接跑路還有一個問題是,A 的端口就直接空出來了,可是 B 不知道,B 原來發過的不少包極可能還在路上,若是 A 的端口被一個新的應用佔用了,這個新的應用會收到上個鏈接中 B 發過來的包,雖然序列號是從新生成的,可是這裏要上一個雙保險,防止產生混亂,於是也須要等足夠長的時間,等到原來 B 發送的全部的包都死翹翹,再空出端口來。等待的時間設爲 2MSL,MSL是Maximum Segment Lifetime,報文最大生存時間, 它是任何報文在網絡上存在的最長時間,超過這個時間報文將被丟棄。由於 TCP 報文基因而IP 協議的,而 IP 頭中有一個 TTL 域,是 IP 數據報能夠通過的最大路由數,每通過一個處理他的路由器此值就減 1,當此值爲 0 則數據報將被丟棄,同時發送 ICMP 報文通知源主機。協議規定 MSL 爲 2 分鐘,實際應用中經常使用的是 30 秒,1 分鐘和 2 分鐘等。還有一個異常狀況就是,B 超過了 2MSL 的時間,依然沒有收到它發的 FIN 的 ACK,怎麼辦呢?按照 TCP 的原理,B 固然還會重發 FIN,這個時候 A 再收到這個包以後,A 就表示,我已經在這裏等了這麼長時間了,已經仁至義盡了,以後的我就都不認了,因而就直接發送 RST,B 就知道 A 早就跑了。
將鏈接創建和鏈接斷開的兩個時序狀態圖綜合起來,就是這個著名的 TCP 的狀態機。
爲了保證順序性,每個包都有一個 ID。在創建鏈接的時候,會商定起始的 ID 是什麼,而後按照 ID 一個個發送。爲了保證不丟包,對於發送的包都要進行應答,可是這個應答也不是一個一個來的,而是會應答某個以前的 ID,表示都收到了,這種模式稱爲累計確認或者累計應答(cumulative acknowledgment)。
爲了記錄全部發送的包和接收的包,TCP 也須要發送端和接收端分別都有緩存來保存這些記錄。發送端的緩存裏是按照包的 ID 一個個排列,根據處理的狀況分紅四個部分。
這裏面爲何要區分第三部分和第四部分呢?沒交代的,一會兒全交代了不就完了嗎?這就是咱們上一節提到的「流量控制,把握分寸」。
在 TCP 裏,接收端會給發送端報一個窗口的大小,叫Advertised window。這個窗口的大小應該等於上面的第二部分加上第三部分,就是已經交代了沒作完的加上立刻要交代的。超過這個窗口的,接收端作不過來,就不能發送了。因而,發送端須要保持下面的數據結構。
對於接收端來說,它的緩存裏記錄的內容要簡單一些。
仍是剛纔的圖,在發送端來看,一、二、3 已經發送並確認;四、五、六、七、八、9 都是發送了還沒確認;十、十一、12 是還沒發出的;1三、1四、15 是接收方沒有空間,不許備發的。在接收端來看,一、二、三、四、5 是已經完成 ACK,可是沒讀取的;六、7 是等待接收的;八、9 是已經接收,可是沒有 ACK 的。
發送端和接收端當前的狀態以下:
根據這個例子,咱們能夠知道,順序問題和丟包問題都有可能發生,因此咱們先來看確認與重發的機制。
假設 4 的確認到了,不幸的是,5 的 ACK 丟了,六、7 的數據包丟了,這該怎麼辦呢?
一種方法就是超時重試, 也即對每個發送了,可是沒有 ACK 的包,都有設一個定時器,超過了必定的時間,就從新嘗試。可是這個超時的時間如何評估呢?這個時間不宜太短,時間必須大於往返時間 RTT, 不然會引發沒必要要的重傳。也不宜過長,這樣超時時間變長,訪問就變慢了。
估計往返時間,須要 TCP 經過採樣 RTT 的時間,而後進行加權平均, 算出一個值,並且這個值仍是要不斷變化的,由於網絡情況不斷的變化。除了採樣 RTT,還要採樣 RTT 的波動範圍,計算出一個估計的超時時間。因爲重傳時間是不斷變化的,咱們稱爲自適應重傳算法(Adaptive Retransmission Algorithm)。
若是過一段時間,五、六、7 都超時了,就會從新發送。接收方發現 5 原來接收過,因而丟棄 5;6 收到了,發送 ACK,要求下一個是 7,7 不幸又丟了。當 7 再次超時的時候,有須要重傳的時候,TCP 的策略是超時間隔加倍。 每當遇到一次超時重傳的時候,都會將下一次超時時間間隔設爲先前值的兩倍。兩次超時,就說明網絡環境差,不宜頻繁反覆發送。
超時觸發重傳存在的問題是,超時週期可能相對較長。那是否是能夠有更快的方式呢?
有一個能夠快速重傳的機制, 當接收方收到一個序號大於下一個所指望的報文段時,就檢測到了數據流中的一個間格,因而發送三個冗餘的 ACK,客戶端收到後,就在定時器過時以前,重傳丟失的報文段。例如,接收方發現 六、八、9 都已經接收了,就是 7 沒來,那確定是丟了,因而發送三個 6的 ACK,要求下一個是 7。客戶端收到 3 個,就會發現 7 的確又丟了,不等超時,立刻重發。
還有一種方式稱爲Selective Acknowledgment(SACK)。 這種方式須要在 TCP 頭裏加一個 SACK 的東西,能夠將緩存的地圖發送給發送方。例如能夠發送 ACK六、SACK八、SACK9,有了地圖,發送方一會兒就能看出來是 7 丟了。
在對於包的確認中,同時會攜帶一個窗口的大小。
咱們先假設窗口不變的狀況,窗口始終爲 9。4 的確認來的時候,會右移一個,這個時候第13 個包也能夠發送了。
這個時候,假設發送端發送過猛,會將第三部分的 十、十一、十二、13 所有發送完畢,以後就中止發送了,未發送可發送部分爲 0。
當對於包 5 的確認到達的時候,在客戶端至關於窗口再滑動了一格,這個時候,才能夠有更多的包能夠發送了,例如第 14 個包才能夠發送
若是接收方實在處理的太慢,致使緩存中沒有空間了,能夠經過確認信息修改窗口的大小,甚至能夠設置爲 0,則發送方將暫時中止發送。
咱們假設一個極端狀況,接收端的應用一直不讀取緩存中的數據,當數據包 6 確認後,窗
口大小就不能再是 9 了,就要縮小一個變爲 8。
這個新的窗口 8 經過 6 的確認消息到達發送端的時候,你會發現窗口沒有平行右移,而是僅僅左面的邊右移了,窗口的大小從 9 改爲了 8。
若是接收端仍是一直不處理數據,則隨着確認的包愈來愈多,窗口愈來愈小,直到爲 0。
當這個窗口經過包 14 的確認到達發送端的時候,發送端的窗口也調整爲 0,中止發送。
若是這樣的話,發送方會定時發送窗口探測數據包,看是否有機會調整窗口的大小。當接收方比較慢的時候,要防止低能窗口綜合徵,別空出一個字節來就趕快告訴發送方,而後立刻又填滿了,能夠當窗口過小的時候,不更新窗口,直到達到必定大小,或者緩衝區一半爲空,才更新窗口。這就是咱們常說的流量控制。
也是經過窗口的大小來控制的,前面的滑動窗口 rwnd是怕發送方把接收方緩存塞滿,而擁塞窗口 cwnd,是怕把網絡塞滿。
這裏有一個公式 LastByteSent - LastByteAcked <= min {cwnd, rwnd} ,是擁塞窗口和滑動窗口共同控制發送的速度。
那發送方怎麼判斷網絡是否是滿呢?這實際上是個挺難的事情,由於對於 TCP 協議來說,他壓根不知道整個網絡路徑都會經歷什麼,對他來說就是一個黑盒。TCP 發送包常被比喻爲往一個水管裏面灌水,而 TCP 的擁塞控制就是在不堵塞,不丟包的狀況下,儘可能發揮帶寬。水管有粗細,網絡有帶寬,也即每秒鐘可以發送多少數據;水管有長度,端到端有時延。在理想狀態下,水管裏面水的量 = 水管粗細 x 水管長度。對於到網絡上,通道的容量 = 帶寬× 往返延遲。
若是咱們設置發送窗口,使得發送但未確認的包爲爲通道的容量,就可以撐滿整個管道。
如圖所示,假設往返時間爲 8s,去 4s,回 4s,每秒發送一個包,每一個包 1024byte。已通過去了 8s,則 8 個包都發出去了,其中前 4 個包已經到達接收端,可是 ACK 尚未返回,不能算髮送成功。5-8 後四個包還在路上,還沒被接收。這個時候,整個管道正好撐滿,在發送端,已發送未確認的爲 8 個包,正好等於帶寬,也即每秒發送 1 個包,乘以來回時間 8s。
若是咱們在這個基礎上再調大窗口,使得單位時間內更多的包能夠發送,會出現什麼現象呢?咱們來想,原來發送一個包,從一端到達另外一端,假設一共通過四個設備,每一個設備處理一個包時間耗費 1s,因此到達另外一端須要耗費 4s,若是發送的更加快速,則單位時間內,會有更多的包到達這些中間設備,這些設備仍是隻能每秒處理一個包的話,多出來的包就會被丟棄,這是咱們不想看到的。這個時候,咱們能夠想其餘的辦法,例如這個四個設備原本每秒處理一個包,可是咱們在這些設備上加緩存,處理不過來的在隊列裏面排着,這樣包就不會丟失,可是缺點是會增長時延,這個緩存的包,4s 確定到達不了接收端了,若是時延達到必定程度,就會超時重傳,也是咱們不想看到的。
因而 TCP 的擁塞控制主要來避免兩種現象,包丟失和超時重傳。 一旦出現了這些現象就說明,發送速度太快了,要慢一點。可是一開始我怎麼知道速度多快呢,我怎麼知道應該把窗口調整到多大呢?
若是咱們經過漏斗往瓶子裏灌水,咱們就知道,不能一桶水一會兒倒進去,確定會濺出來,要一開始慢慢的倒,而後發現總可以倒進去,就能夠越倒越快。這叫做慢啓動。 一條 TCP 鏈接開始,cwnd 設置爲一個報文段,一次只能發送一個;當收到這一個確認的時候,cwnd 加一,因而一次可以發送兩個;當這兩個的確認到來的時候,每一個確認 cwnd加一,兩個確認 cwnd 加二,因而一次可以發送四個;當這四個的確認到來的時候,每一個確認 cwnd 加一,四個確認 cwnd 加四,因而一次可以發送八個。能夠看出這是指數性的增加。
漲到何時是個頭呢?有一個值 ssthresh 爲 65535 個字節,當超過這個值的時候,就要當心一點了,不能倒這麼快了,可能快滿了,再慢下來。每收到一個確認後,cwnd 增長 1/cwnd,咱們接着上面的過程來,一次發送八個,當八個確認到來的時候,每一個確認增長 1/8,八個確認一共 cwnd 增長 1,因而一次可以發送九個,變成了線性增加。可是線性增加仍是增加,仍是愈來愈多,直到有一天,水滿則溢,出現了擁塞,這時候通常就會一會兒下降倒水的速度,等待溢出的水慢慢滲下去。
擁塞的一種表現形式是丟包,須要超時重傳,這個時候,將 sshresh 設爲 cwnd/2,將cwnd 設爲 1,從新開始慢啓動。這真是一旦超時重傳,立刻回到解放前。可是這種方式太激進了,將一個高速的傳輸速度一會兒停了下來,會形成網絡卡頓。前面咱們講過快速重傳算法。當接收端發現丟了一箇中間包的時候,發送三次前一個包的ACK,因而發送端就會快速的重傳,沒必要等待超時再重傳。TCP 認爲這種狀況不嚴重,由於大部分沒丟,只丟了一小部分,cwnd 減半爲 cwnd/2,而後 sshthresh = cwnd,當三個包返回的時候,cwnd = sshthresh + 3,也就是沒有一晚上回到解放前,而是還在比較高的值,呈線性增加。
就像前面說的同樣,正是這種知進退,使得時延很重要的狀況下,反而下降了速度。可是若是你仔細想一下,TCP 的擁塞控制主要來避免的兩個現象都是有問題的。
爲了優化這兩個問題,後來有了TCP BBR 擁塞算法。 它企圖找到一個平衡點,就是經過不斷的加快發送速度,將管道填滿,可是不要填滿中間設備的緩存,由於這樣時延會增長,在這個平衡點能夠很好的達到高帶寬和低時延的平衡。