網絡協議筆記(四)傳輸層(1)

9、UDP 協議

TCP 和 UDP 有哪些區別?

TCP 是面向鏈接的,UDP 是面向無鏈接的。所謂的創建鏈接,是爲了在客戶端和服務端維護鏈接,而創建必定的數據結構來維護雙方交互的狀態,用這樣的數據結構來保證所謂的面向鏈接的特性。算法

TCP 提供可靠交付。 經過 TCP 鏈接傳輸的數據,無差錯、不丟失、不重複、而且按序到達。咱們都知道 IP 包是沒有任何可靠性保證的, 一旦發出去只能聽天由命,而UDP 繼承了 IP 包的特性,不保證不丟失,不保證按順序到達。緩存

TCP 是面向字節流的。發送的時候發的是一個流,沒頭沒尾。 IP 包可不是一個流,而是一個個的 IP 包。之因此變成了流,這也是 TCP 本身的狀態維護作的事情。而UDP 繼承了 IP 的特性,基於數據報的,一個一個地發,一個一個地收。服務器

TCP 是能夠有擁塞控制的。 它意識到包丟棄了或者網絡的環境很差了,就會根據狀況調整本身的行爲,看看是否是發快了,要不要發慢點。UDP 就不會,應用讓我發,我就發,管它洪水滔天。網絡

於是TCP 實際上是一個有狀態服務, 通俗地講就是有腦子的,裏面精確地記着發送了沒有,接收到沒有,發送到哪一個了,應該接收哪一個了,錯一點兒都不行。而 UDP 則是無狀態服務。 通俗地說是沒腦子的,天真無邪的,發出去就發出去了。數據結構

咱們能夠這樣比喻,若是 MAC 層定義了本地局域網的傳輸行爲,IP 層定義了整個網絡端到端的傳輸行爲, 這兩層基本定義了這樣的基因:網絡傳輸是以包爲單位的,二層叫幀,網絡層叫包,傳輸層叫段。 咱們籠統地稱爲包。包單獨傳輸,自行選路,在不一樣的設備封裝解封裝,不保證到達。 基於這個基因,生下來的孩子 UDP 徹底繼承了這些特性,幾乎沒有本身的思想。ssh

UDP 包頭

發送的時候,我知道我發的是一個 UDP 的包,收到的那臺機器咋知道的呢?因此在 IP 頭裏面有個 8 位協議,這裏會存放,數據裏面究竟是 TCP 仍是 UDP,固然這裏是 UDP。因而,若是咱們知道 UDP 頭的格式,就能從數據裏面,將它解析出來。異步

當我發送的 UDP 包到達目標機器後,發現 MAC 地址匹配,因而就取下來,將剩下的包傳給處理 IP 層的代碼。把 IP 頭取下來,發現目標 IP 匹配,接下來而後就給傳輸層處理,處理完後,內核的事情基本就幹完了,裏面的數據應該交給應用程序本身去處理。
image.pngtcp

UDP 的三大特色

  1. 溝通簡單,不須要一肚子花花腸子(大量的數據結構、處理邏輯、包頭字段)。
  2. 輕信他人。它不會創建鏈接,雖然有端口號,可是監聽在這個地方,誰均可以傳給他數據,他也能夠傳給任何人數據,甚至能夠同時傳給多我的數據。
  3. 愣頭青,作事不懂權變。它不會根據網絡的狀況進行發包的擁塞控制,不管網絡丟包丟成啥樣了,它該怎麼發還怎麼發。

UDP 的三大使用場景

  1. 須要資源少,在網絡狀況比較好的內網,或者對於丟包不敏感的應用。DHCP 就是基於 UDP 協議的。通常的獲取 IP 地址都是內網請求,並且一次獲取不到 IP 又沒事,過一下子還有機會。
  2. 不須要一對一溝通,創建鏈接,而是能夠廣播的應用。UDP 的不面向鏈接的功能,可使得能夠承載廣播或者多播的協議。DHCP 就是一種廣播的形式, 就是基於 UDP 協議的,而廣播包的格式前面說過了。
  3. 須要處理速度快,時延低,能夠容忍少數丟包,可是要求即使網絡擁塞,也絕不退縮,勇往直前的時候。

基於 UDP 的五個例子

網頁或者 APP 的訪問

QUIC(全稱Quick UDP Internet Connections,快速 UDP 互聯網鏈接)是 Google提出的一種基於 UDP 改進的通訊協議,其目的是下降網絡通訊的延遲,提供更好的用戶互動體驗。優化

QUIC 在應用層上,會本身實現快速鏈接創建、減小重傳時延,自適應擁塞控制,ui

流媒體的協議

直播協議多使用 RTMP。而這個RTMP 協議也是基於 TCP 的。TCP 的嚴格順序傳輸要保證前一個收到了,下一個才能確認,若是前一個收不到,下一個就算包已經收到了,在緩存裏面,也須要等着。對於直播來說,這顯然是不合適的,由於老的視頻幀丟了其實也就丟了,就算再傳過來用戶也不在乎了,他們要看新的了,若是總是沒來就等着,卡頓了,新的也看不了,那就會丟失客戶,因此直播,實時性比較比較重要,寧肯丟包,也不要卡頓的。

實時遊戲

實時遊戲中客戶端和服務端要創建長鏈接,來保證明時傳輸。可是遊戲玩家不少,服務器卻很少。因爲維護 TCP 鏈接須要在內核維護一些數據結構,於是一臺機器可以支撐的TCP 鏈接數目是有限的,而後 UDP 因爲是沒有鏈接的,在異步 IO 機制引入以前,經常是應對海量客戶端鏈接的策略。

另外仍是 TCP 的強順序問題,對戰的遊戲,對網絡的要求很簡單,玩家經過客戶端發送給服務器鼠標和鍵盤行走的位置,服務器會處理每一個用戶發送過來的全部場景,處理完再返回給客戶端,客戶端解析響應,渲染最新的場景展現給玩家。
若是出現一個數據包丟失,全部事情都須要停下來等待這個數據包重發。客戶端會出現等待接收數據,然而玩家並不關心過時的數據,激戰中卡 1 秒,等能動了都已經死了。

遊戲對實時要求較爲嚴格的狀況下,採用自定義的可靠 UDP 協議,自定義重傳策略,可以把丟包產生的延遲降到最低,儘可能減小網絡問題對遊戲性形成的影響。

IoT 物聯網

一方面,物聯網領域終端資源少, 極可能只是個內存很是小的嵌入式系統,而維護 TCP 協議代價太大;另外一方面,物聯網對實時性要求也很高, 而 TCP 仍是由於上面的那些緣由致使時延大。Google 旗下的 Nest 創建 Thread Group,推出了物聯網通訊協議 Thread,就是基於 UDP 協議的。

移動通訊領域

在 4G 網絡裏,移動流量上網的數據面對的協議 GTP-U 是基於 UDP 的。由於移動網絡協議比較複雜,而 GTP 協議自己就包含複雜的手機上線下線的通訊協議。若是基於 TCP,TCP 的機制就顯得很是多餘。

10、TCP 協議

TCP 協議。它之因此這麼複雜,那是由於它秉承的是「性惡論」。它自然認爲網絡環境是惡劣的,丟包、亂序、重傳,擁塞都是常有的事情,一言不合就可能送達不了,於是要從算法層面來保證可靠性。

TCP 包頭格式

image.png

  1. 包的序號。 爲何要給包編號呢?固然是爲了解決亂序的問題。不編好號怎麼確認哪一個應該先來,哪一個應該後到呢。編號是爲了解決亂序問題。
  2. 確認序號。 發出去的包應該有確認,要否則我怎麼知道對方有沒有收到呢?若是沒有收到就應該從新發送,直到送達。這個能夠解決不丟包的問題。
    TCP 是靠譜的協議,可是這不能說明它面臨的網絡環境好。從 IP 層面來說,若是網絡情況的確那麼差,是沒有任何可靠性保證的,而做爲 IP 的上一層 TCP 也無能爲力,惟一能作的就是更加努力,不斷重傳,經過各類算法保證。也就是說,對於 TCP 來說,IP 層你丟不丟包,我管不着,可是我在個人層面上,會努力保證可靠性。
  3. 狀態位。 例如 SYN 是發起一個鏈接,ACK 是回覆,RST 是從新鏈接,FIN 是結束鏈接等。TCP 是面向鏈接的,於是雙方要維護鏈接的狀態,這些帶狀態位的包的發送,會引發雙方的狀態變動。
  4. 窗口大小。 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,也即生存時間。好了,雙方終於創建了信任,創建了鏈接。前面也說過,爲了維護這個鏈接,雙方都要維護一個狀態機,在鏈接創建的過程當中,雙方的狀態變化時序圖就像這樣。

image.png

一開始,客戶端和服務端都處於 CLOSED 狀態。先是服務端主動監聽某個端口,處於LISTEN 狀態。而後客戶端主動發起鏈接 SYN,以後處於 SYN-SENT 狀態。服務端收到發起的鏈接,返回 SYN,而且 ACK 客戶端的 SYN,以後處於 SYN-RCVD 狀態。客戶端收到服務端發送的 SYN 和 ACK 以後,發送 ACK 的 ACK,以後處於 ESTABLISHED 狀態,由於它一發一收成功了。服務端收到 ACK 的 ACK 以後,處於 ESTABLISHED 狀態,由於它也一發一收了。

TCP 的四次揮手

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 是還有事情要處理,仍是過一下子會發送結束。
image.png

斷開的時候,咱們能夠看到,當 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 狀態機

將鏈接創建和鏈接斷開的兩個時序狀態圖綜合起來,就是這個著名的 TCP 的狀態機。
image.png

如何實現一個靠譜的協議?

爲了保證順序性,每個包都有一個 ID。在創建鏈接的時候,會商定起始的 ID 是什麼,而後按照 ID 一個個發送。爲了保證不丟包,對於發送的包都要進行應答,可是這個應答也不是一個一個來的,而是會應答某個以前的 ID,表示都收到了,這種模式稱爲累計確認或者累計應答(cumulative acknowledgment)。

爲了記錄全部發送的包和接收的包,TCP 也須要發送端和接收端分別都有緩存來保存這些記錄。發送端的緩存裏是按照包的 ID 一個個排列,根據處理的狀況分紅四個部分。

  1. 發送了而且已經確認的。這部分就是你交代下屬的,而且也作完了的,應該劃掉的。
  2. 發送了而且還沒有確認的。這部分是你交代下屬的,可是還沒作完的,須要等待作完的回覆以後,才能劃掉。
  3. 沒有發送,可是已經等待發送的。這部分是你尚未交代給下屬,可是立刻就要交代的。
  4. 沒有發送,而且暫時還不會發送的。這部分是你尚未交代給下屬,並且暫時還不會交代給下屬的。

這裏面爲何要區分第三部分和第四部分呢?沒交代的,一會兒全交代了不就完了嗎?這就是咱們上一節提到的「流量控制,把握分寸」。

在 TCP 裏,接收端會給發送端報一個窗口的大小,叫Advertised window。這個窗口的大小應該等於上面的第二部分加上第三部分,就是已經交代了沒作完的加上立刻要交代的。超過這個窗口的,接收端作不過來,就不能發送了。因而,發送端須要保持下面的數據結構。
image.png

對於接收端來說,它的緩存裏記錄的內容要簡單一些。

  1. 接受而且確認過的。也就是我領導交代給我,而且我作完的。
  2. 還沒接收,可是立刻就能接收的。也便是我本身的可以接受的最大工做量。
  3. 還沒接收,也無法接收的。也即超過工做量的部分,實在作不完。

image.png

順序問題與丟包問題

仍是剛纔的圖,在發送端來看,一、二、3 已經發送並確認;四、五、六、七、八、9 都是發送了還沒確認;十、十一、12 是還沒發出的;1三、1四、15 是接收方沒有空間,不許備發的。在接收端來看,一、二、三、四、5 是已經完成 ACK,可是沒讀取的;六、7 是等待接收的;八、9 是已經接收,可是沒有 ACK 的。
發送端和接收端當前的狀態以下:

  • 一、二、3 沒有問題,雙方達成了一致。
  • 四、5 接收方說 ACK 了,可是發送方還沒收到,有可能丟了,有可能在路上。
  • 六、七、八、9 確定都發了,可是 八、9 已經到了,可是 六、7 沒到,出現了亂序,緩存着可是沒辦法 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 個包也能夠發送了。
image.png

這個時候,假設發送端發送過猛,會將第三部分的 十、十一、十二、13 所有發送完畢,以後就中止發送了,未發送可發送部分爲 0。
image.png
當對於包 5 的確認到達的時候,在客戶端至關於窗口再滑動了一格,這個時候,才能夠有更多的包能夠發送了,例如第 14 個包才能夠發送
image.png
若是接收方實在處理的太慢,致使緩存中沒有空間了,能夠經過確認信息修改窗口的大小,甚至能夠設置爲 0,則發送方將暫時中止發送。

咱們假設一個極端狀況,接收端的應用一直不讀取緩存中的數據,當數據包 6 確認後,窗
口大小就不能再是 9 了,就要縮小一個變爲 8。
image.png
這個新的窗口 8 經過 6 的確認消息到達發送端的時候,你會發現窗口沒有平行右移,而是僅僅左面的邊右移了,窗口的大小從 9 改爲了 8。
image.png
若是接收端仍是一直不處理數據,則隨着確認的包愈來愈多,窗口愈來愈小,直到爲 0。
image.png
當這個窗口經過包 14 的確認到達發送端的時候,發送端的窗口也調整爲 0,中止發送。
image.png
若是這樣的話,發送方會定時發送窗口探測數據包,看是否有機會調整窗口的大小。當接收方比較慢的時候,要防止低能窗口綜合徵,別空出一個字節來就趕快告訴發送方,而後立刻又填滿了,能夠當窗口過小的時候,不更新窗口,直到達到必定大小,或者緩衝區一半爲空,才更新窗口。這就是咱們常說的流量控制。

擁塞控制問題

也是經過窗口的大小來控制的,前面的滑動窗口 rwnd是怕發送方把接收方緩存塞滿,而擁塞窗口 cwnd,是怕把網絡塞滿。

這裏有一個公式 LastByteSent - LastByteAcked <= min {cwnd, rwnd} ,是擁塞窗口和滑動窗口共同控制發送的速度。

那發送方怎麼判斷網絡是否是滿呢?這實際上是個挺難的事情,由於對於 TCP 協議來說,他壓根不知道整個網絡路徑都會經歷什麼,對他來說就是一個黑盒。TCP 發送包常被比喻爲往一個水管裏面灌水,而 TCP 的擁塞控制就是在不堵塞,不丟包的狀況下,儘可能發揮帶寬。水管有粗細,網絡有帶寬,也即每秒鐘可以發送多少數據;水管有長度,端到端有時延。在理想狀態下,水管裏面水的量 = 水管粗細 x 水管長度。對於到網絡上,通道的容量 = 帶寬× 往返延遲。
若是咱們設置發送窗口,使得發送但未確認的包爲爲通道的容量,就可以撐滿整個管道。
image.png
如圖所示,假設往返時間爲 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,也就是沒有一晚上回到解放前,而是還在比較高的值,呈線性增加。
image.png
就像前面說的同樣,正是這種知進退,使得時延很重要的狀況下,反而下降了速度。可是若是你仔細想一下,TCP 的擁塞控制主要來避免的兩個現象都是有問題的。

  1. 丟包並不表明着通道滿了,也多是管子原本就漏水。例如公網上帶寬不滿也會丟包,這個時候就認爲擁塞了,退縮了,實際上是不對的。
  2. TCP 的擁塞控制要等到將中間設備都填充滿了,才發生丟包,從而下降速度,這時候已經晚了。其實 TCP 只要填滿管道就能夠了,不該該接着填,直到連緩存也填滿。

爲了優化這兩個問題,後來有了TCP BBR 擁塞算法。 它企圖找到一個平衡點,就是經過不斷的加快發送速度,將管道填滿,可是不要填滿中間設備的緩存,由於這樣時延會增長,在這個平衡點能夠很好的達到高帶寬和低時延的平衡。
image.png

相關文章
相關標籤/搜索