1.5w字 + 24張圖肝翻 TCP。

TCP 是一種面向鏈接的單播協議,在 TCP 中,並不存在多播、廣播的這種行爲,由於 TCP 報文段中能明確發送方和接受方的 IP 地址。程序員

在發送數據前,相互通訊的雙方(即發送方和接受方)須要創建一條鏈接,在發送數據後,通訊雙方須要斷開鏈接,這就是 TCP 鏈接的創建和終止。算法

TCP 鏈接的創建和終止

若是你看過我以前寫的關於網絡層的一篇文章,你應該知道 TCP 的基本元素有四個:即發送方的 IP 地址、發送方的端口號、接收方的 IP 地址、接收方的端口號。而每一方的 IP + 端口號均可以看做是一個套接字,套接字可以被惟一標示。套接字就至關因而門,出了這個門,就要進行數據傳輸了。shell

TCP 的鏈接創建 -> 終止總共分爲三個階段緩存

下面咱們所討論的重點也是集中在這三個層面。服務器

下圖是一個很是典型的 TCP 鏈接的創建和關閉過程,其中不包括數據傳輸的部分。微信

TCP 創建鏈接 - 三次握手

  1. 服務端進程準備好接收來自外部的 TCP 鏈接,通常狀況下是調用 bind、listen、socket 三個函數完成。這種打開方式被認爲是 被動打開(passive open)。而後服務端進程處於 LISTEN 狀態,等待客戶端鏈接請求。
  2. 客戶端經過 connect 發起主動打開(active open),向服務器發出鏈接請求,請求中首部同步位 SYN = 1,同時選擇一個初始序號 sequence ,簡寫 seq = x。SYN 報文段不容許攜帶數據,只消耗一個序號。此時,客戶端進入 SYN-SEND 狀態。
  3. 服務器收到客戶端鏈接後,,須要確認客戶端的報文段。在確認報文段中,把 SYN 和 ACK 位都置爲 1 。確認號是 ack = x + 1,同時也爲本身選擇一個初始序號 seq = y。這個報文段也不能攜帶數據,但一樣要消耗掉一個序號。此時,TCP 服務器進入 SYN-RECEIVED(同步收到) 狀態。
  4. 客戶端在收到服務器發出的響應後,還須要給出確認鏈接。確認鏈接中的 ACK 置爲 1 ,序號爲 seq = x + 1,確認號爲 ack = y + 1。TCP 規定,這個報文段能夠攜帶數據也能夠不攜帶數據,若是不攜帶數據,那麼下一個數據報文段的序號還是 seq = x + 1。這時,客戶端進入 ESTABLISHED (已鏈接) 狀態
  5. 服務器收到客戶的確認後,也進入 ESTABLISHED 狀態。

這是一個典型的三次握手過程,經過上面 3 個報文段就可以完成一個 TCP 鏈接的創建。三次握手的的目的不只僅在於讓通訊雙方知曉正在創建一個鏈接,也在於利用數據包中的選項字段來交換一些特殊信息,交換初始序列號網絡

通常首個發送 SYN 報文的一方被認爲是主動打開一個鏈接,而這一方一般也被稱爲 客戶端。而 SYN 的接收方一般被稱爲 服務端,它用於接收這個 SYN,併發送下面的 SYN,所以這種打開方式是被動打開。

TCP 創建一個鏈接須要三個報文段,釋放一個鏈接卻須要四個報文段。併發

TCP 斷開鏈接 - 四次揮手

數據傳輸結束後,通訊的雙方能夠釋放鏈接。數據傳輸結束後的客戶端主機和服務端主機都處於 ESTABLISHED 狀態,而後進入釋放鏈接的過程。socket

TCP 斷開鏈接須要歷經的過程以下tcp

  1. 客戶端應用程序發出釋放鏈接的報文段,並中止發送數據,主動關閉 TCP 鏈接。客戶端主機發送釋放鏈接的報文段,報文段中首部 FIN 位置爲 1 ,不包含數據,序列號位 seq = u,此時客戶端主機進入 FIN-WAIT-1(終止等待 1) 階段。
  2. 服務器主機接受到客戶端發出的報文段後,即發出確認應答報文,確認應答報文中 ACK = 1,生成本身的序號位 seq = v,ack = u + 1,而後服務器主機就進入 CLOSE-WAIT(關閉等待) 狀態。
  3. 客戶端主機收到服務端主機的確認應答後,即進入 FIN-WAIT-2(終止等待2) 的狀態。等待客戶端發出鏈接釋放的報文段。
  4. 這時服務端主機會發出斷開鏈接的報文段,報文段中 ACK = 1,序列號 seq = v,ack = u + 1,在發送完斷開請求的報文後,服務端主機就進入了 LAST-ACK(最後確認)的階段。
  5. 客戶端收到服務端的斷開鏈接請求後,客戶端須要做出響應,客戶端發出斷開鏈接的報文段,在報文段中,ACK = 1, 序列號 seq = u + 1,由於客戶端從鏈接開始斷開後就沒有再發送數據,ack = v + 1,而後進入到 TIME-WAIT(時間等待) 狀態,請注意,這個時候 TCP 鏈接尚未釋放。必須通過時間等待的設置,也就是 2MSL 後,客戶端纔會進入 CLOSED 狀態,時間 MSL 叫作最長報文段壽命(Maximum Segment Lifetime)
  6. 服務端主要收到了客戶端的斷開鏈接確認後,就會進入 CLOSED 狀態。由於服務端結束 TCP 鏈接時間要比客戶端早,而整個鏈接斷開過程須要發送四個報文段,所以釋放鏈接的過程也被稱爲四次揮手。

TCP 鏈接的任意一方均可以發起關閉操做,只不過一般狀況下發起關閉鏈接操做通常都是客戶端。然而,一些服務器好比 Web 服務器在對請求做出相應後也會發起關閉鏈接的操做。TCP 協議規定經過發送一個 FIN 報文來發起關閉操做。

因此綜上所述,創建一個 TCP 鏈接須要三個報文段,而關閉一個 TCP 鏈接須要四個報文段。TCP 協議還支持一種半開啓(half-open) 狀態,雖然這種狀況並很少見。

TCP 半開啓

TCP 鏈接處於半開啓的這種狀態是由於鏈接的一方關閉或者終止了這個 TCP 鏈接卻沒有通知另外一方,也就是說兩我的正在微信聊天,cxuan 你下線了你不告訴我,我還在跟你侃八卦呢。此時就認爲這條鏈接處於半開啓狀態。這種狀況發生在通訊中的一方處於主機崩潰的狀況下,你 xxx 的,我電腦死機了我咋告訴你?只要處於半鏈接狀態的一方不傳輸數據的話,那麼是沒法檢測出來對方主機已經下線的。

另一種處於半開啓狀態的緣由是通訊的一方關閉了主機電源 而不是正常關機。這種狀況下會致使服務器上有不少半開啓的 TCP 鏈接。

TCP 半關閉

既然 TCP 支持半開啓操做,那麼咱們能夠設想 TCP 也支持半關閉操做。一樣的,TCP 半關閉也並不常見。TCP 的半關閉操做是指僅僅關閉數據流的一個傳輸方向。兩個半關閉操做合在一塊兒就可以關閉整個鏈接。在通常狀況下,通訊雙方會經過應用程序互相發送 FIN 報文段來結束鏈接,可是在 TCP 半關閉的狀況下,應用程序會代表本身的想法:"我已經完成了數據的發送發送,併發送了一個 FIN 報文段給對方,可是我依然但願接收來自對方的數據直到它發送一個 FIN 報文段給我"。 下面是一個 TCP 半關閉的示意圖。

解釋一下這個過程:

首先客戶端主機和服務器主機一直在進行數據傳輸,一段時間後,客戶端發起了 FIN 報文,要求主動斷開鏈接,服務器收到 FIN 後,迴應 ACK ,因爲此時發起半關閉的一方也就是客戶端仍然但願服務器發送數據,因此服務器會繼續發送數據,一段時間後服務器發送另一條 FIN 報文,在客戶端收到 FIN 報文迴應 ACK 給服務器後,斷開鏈接。

TCP 的半關閉操做中,鏈接的一個方向被關閉,而另外一個方向仍在傳輸數據直到它被關閉爲止。只不過不多有應用程序使用這一特性。

同時打開和同時關閉

還有一種比較很是規的操做,這就是兩個應用程序同時主動打開鏈接。雖然這種狀況看起來不太可能,可是在特定的安排下倒是有可能發生的。咱們主要講述這個過程。

通訊雙方在接收到來自對方的 SYN 以前會首先發送一個 SYN,這個場景還要求通訊雙方都知道對方的 IP 地址 + 端口號

下面是同時打開的例子

如上圖所示,通訊雙方都在收到對方報文前主動發送了 SYN 報文,都在收到彼此的報文後回覆了一個 ACK 報文。

一個同時打開過程須要交換四個報文段,比普通的三次握手增長了一個,因爲同時打開沒有客戶端和服務器一說,因此這裏我用了通訊雙方來稱呼。

像同時打開同樣,同時關閉也是通訊雙方同時提出主動關閉請求,發送 FIN 報文,下圖顯示了一個同時關閉的過程。

同時關閉過程當中須要交換和正常關閉相同數量的報文段,只不過同時關閉不像四次揮手那樣順序進行,而是交叉進行的。

聊一聊初始序列號

也許是我上面圖示或者文字描述的不專業,初始序列號它是有專業術語表示的,初始序列號的英文名稱是Initial sequence numbers (ISN),因此咱們上面表示的 seq = v,其實就表示的 ISN。

在發送 SYN 以前,通訊雙方會選擇一個初始序列號。初始序列號是隨機生成的,每個 TCP 鏈接都會有一個不一樣的初始序列號。RFC 文檔指出初始序列號是一個 32 位的計數器,每 4 us(微秒) + 1。由於每一個 TCP 鏈接都是一個不一樣的實例,這麼安排的目的就是爲了防止出現序列號重疊的狀況。

當一個 TCP 鏈接創建的過程當中,只有正確的 TCP 四元組和正確的序列號纔會被對方接收。這也反應了 TCP 報文段容易被僞造 的脆弱性,由於只要我僞造了一個相同的四元組和初始序列號就可以僞造 TCP 鏈接,從而打斷 TCP 的正常鏈接,因此抵禦這種攻擊的一種方式就是使用初始序列號,另一種方法就是加密序列號。

TCP 狀態轉換

咱們上面聊到了三次握手和四次揮手,提到了一些關於 TCP 鏈接之間的狀態轉換,那麼下面我就從頭開始和你好好梳理一下這些狀態之間的轉換。

首先第一步,剛開始時服務器和客戶端都處於 CLOSED 狀態,這時須要判斷是主動打開仍是被動打開,若是是主動打開,那麼客戶端向服務器發送 SYN 報文,此時客戶端處於 SYN-SEND 狀態,SYN-SEND 表示發送鏈接請求後等待匹配的鏈接請求,服務器被動打開會處於 LISTEN 狀態,用於監聽 SYN 報文。若是客戶端調用了 close 方法或者通過一段時間沒有操做,就會從新變爲 CLOSED 狀態,這一步轉換圖以下

這裏有個疑問,爲何處於 LISTEN 狀態下的客戶端還會發送 SYN 變爲 SYN_SENT 狀態呢?

知乎看到了車小胖大佬的回答,這種狀況可能出如今 FTP 中,LISTEN -> SYN_SENT 是由於這個鏈接多是因爲服務器端的應用有數據發送給客戶端所觸發的,客戶端被動接受鏈接,鏈接創建後,開始傳輸文件。也就是說,處於 LISTEN 狀態的服務器也是有可能發送 SYN 報文的,只不過這種狀況很是少見。

處於 SYN_SEND 狀態的服務器會接收 SYN 併發送 SYN 和 ACK 轉換成爲 SYN_RCVD 狀態,一樣的,處於 LISTEN 狀態的客戶端也會接收 SYN 併發送 SYN 和 ACK 轉換爲 SYN_RCVD 狀態。若是處於 SYN_RCVD 狀態的客戶端收到 RST 就會變爲 LISTEN 狀態。

這兩張圖一塊兒看會比較好一些。

這裏須要解釋下什麼是 RST

這裏有一種狀況是當主機收到 TCP 報文段後,其 IP 和端口號不匹配的狀況。假設客戶端主機發送一個請求,而服務器主機通過 IP 和端口號的判斷後發現不是給這個服務器的,那麼服務器就會發出一個 RST 特殊報文段給客戶端。

所以,當服務端發送一個 RST 特殊報文段給客戶端的時候,它就會告訴客戶端沒有匹配的套接字鏈接,請不要再繼續發送了

RST:(Reset the connection)用於復位因某種緣由引發出現的錯誤鏈接,也用來拒絕非法數據和請求。若是接收到 RST 位時候,一般發生了某些錯誤。

上面沒有識別正確的 IP 端口是一種致使 RST 出現的狀況,除此以外,RST 還可能因爲請求超時、取消一個已存在的鏈接等出現。

位於 SYN_RCVD 的服務器會接收 ACK 報文,SYN_SEND 的客戶端會接收 SYN 和 ACK 報文,併發送 ACK 報文,由此,客戶端和服務器之間的鏈接就創建了。

這裏還要注意一點,同時打開的狀態我在上面沒有刻意表示出來,實際上,在同時打開的狀況下,它的狀態變化是這樣的。

爲何會是這樣呢?由於你想,在同時打開的狀況下,兩端主機都發起 SYN 報文,而主動發起 SYN 的主機會處於 SYN-SEND 狀態,發送完成後,會等待接收 SYN 和 ACK , 在雙方主機都發送了 SYN + ACK 後,雙方都處於 SYN-RECEIVED(SYN-RCVD) 狀態,而後等待 SYN + ACK 的報文到達後,雙方就會處於 ESTABLISHED 狀態,開始傳輸數據。

好了,到如今爲止,我給你敘述了一下 TCP 鏈接創建過程當中的狀態轉換,如今你能夠泡一壺茶喝點水,等着數據傳輸了。

好了,如今水喝夠了,這時候數據也傳輸完成了,數據傳輸完成後,這條 TCP 鏈接就能夠斷開了。

如今咱們把時鐘往前撥一下,調整到服務端處於 SYN_RCVD 狀態的時刻,由於剛收到了 SYN 包併發送了 SYN + ACK 包,此時服務端很開心,可是這時,服務端應用進程關閉了,而後應用進程發了一個 FIN 包,就會讓服務器從 SYN_RCVD -> FIN_WAIT_1 狀態。

而後把時鐘調到如今,客戶端和服務器如今已經傳輸完數據了 ,此時客戶端發送了一條 FIN 報文但願斷開鏈接,此時客戶端也會變爲 FIN_WAIT_1 狀態,對於服務器來講,它接收到了 FIN 報文段並回復了 ACK 報文,就會從 ESTABLISHED -> CLOSE_WAIT 狀態。

位於 CLOSE_WAIT 狀態的服務端會發送 FIN 報文,而後把本身置於 LAST_ACK 狀態。處於 FIN_WAIT_1 的客戶端接收 ACK 消息就會變爲 FIN_WAIT_2 狀態。

這裏須要先解釋一下 CLOSING 這個狀態,FIN_WAIT_1 -> CLOSING 的轉換比較特殊

CLOSING 這種狀態比較特殊,實際狀況中應該是不多見,屬於一種比較罕見的例外狀態。正常狀況下,當你發送FIN 報文後,按理來講是應該先收到(或同時收到)對方的 ACK 報文,再收到對方的 FIN 報文。可是 CLOSING 狀態表示你發送 FIN 報文後,並無收到對方的 ACK 報文,反而卻也收到了對方的 FIN 報文。

什麼狀況下會出現此種狀況呢?其實細想一下,也不可貴出結論:那就是若是雙方在同時關閉一個連接的話,那麼就出現了同時發送 FIN 報文的狀況,也即會出現 CLOSING 狀態,表示雙方都正在關閉鏈接。

FIN_WAIT_2 狀態的客戶端接收服務端主機發送的 FIN + ACK 消息,併發送 ACK 響應後,會變爲 TIME_WAIT 狀態。處於 CLOSE_WAIT 的客戶端發送 FIN 會處於 LAST_ACK 狀態。

這裏很多圖和博客雖然在圖上畫的是 FIN + ACK 報文後纔會處於 LAST_ACK 狀態,可是描述的時候,通常一般只對於 FIN 進行描述。也就是說 CLOSE_WAIT 發送 FIN 纔會處於 LAST_ACK 狀態。

因此這裏 FIN_WAIT_1 -> TIME_WAIT 的狀態也就是接收 FIN 和 ACK 併發送 ACK 以後,客戶端處於的狀態。

而後位於 CLOSINIG 狀態的客戶端這時候還有 ACK 接收的話,會繼續處於 TIME_WAIT 狀態,能夠看到,TIME_WAIT 狀態至關因而客戶端在關閉前的最後一個狀態,它是一種主動關閉的狀態;而 LAST_ACK 是服務端在關閉前的最後一個狀態,它是一種被動打開的狀態。

上面有幾個狀態比較特殊,這裏咱們向西解釋下。

TIME_WAIT 狀態

通訊雙方創建 TCP 鏈接後,主動關閉鏈接的一方就會進入 TIME_WAIT 狀態。TIME_WAIT 狀態也稱爲 2MSL 的等待狀態。在這個狀態下,TCP 將會等待最大段生存期(Maximum Segment Lifetime, MSL) 時間的兩倍。

這裏須要解釋下 MSL

MSL 是 TCP 段指望的最大生存時間,也就是在網絡中存在的最長時間。這個時間是有限制的,由於咱們知道 TCP 是依靠 IP 數據段來進行傳輸的,IP 數據報中有 TTL 和跳數的字段,這兩個字段決定了 IP 的生存時間,通常狀況下,TCP 的最大生存時間是 2 分鐘,不過這個數值是能夠修改的,根據不一樣操做系統能夠修改此值。

基於此,咱們來探討 TIME_WAIT 的狀態。

當 TCP 執行一個主動關閉併發送最終的 ACK 時,TIME_WAIT 應該以 2 * 最大生存時間存在,這樣就可以讓 TCP 從新發送最終的 ACK 以免出現丟失的狀況。從新發送最終的 ACK 並非由於 TCP 重傳了 ACK,而是由於通訊另外一方重傳了 FIN,客戶端常常回發送 FIN,由於它須要 ACK 的響應纔可以關閉鏈接,若是生存時間超過了 2MSL 的話,客戶端就會發送 RST,使服務端出錯。

TCP 超時和重傳

沒有永遠不出錯誤的通訊,這句話代表着無論外部條件多麼完備,永遠都會有出錯的可能。因此,在 TCP 的正常通訊過程當中,也會出現錯誤,這種錯誤多是因爲數據包丟失引發的,也多是因爲數據包重複引發的,甚至多是因爲數據包失序 引發的。

TCP 的通訊過程當中,會由 TCP 的接收端返回一系列的確認信息來判斷是否出現錯誤,一旦出現丟包等狀況,TCP 就會啓動重傳操做,重傳還沒有確認的數據。

TCP 的重傳有兩種方式,一種是基於時間,一種是基於確認信息,通常經過確認信息要比經過時間更加高效。

因此從這點就能夠看出,TCP 的確認和重傳,都是基於數據包是否被確認爲前提的。

TCP 在發送數據時會設置一個定時器,若是在定時器指定的時間內未收到確認信息,那麼就會觸發相應的超時或者基於計時器的重傳操做,計時器超時一般被稱爲重傳超時(RTO)

可是有另一種不會引發延遲的方式,這就是快速重傳

TCP 在每次重傳一次報文後,其重傳時間都會加倍,這種"間隔時間加倍"被稱爲二進制指數補償(binary exponential backoff) 。等到間隔時間加倍到 15.5 min 後,客戶端會顯示

Connection closed by foreign host.

TCP 擁有兩個閾值來決定如何重傳一個報文段,這兩個閾值被定義在 RFC[RCF1122] 中,第一個閾值是 R1,它表示願意嘗試重傳的次數,閾值 R2 表示 TCP 應該放棄鏈接的時間。R1 和 R2 應至少設爲三次重傳和 100 秒放棄 TCP 鏈接。

這裏須要注意下,對鏈接創建報文 SYN 來講,它的 R2 至少應該設置爲 3 分鐘,可是在不一樣的系統中,R1 和 R2 值的設置方式也不一樣。

在 Linux 系統中,R1 和 R2 的值能夠經過應用程序來設置,或者是修改 net.ipv4.tcp_retries1 和 net.ipv4.tcp_retries2 的值來設置。變量值就是重傳次數。

tcp_retries2 的默認值是 15,這個充實次數的耗時大約是 13 - 30 分鐘,這只是一個大概值,最終耗時時間還要取決於 RTO ,也就是重傳超時時間。tcp_retries1 的默認值是 3 。

對於 SYN 段來講,net.ipv4.tcp_syn_retries 和 net.ipv4.tcp_synack_retries 這兩個值限制了 SYN 的重傳次數,默認是 5,大約是 180 秒。

Windows 操做系統下也有 R1 和 R2 變量,它們的值被定義在下方的註冊表中

HKLM\System\CurrentControlSet\Services\Tcpip\Parameters
HKLM\System\CurrentControlSet\Services\Tcpip6\Parameters

其中有一個很是重要的變量就是 TcpMaxDataRetransmissions,這個 TcpMaxDataRetransmissions 對應 Linux 中的 tcp_retries2 變量,默認值是 5。這個值的意思表示的是 TCP 在現有鏈接上未確認數據段的次數。

快速重傳

咱們上面提到了快速重傳,實際上快速重傳機制是基於接收端的反饋信息來觸發的,它並不受重傳計時器的影響。因此與超時重傳相比,快速重傳可以有效的修復丟包狀況。當 TCP 鏈接的過程當中接收端出現亂序的報文(好比 2 - 4 - 3)到達時,TCP 須要馬上生成確認消息,這種確認消息也被稱爲重複 ACK

當失序報文到達時,重複 ACK 要作到馬上返回,不容許延遲發送,此舉的目的是要告訴發送方某段報文失序到達了,但願發送方指出失序報文段的序列號。

還有一種狀況也會致使重複 ACK 發給發送方,那就是當前報文段的後續報文發送至接收端,由此能夠判斷當前發送方的報文段丟失或者延遲到達。由於這兩種狀況致使的後果都是接收方沒有收到報文,可是咱們卻沒法判斷究竟是報文段丟失仍是報文段沒有送達。所以 TCP 發送端會等待必定數目的重複 ACK 被接受來決定數據是否丟失並觸發快速重傳。通常這個判斷的數量是 3,這段文字表述可能沒法清晰理解,咱們舉個例子。

如上圖所示,報文段 1 成功接收並被確認爲 ACK 2,接收端的期待序號爲 2,當報文段 2 丟失後,報文段 3。失序到達,可是與接收端的指望不匹配,因此接收端會重複發送冗餘 ACK 2。

這樣,在超時重傳定時器到期以前,接收收到連續三個相同的 ACK 後,發送端就知道哪一個報文段丟失了,因而發送方會重發這個丟失的報文段,這樣就不用等待重傳定時器的到期,大大提升了效率。

SACK

在標準的 TCP 確認機制中,若是發送方發送了 0 - 10000 序號之間的數據,可是接收方只接收到了 0 -1000, 3000 - 10000 之間的數據,而 1000 - 3000 之間的數據沒有到達接收端,此時發送方會重傳 1000 - 10000 之間的數據,實際上這是沒有必要的,由於 3000 後面的數據已經被接收了。可是發送方沒法感知這種狀況的存在。

如何避免或者說解決這種問題呢?

爲了優化這種狀況,咱們有必要讓客戶端知道更多的消息,在 TCP 報文段中,有一個 SACK 選項字段,這個字段是一種選擇性確認(selective acknowledgment)機制,這個機制能告訴 TCP 客戶端,用咱們的俗語來解釋就是:「我這裏最多容許接收 1000 以後的報文段,可是我卻收到了 3000 - 10000 的報文段,請給我 1000 - 3000 之間的報文段」。

可是,這個選擇性確認機制的是否開啓還受一個字段的影響,這個字段就是 SACK 容許選項字段,通訊雙方在 SYN 段或者 SYN + ACK 段中添加 SACK 容許選項字段來通知對端主機是否支持 SACK,若是雙方都支持的話,後續在 SYN 段中就可使用 SACK 選項了。

這裏須要注意下:SACK 選項字段只能出如今 SYN 段中。

僞超時和重傳

在某些狀況下,即便沒有出現報文段的丟失也可能會引起報文重傳。這種重傳行爲被稱爲 僞重傳(spurious retransmission) ,這種重傳是沒有必要的,形成這種狀況的因素多是因爲僞超時(spurious timeout),僞超時的意思就是過早的斷定超時發生。形成僞超時的因素有不少,好比報文段失序到達,報文段重複,ACK 丟失等狀況。

檢測和處理僞超時的方法有不少,這些方法統稱爲檢測算法和響應算法。檢測算法用於判斷是否出現了超時現象或出現了計時器的重傳現象。一旦出現了超時或者重傳的狀況,就會執行響應算法撤銷或者減輕超時帶來的影響,下面是幾種算法,此篇文章暫不深刻這些實現細節

  • 重複 SACK 擴展- DSACK
  • Eifel 檢測算法
  • 前移 RTO 恢復 - F-RTO
  • Eifel 響應算法

包失序和包重複

上面咱們討論的都是 TCP 如何處理丟包的問題,咱們下面來討論一下包失序和包重複的問題。

包失序

數據包的失序到達是互聯網中極其容易出現的一種狀況,因爲 IP 層並不能保證數據包的有序性,每一個數據包的發送均可能會選擇當前狀況傳輸速度最快的鏈路,因此頗有可能出現發送了 A - > B -> C 的三個數據包,到達接收端的數據包順序是 C -> A -> B 或者 B -> C -> A 等等。這就是包失序的一種現象。

在包傳輸中,主要分爲兩種鏈路:正向鏈路(SYN)和反向鏈路(ACK)

若是失序發生在正向鏈路,TCP 是沒法正確判斷數據包是否丟失的,數據的丟失和失序都會致使接收端收到無序的數據包,形成數據之間的空缺。若是這種空缺不夠大的話,這種狀況影響不大;可是若是空缺比較大的話,可能會致使僞重傳。

若是失序發生在反向鏈路,就會使 TCP 的窗口前移,而後收到重複而應該被丟棄的 ACK,致使發送端出現沒必要要的流量突發,影響可用網絡帶寬。

回到咱們上面討論的快速重傳,因爲快速重傳是根據重複 ACK 推斷出現丟包而啓動的,它不用等到重傳計時器超時。因爲 TCP 接收端會對接收到的失序報文馬上返回 ACK,因此網絡中任何一個失序到達的報文均可能會形成重複 ACK。假設一旦收到 ACK,就會啓動快速重傳機制,當 ACK 數量激增,就會致使大量沒必要要的重傳發生,因此快速重傳應該達到重複閾值(dupthresh) 再觸發。可是在互聯網中,嚴重的失序並不常見,所以 dupthresh 的值能夠設置的儘可能小,通常來講 3 就能處理絕大部分狀況。

包重複

包重複也是互聯網中出現不多的一種狀況,它指的是在網絡傳輸過程當中,包可能會出現傳輸屢次的狀況,當重傳生成時,TCP 可能會出現混淆。

包的重複可使接收端生成一系列的重複 ACK,這種狀況可使用 SACK 協商來解決。

TCP 數據流和窗口管理

咱們在 40 張圖帶你搞懂 TCP 和 UDP 這篇文章中知道了可使用滑動窗口來實現流量控制,也就是說,客戶端和服務器能夠相互提供數據流信息的交換,數據流的相關信息主要包括報文段序列號、ACK 號和窗口大小

圖中的兩個箭頭表示數據流方向,數據流方向也就是 TCP 報文段的傳輸方向。能夠看到,每一個 TCP 報文段中都包括了序列號、ACK 和窗口信息,可能還會有用戶數據。TCP 報文段中的窗口大小表示接收端還可以接收的緩存空間的大小,以字節爲單位。這個窗口大小是一種動態的,由於無時無刻都會有報文段的接收和消失,這種動態調整的窗口大小咱們稱之爲滑動窗口,下面咱們就來具體認識一下滑動窗口。

滑動窗口

TCP 鏈接的每一端均可以發送數據,可是數據的發送不是沒有限制的,實際上,TCP 鏈接的兩端都各自維護了一個發送窗口結構 (send window structure)接收窗口結構 (receive window structure),這兩個窗口結構就是數據發送的限制。

發送方窗口

下圖是一個發送方窗口的示例。

在這幅圖中,涉及滑動窗口的四種概念:

  • 已經發送並確認的報文段:發送給接收方後,接收方回回復 ACK 來對報文段進行響應,圖中標註綠色的報文段就是已經通過接收方確認的報文段。
  • 已經發送可是還沒確認的報文段:圖中綠色區域是通過接收方確認的報文段,而淺藍色這段區域指的是已經發送可是還未通過接收方確認的報文段。
  • 等待發送的報文段:圖中深藍色區域是等待發送的報文段,它屬於發送窗口結構的一部分,也就是說,發送窗口結構實際上是由已發送未確認 + 等待發送的報文段構成。
  • 窗口滑動時才能發送的報文段:若是圖中的 [4,9] 這個集合內的報文段發送完畢後,整個滑動窗口會向右移動,圖中橙色區域就是窗口右移時才能發送的報文段。

滑動窗口也是有邊界的,這個邊界是 Left edgeRight edge,Left edge 是窗口的左邊界,Right edge 是窗口的右邊界。

當 Left edge 向右移動而 Right edge 不變時,這個窗口可能處於 close 關閉狀態。隨着已發送的數據逐漸被確認從而致使窗口變小時,就會發生這種狀況。

當 Right edge 向右移動時,窗口會處於 open 打開狀態,容許發送更多的數據。當接收端進程讀取緩衝區數據,從而使緩衝區接收更多數據時,就會處於這種狀態。

還可能會發生 Right edge 向左移動的狀況,會致使發送並確認的報文段變小,這種狀況被稱爲糊塗窗口綜合症,這種狀況是咱們不肯意看到的。出現糊塗窗口綜合症時,通訊雙方用於交換的數據段大小會變小,而網絡固定的開銷卻沒有變化,每一個報文段中有用數據相對於頭部信息的比例較小,致使傳輸效率很是低。

這就至關於以前你明明有能力花一天時間寫完一個複雜的頁面,如今你花了一天的時間卻改了一個標題的 bug,大材小用。

每一個 TCP 報文段都包含ACK 號和窗口通告信息,因此每當收到響應時,TCP 接收方都會根據這兩個參數調整窗口結構。

TCP 滑動窗口的 Left edge 永遠不可能向左移動,由於發送並確認的報文段永遠不可能被取消,就像這世界上沒有後悔藥同樣。這條邊緣是由另外一段發送的 ACK 號控制的。當 ACK 標號使窗口向右移動可是窗口大小沒有改變時,則稱該窗口向前滑動

若是 ACK 的編號增長可是窗口通告信息隨着其餘 ACK 的到達卻變小了,此時 Left edge 會接近 Right edge。當 Left edge 和 Right edge 重合時,此時發送方不會再傳輸任何數據,這種狀況被稱爲零窗口。此時 TCP 發送方會發起窗口探測,等待合適的時機再發送數據。

接收方窗口

接收方也維護了一個窗口結構,這個窗口要比發送方的簡單不少。這個窗口記錄了已經接收並確認的數據,以及它可以接收的最大序列號。接收方的窗口結構不會存儲重複的報文段和 ACK,同時接收方的窗口也不會記錄不該該收到的報文段和 ACK。下面是 TCP 接收方的窗口結構。

與發送端的窗口同樣,接收方窗口結構也維護了一個 Left edge 和 Right edge。位於 Left edge 左邊的被稱爲已經接收並確認的報文段,位於 Right edge 右邊的被稱爲不能接收的報文段。

對於接收端來講,到達序列號小於 Left efge 的被認爲是已經重複的數據,須要丟棄。超過 Right edge 的被認爲超出處理範圍。只有當到達的報文段等於 Left edge 時,數據纔不會被丟棄,窗口才可以向前滑動。

接收方窗口結構也會存在零窗口的狀況,若是某個應用進程消耗數據很慢,而 TCP 發送方卻發送了大量的數據給接收方,會形成 TCP 緩衝區溢出,通告發送方不要再發送數據了,可是應用進程卻以很是慢的速度消耗緩衝區的數據(好比 1 字節),就會告訴接收端只能發送一個字節的數據,這個過程慢慢持續,形成網絡開銷大,效率很低。

咱們上面提到了窗口存在 Left edge = Right edge 的狀況,此時被稱爲零窗口,下面咱們就來具體研究一下零窗口。

零窗口

TCP 是經過接收端的窗口通告信息來實現流量控制的。通告窗口告訴了 TCP ,接收端可以接收的數據量。當接收方的窗口變爲 0 時,能夠有效的阻止發送端繼續發送數據。當接收端從新得到可用空間時,它會給發送端傳輸一個 窗口更新 告知本身可以接收數據了。窗口更新通常是純 ACK ,即不帶任何數據。可是純 ACK 不能保證必定會到達發送端,因而須要有相關的措施可以處理這種丟包。

若是純 ACK 丟失的話,通訊雙方就會一直處於等待狀態,發送方心想拉垮的接收端怎麼還讓我發送數據!接收端心想天殺的發送方怎麼還不發數據!爲了防止這種狀況,發送方會採用一個持續計時器來間歇性的查詢接收方,看看其窗口是否已經增加。持續計時器會觸發窗口探測,強制要求接收方返回帶有更新窗口的 ACK。

窗口探測包含一個字節的數據,採用的是 TCP 丟失重傳的方式。當 TCP 持續計時器超時後,就會觸發窗口探測的發送。一個字節的數據可否被接收端接收,還要取決於其緩衝區的大小。

擁塞控制

有了 TCP 的窗口控制後,使計算機網絡中兩個主機之間再也不是以單個數據段的形式發送了,而是可以連續發送大量的數據包。然而,大量數據包同時也伴隨着其餘問題,好比網絡負載、網絡擁堵等問題。TCP 爲了防止這類問題的出現,使用了 擁塞控制 機制,擁塞控制機制會在面臨網絡擁塞時遏制發送方的數據發送。

擁塞控制主要有兩種方法

  • 端到端的擁塞控制: 由於網絡層沒有爲運輸層擁塞控制提供顯示支持。因此即便網絡中存在擁塞狀況,端系統也要經過對網絡行爲的觀察來推斷。TCP 就是使用了端到端的擁塞控制方式。IP 層不會向端系統提供有關網絡擁塞的反饋信息。那麼 TCP 如何推斷網絡擁塞呢?若是超時或者三次冗餘確認就被認爲是網絡擁塞,TCP 會減少窗口的大小,或者增長往返時延來避免
  • 網絡輔助的擁塞控制: 在網絡輔助的擁塞控制中,路由器會向發送方提供關於網絡中擁塞狀態的反饋。這種反饋信息就是一個比特信息,它指示鏈路中的擁塞狀況。

下圖描述了這兩種擁塞控制方式

圖片

TCP 擁塞控制

若是你看到這裏,那我就暫定認爲你瞭解了 TCP 實現可靠性的基礎了,那就是使用序號和確認號。除此以外,另一個實現 TCP 可靠性基礎的就是 TCP 的擁塞控制。若是說

TCP 所採用的方法是讓每個發送方根據所感知到的網絡的擁塞程度來限制發出報文段的速率,若是 TCP 發送方感知到沒有什麼擁塞,則 TCP 發送方會增長髮送速率;若是發送方感知沿着路徑有阻塞,那麼發送方就會下降發送速率。

可是這種方法有三個問題

  1. TCP 發送方如何限制它向其餘鏈接發送報文段的速率呢?
  2. 一個 TCP 發送方是如何感知到網絡擁塞的呢?
  3. 當發送方感知到端到端的擁塞時,採用何種算法來改變其發送速率呢?

咱們先來探討一下第一個問題,TCP 發送方如何限制它向其餘鏈接發送報文段的速率呢

咱們知道 TCP 是由接收緩存、發送緩存和變量(LastByteRead, rwnd,等)組成。發送方的 TCP 擁塞控制機制會跟蹤一個變量,即 擁塞窗口(congestion window) 的變量,擁塞窗口表示爲 cwnd,用於限制 TCP 在接收到 ACK 以前能夠發送到網絡的數據量。而接收窗口(rwnd) 是一個用於告訴接收方可以接受的數據量。

通常來講,發送方未確認的數據量不得超過 cwnd 和 rwnd 的最小值,也就是

LastByteSent - LastByteAcked <= min(cwnd,rwnd)

因爲每一個數據包的往返時間是 RTT,咱們假設接收端有足夠的緩存空間用於接收數據,咱們就不用考慮 rwnd 了,只專一於 cwnd,那麼,該發送方的發送速率大概是 cwnd/RTT 字節/秒 。經過調節 cwnd,發送方所以能調整它向鏈接發送數據的速率。

一個 TCP 發送方是如何感知到網絡擁塞的呢

這個咱們上面討論過,是 TCP 根據超時或者 3 個冗餘 ACK 來感知的。

當發送方感知到端到端的擁塞時,採用何種算法來改變其發送速率呢 ?

這個問題比較複雜,且容我娓娓道來,通常來講,TCP 會遵循下面這幾種指導性原則

  • 若是在報文段發送過程當中丟失,那就意味着網絡擁堵,此時須要適當下降 TCP 發送方的速率。
  • 一個確認報文段指示發送方正在向接收方傳遞報文段,所以,當對先前未確認報文段的確認到達時,可以增長髮送方的速率。爲啥呢?由於未確認的報文段到達接收方也就表示着網絡不擁堵,可以順利到達,所以發送方擁塞窗口長度會變大,因此發送速率會變快
  • 帶寬探測,帶寬探測說的是 TCP 能夠經過調節傳輸速率來增長/減少 ACK 到達的次數,若是出現丟包事件,就會減少傳輸速率。所以,爲了探測擁塞開始出現的頻率, TCP 發送方應該增長它的傳輸速率。而後慢慢使傳輸速率下降,進而再次開始探測,看看擁塞開始速率是否發生了變化。

在瞭解完 TCP 擁塞控制後,下面咱們就該聊一下 TCP 的 擁塞控制算法(TCP congestion control algorithm) 了。TCP 擁塞控制算法主要包含三個部分:慢啓動、擁塞避免、快速恢復,下面咱們依次來看一下

慢啓動

當一條 TCP 開始創建鏈接時,cwnd 的值就會初始化爲一個 MSS 的較小值。這就使得初始發送速率大概是 MSS/RTT 字節/秒 ,好比要傳輸 1000 字節的數據,RTT 爲 200 ms ,那麼獲得的初始發送速率大概是 40 kb/s 。實際狀況下可用帶寬要比這個 MSS/RTT 大得多,所以 TCP 想要找到最佳的發送速率,能夠經過 慢啓動(slow-start) 的方式,在慢啓動的方式中,cwnd 的值會初始化爲 1 個 MSS,而且每次傳輸報文確認後就會增長一個 MSS,cwnd 的值會變爲 2 個 MSS,這兩個報文段都傳輸成功後每一個報文段 + 1,會變爲 4 個 MSS,依此類推,每成功一次 cwnd 的值就會翻倍。以下圖所示

圖片

發送速率不可能會一直增加,增加總有結束的時候,那麼什麼時候結束呢?慢啓動一般會使用下面這幾種方式結束髮送速率的增加。

  • 若是在慢啓動的發送過程出現丟包的狀況,那麼 TCP 會將發送方的 cwnd 設置爲 1 並從新開始慢啓動的過程,此時會引入一個 ssthresh(慢啓動閾值) 的概念,它的初始值就是產生丟包的 cwnd 的值 / 2,即當檢測到擁塞時,ssthresh 的值就是窗口值的一半。
  • 第二種方式是直接和 ssthresh 的值相關聯,由於當檢測到擁塞時,ssthresh 的值就是窗口值的一半,那麼當 cwnd > ssthresh 時,每次翻番均可能會出現丟包,因此最好的方式就是 cwnd 的值 = ssthresh ,這樣 TCP 就會轉爲擁塞控制模式,結束慢啓動。
  • 慢啓動結束的最後一種方式就是若是檢測到 3 個冗餘 ACK,TCP 就會執行一種快速重傳並進入恢復狀態。

擁塞避免

當 TCP 進入擁塞控制狀態後,cwnd 的值就等於擁塞時值的一半,也就是 ssthresh 的值。因此,沒法每次報文段到達後都將 cwnd 的值再翻倍。而是採用了一種相對保守的方式,每次傳輸完成後只將 cwnd 的值增長一個 MSS,好比收到了 10 個報文段的確認,可是 cwnd 的值只增長一個 MSS。這是一種線性增加模式,它也會有增加逾值,它的增加逾值和慢啓動同樣,若是出現丟包,那麼 cwnd 的值就是一個 MSS,ssthresh 的值就等於 cwnd 的一半;或者是收到 3 個冗餘的 ACK 響應也能中止 MSS 增加。若是 TCP 將 cwnd 的值減半後,仍然會收到 3 個冗餘 ACK,那麼就會將 ssthresh 的值記錄爲 cwnd 值的一半,進入 快速恢復 狀態。

快速恢復

在快速恢復中,對於使 TCP 進入快速恢復狀態缺失的報文段,對於每一個收到的冗餘 ACK,cwnd 的值都會增長一個 MSS 。當對丟失報文段的一個 ACK 到達時,TCP 在下降 cwnd 後進入擁塞避免狀態。若是在擁塞控制狀態後出現超時,那麼就會遷移到慢啓動狀態,cwnd 的值被設置爲 1 個 MSS,ssthresh 的值設置爲 cwnd 的一半。

我本身肝了六本 PDF,全網傳播超過10w+ ,微信搜索「程序員cxuan」關注公衆號後,在後臺回覆 cxuan ,領取所有 PDF,這些 PDF 以下

六本 PDF 連接

相關文章
相關標籤/搜索