面試不再慌,終於有人把TCP講明白了。。。
前言
TCP(Transmission Control Protocol,傳輸控制協議) 是計算機網路的的重要組成部分,也是網路程式設計的重要內容,還有我們平時接觸最多的 HTTP 也是基於 TCP 實現的。TCP 可以說是最重要的傳輸層協議,既然如此,作為開發人員,就有必要把 TCP 的核心概念和原理搞清楚。除此之外,諸如三次握手、四次揮手、滑動視窗和擁塞控制這些概念更是高頻面試題,這就更有理由深入學習一下 TCP 了,本文就為大家詳細梳理一下 TCP 的核心概念和原理。注:由於本文圖片較多,標註有 by HYN 的圖片為作者自制,其它來自於網路或參考資料,侵刪
一、TCP 簡介
第一部分先為大家介紹一下 TCP 的主要概念,並講解一下 TCP 的三個重要特性——1. 面向連線;2. 基於位元組流;3. 可靠性。
關於網路分層的概念實在是老生常談了,下圖就是兩種經典的分層模型,可以看到 TCP 在網路分層中的位置。
網路分層模型
本文重點對 TCP 進行介紹,從圖中可以看到 TCP 位於傳輸層,而且構建於網路層的 IP 協議之上,對於 TCP 最常見的介紹就是 “TCP 是一種面向連線的、可靠的、基於位元組流的傳輸層通訊協議”,那這三個形容詞究竟是什麼意思呢?
1.1 面向連線
面向連線意味著兩個使用 TCP 的應用 (通常是一個客戶端和一個伺服器) 在彼此交換資料之前必須先建立一個 TCP 連線。這一過程與打電話很相似,先撥號響鈴,等對方應答後後再說明是誰。詳細的三次握手、四次揮手過程將在第二部分——連線管理部分進行介紹。
1.2 基於位元組流
TCP 連線雙方的資料交換格式是以位元組 (byte,1byte = 8 bit)構成的有序但無結構的位元組流。TCP 不在位元組流中插入記錄識別符號,這被稱為位元組流服務(byte stream service)。如果一方的應用程式先傳 10 位元組,又傳 20 位元組,再傳 50 位元組 ,連線的另一方將無法瞭解發方每次傳送了多少位元組。收方可以分 4 次接收這 80 個位元組,每次接收 20 位元組。一端將位元組流放到 TCP 連線上,同樣的位元組流將出現在 TCP 連線的另一端。另外,TCP 對位元組流的內容不作任何解釋,TCP 無法知道傳輸的資料位元組流是二進位制資料,還是 ASCI I 字元。
如果覺得上面這段話比較抽象的話,可以拿 TCP 的位元組流和 UDP 的報文 (message) 進行比較( UDP:User Datagram Protocol,使用者資料報協議,和 TCP 同為傳輸層的協議,後面會提供兩者的全面對比 )。TCP 的位元組流類似於自來水,連線雙方都有緩衝區,可以類比成蓄水池,傳送方的傳送頻率和每次的傳送量沒有固定要求,接收方也可以自由決定自己的接收頻率和每次的接收量,只要把所有的資料接收完畢即可。而 UDP 的資料報則類似於瓶裝水 (比如農夫山泉),傳送方傳送一瓶,接收方就要相應地接收一瓶。
下圖描述了 TCP 連線中資料的傳輸過程以及 TCP 在整個過程中所扮演的角色。
TCP 在網路資料傳輸中的位置和角色
按照圖中的流程,比如我們在瀏覽 B 站,在 TCP 連線建立之後,客戶端的應用層協議可以向 TCP 傳送無特殊格式的位元組流,TCP 會將這些位元組打包成報文段(segment),報文段大小視情況而定,這些報文段會被網路層的 IP 封裝成 IP 資料報(IP Datagram),然後經過網路傳輸給伺服器,而接下來伺服器的操作相當於客戶端的逆操作,先從 IP 資料報中拆分出 TCP 報文段,再把 TCP 報文段還原成位元組流併發送給上層的應用層協議。伺服器向客戶端傳送資料的流程也是一樣的,傳送方和接收方的角色互換即可。
報文段簡介
上面多次提到了報文段的概念,其結構非常重要,後面的連線過程和擁塞控制等內容也要用到相關概念,先在這裡介紹一下。
TCP 報文段結構
圖的上半部分顯示 TCP 報文段被封裝在 IP 資料報中,圖的下半部分則顯示了 TCP 報文段和 TCP 首部的結構,TCP 首部的固定資料有 20 位元組,加上選項部分最大可達 60 位元組,而有效資料部分則是被打包的應用層資料。下面介紹一下 TCP 首部的結構:
-
埠號 (Source Port and Destination Port):每個 TCP 報文段都包含源端和目的端的埠號,用於尋找傳送端和接收端應用程序。這兩個值加上 IP 首部中的源端 IP 地址和目的端 IP 地址就可以確定一個唯一的 TCP 連線。
-
序號 (Sequence Number):這個欄位的主要作用是用於將失序的資料重新排列。TCP 會隱式地對位元組流中的每個位元組進行編號,而 TCP 報文段的序號被設定為其資料部分的第一個位元組的編號。序號是 32 bit 的無符號數,取值範圍是 0 到 2 32 - 1。
-
確認序號 (Acknowledgment Number):接收方在接受到資料後,會回覆確認報文,其中包含確認序號,作用就是告訴傳送方自己接收到了哪些資料,下一次資料從哪裡開始發,因此,確認序號應當是上次已成功收到資料位元組序號加 1。只有 ACK 標誌為 1 時確認序號欄位才有效。
-
首部長度 (Header Length):首部中的選項部分的長度是可變的,因此首部的長度也是可變的,所以需要這個欄位來明確表示首部的長度,這個欄位佔 4 bit,4 位的二進位制數最大可以表示 15,而首部長度是以 4 個位元組為一個單位的,因此首部最大長度是 15 * 4 = 60 位元組。
-
保留欄位 (Reserved):佔 6 位,未來可能有具體用途,目前預設值為 0.
-
控制位 (Control Bits):在三次握手和四次揮手中會經常看到 SYN、ACK 和 FIN 的身影,一共有 6 個標誌位,它們表示的意義如下:URG (Urgent Bit):值為 1 時,緊急指標生效 ACK (Acknowledgment Bit):值為 1 時,確認序號生效 PSH (Push Bit):接收方應儘快將這個報文段交給應用層 RST (Reset Bit):傳送端遇到問題,想要重建連線 SYN (Synchronize Bit):同步序號,用於發起一個連線 FIN (Finish Bit):傳送端要求關閉連線
-
視窗大小 (Window): TCP 的流量控制由連線的每一端通過宣告的視窗大小來提供。視窗大小為位元組數,起始於確認序號欄位指明的值,這個值是接收端正期望接收的位元組。視窗大小是一個 16 bit 欄位,單位是位元組, 因而視窗大小最大為 65535 位元組。
-
檢驗和 (Checksum):功能類似於數字簽名,用於驗證資料完整性,也就是確保資料未被修改。檢驗和覆蓋了整個 TCP 報文段,包括 TCP 首部和 TCP 資料,傳送端根據特定演算法對整個報文段計算出一個檢驗和,接收端會進行計算並驗證。
-
緊急指標 (Urgent Pointer):當 URG 控制位值為 1 時,此欄位生效,緊急指標是一個正的偏移量,和序號欄位中的值相加表示緊急資料最後一個位元組的序號。 TCP 的緊急方式是傳送端向另一端傳送緊急資料的一種方式。
-
選項 (Options):這一部分是可選欄位,也就是非必須欄位,最常見的可選欄位是“最長報文大小 (MSS,Maximum Segment Size)”。
-
有效資料部分 (Data):這部分也不是必須的,比如在建立和關閉 TCP 連線的階段,雙方交換的報文段就只包含 TCP 首部。
1.3 可靠性
我們都知道 TCP 是具有可靠性的通訊協議,它主要通過以下方式確保可靠性,這裡先了解一下可靠性的原理,其中細節部分後文會講:
-
合理的資料大小:TCP 傳送的資料並不是固定的大小,而是會根據實際情況調整報文段的大小。
-
檢驗和:傳送端按照特定演算法計算出 TCP 報文段的檢驗和並存儲在 TCP 首部中的對應欄位上,接收端在接收時會以同樣的方式計算校驗和,如果不一致,說明報文段出現錯誤,會將其丟棄。
-
序號與確認序號:對亂序的資料進行排序後發給應用層,並丟棄重複的資料。
-
超時重傳機制:當 TCP 發出一個報文段後,它會啟動一個定時器,等待目的端確認收到這個報文段。如果不能及時收到一個確認,將重發這個報文段,後面會細講這個機制。
-
連線管理:也就是三次握手和四次揮手,連線的可靠性是整體可靠性的前提,本文第二部分將會詳細介紹連線管理的內容。
-
流量控制:TCP 雙方都有固定大小的緩衝區,流量控制的原理是利用滑動視窗控制資料傳送速度,避免緩衝區溢位導致資料丟失。
-
擁塞控制:TCP 利用慢啟動和擁塞避免等演算法實現了擁塞控制。
上面為大家介紹了 TCP 最重要的三個特點,在本文第一部分的最後,再來看看 TCP 和 UDP 的對比吧。
1.4 TCP 和 UDP 的區別
二、TCP 的連線控制
2.1 建立連線
2.1.1 三次握手
這個問題簡直太經典了,如果你在面試中只被問到了一個關於 TCP 的問題,那大概率就是關於三次握手的問題。TCP 的重要特性之一就是面向連線,連線雙方在傳送資料之前必須經歷握手的階段,那具體的過程是怎樣的呢?先來看圖,大家最好可以動手簡單畫畫這個圖,當然還有後文四次揮手的圖,幫助加深記憶。
三次握手過程
如圖所示,雙方之間的三個藍色箭頭就表示了三次握手過程中所發生的資料交換:
-
第一次握手:客戶端向伺服器傳送報文段 1,其中的 SYN 標誌位 (前文已經介紹過各種標誌位的作用)的值為 1,表示這是一個用於請求發起連線的報文段,其中的序號欄位 (Sequence Number,圖中簡寫為 seq)被設定為初始序號 x (Initial Sequence Number,ISN),TCP 連線雙方均可隨機選擇初始序號。傳送完報文段 1 之後,客戶端進入 SYN-SENT 狀態,等待伺服器的確認。
-
第二次握手:伺服器在收到客戶端的連線請求後,向客戶端傳送報文段 2 作為應答,其中 ACK 標誌位設定為 1,表示對客戶端做出應答,其確認序號欄位 (Acknowledgment Number,圖中簡寫為小寫 ack) 生效,該欄位值為 x + 1,也就是從客戶端收到的報文段的序號加一,代表伺服器期望下次收到客戶端的資料的序號。此外,報文段 2 的 SYN 標誌位也設定為 1,代表這同時也是一個用於發起連線的報文段,序號 seq 設定為伺服器初始序號 y。傳送完報文段 2 後,伺服器進入 SYN-RECEIVED 狀態。
-
第三次握手:客戶端在收到報文段 2 後,向伺服器傳送報文段 3,其 ACK 標誌位為 1,代表對伺服器做出應答,確認序號欄位 ack 為 y + 1,序號欄位 seq 為 x + 1。此報文段傳送完畢後,雙方都進入 ESTABLISHED 狀態,表示連線已建立。
常見面試題 1:TCP 建立連線為什麼要三次握手而不是兩次?
答:網上大多數資料對這個問題的回答只有簡單的一句:防止已過期的連線請求報文突然又傳送到伺服器,因而產生錯誤,這既不夠全面也不夠具體。下面給出比較詳細而全面的回答:
-
防止已過期的連線請求報文突然又傳送到伺服器,因而產生錯誤在雙方兩次握手即可建立連線的情況下,假設客戶端傳送 A 報文段請求建立連線,由於網路原因造成 A 暫時無法到達伺服器,伺服器接收不到請求報文段就不會返回確認報文段,客戶端在長時間得不到應答的情況下重新發送請求報文段 B,這次 B 順利到達伺服器,伺服器隨即返回確認報文並進入 ESTABLISHED 狀態,客戶端在收到 確認報文後已進入 ESTABLISHED 狀態,雙方建立連線並傳輸資料,之後正常斷開連線。此時姍姍來遲的 A 報文段才到達伺服器,伺服器隨即返回確認報文並進入 ESTABLISHED 狀態,但是已經進入 CLOSED 狀態的客戶端無法再接受確認報文段,更無法進入 ESTABLISHED 狀態,這將導致伺服器長時間單方面等待,造成資源浪費。
-
三次握手才能讓雙方均確認自己和對方的傳送和接收能力都正常第一次握手:客戶端只是傳送處請求報文段,什麼都無法確認,而伺服器可以確認自己的接收能力和對方的傳送能力正常;第二次握手:客戶端可以確認自己傳送能力和接收能力正常,對方傳送能力和接收能力正常;第三次握手:伺服器可以確認自己傳送能力和接收能力正常,對方傳送能力和接收能力正常;可見三次握手才能讓雙方都確認自己和對方的傳送和接收能力全部正常,這樣就可以愉快地進行通訊了。
-
告知對方自己的初始序號值,並確認收到對方的初始序號值 TCP 實現了可靠的資料傳輸,原因之一就是 TCP 報文段中維護了序號欄位和確認序號欄位,也就是圖中的 seq 和 ack,通過這兩個欄位雙方都可以知道在自己發出的資料中,哪些是已經被對方確認接收的。這兩個欄位的值會在初始序號值得基礎遞增,如果是兩次握手,只有發起方的初始序號可以得到確認,而另一方的初始序號則得不到確認。
常見面試題 2:TCP 建立連線為什麼要三次握手而不是四次?
答:相比上個問題而言,這個問題就簡單多了。因為三次握手已經可以確認雙方的傳送接收能力正常,雙方都知道彼此已經準備好,而且也可以完成對雙方初始序號值得確認,也就無需再第四次握手了。
常見面試題 3:有一種網路攻擊是利用了 TCP 建立連線機制的漏洞,你瞭解嗎?這個問題怎麼解決?
答:在三次握手過程中,伺服器在收到了客戶端的 SYN 報文段後,會分配並初始化連線變數和快取,並向客戶端傳送 SYN + ACK 報文段,這相當於是打開了一個“半開連線 (half-open connection)”,會消耗伺服器資源。如果客戶端正常返回了 ACK 報文段,那麼雙方可以正常建立連線,否則,伺服器在等待一分鐘後會終止這個“半開連線”並回收資源。這樣的機制為 SYN 洪泛攻擊 (SYN flood attack)提供了機會,這是一種經典的 DoS 攻擊 (Denial of Service,拒絕服務攻擊),所謂的拒絕服務攻擊就是通過進行攻擊,使受害主機或網路不能提供良好的服務,從而間接達到攻擊的目的。在 SYN 洪泛攻擊中,攻擊者傳送大量的 SYN 報文段到伺服器請求建立連線,但是卻不進行第三次握手,這會導致伺服器開啟大量的半開連線,消耗大量的資源,最終無法進行正常的服務。
解決方法:SYN Cookies,現在大多數主流作業系統都有這種防禦系統。SYN Cookies 是對 TCP 伺服器端的三次握手做一些修改,專門用來防範 SYN 洪泛攻擊的一種手段。它的原理是,在伺服器接收到 SYN 報文段並返回 SYN + ACK 報文段時,不再開啟一個半開連線,也不分配資源,而是根據這個 SYN 報文段的重要資訊 (包括源和目的 IP 地址,埠號可一個祕密數),利用特定雜湊函式計算出一個 cookie 值。這個 cookie 作為將要返回的 SYN + ACK 報文段的初始序列號(ISN)。當客戶端返回一個 ACK 報文段時,伺服器根據首部欄位資訊計算 cookie,與返回的確認序號(初始序列號 + 1)進行對比,如果相同,則是一個正常連線,然後分配資源並建立連線,否則拒絕建立連線。
2.2.2 同時開啟
這是 TCP 建立連線的特殊情況,有時會出現兩臺機器同時執行主動開啟的情況,不過概率非常小,這種情況大家僅作了解即可。在這種情況下就無所謂傳送方和接收方了,雙放都可以稱為客戶端和伺服器,同時開啟的過程如下:
同時開啟的過程
如圖所示,雙方在同一時刻傳送 SYN 報文段,並進入 SYN-SENT 狀態,在收到 SYN 後,狀態變為 SYN-RECEIVED,同時它們都在傳送一個 SYN + ACK 的報文段,狀態都變為 ESTABLISHED,連線成功建立。在此過程中雙方一共交換了 4 個報文段,比三次握手多一個。
2.2 關閉連線
2.2.1 四次揮手
建立一個連線需要三次握手,而終止一個連線要經過 4 次握手。這由 TCP 的半關閉( half-close) 造成的。既然一個 TCP 連線是全雙工 (即資料在兩個方向上能同時傳遞), 因此每個方向必須單獨地進行關閉。這原則就是當一方完成它的資料傳送任務後就能傳送一個 FIN 來終止這個方向連線。當一端收到一個 FIN,它必須通知應用層另一端已經終止了資料傳送。理論上客戶端和伺服器都可以發起主動關閉,但是更多的情況下是客戶端主動發起。
四次揮手過程
四次揮手詳細過程如下:
-
客戶端傳送關閉連線的報文段,FIN 標誌位 1,請求關閉連線,並停止傳送資料。序號欄位 seq = x (等於之前傳送的所有資料的最後一個位元組的序號加一),然後客戶端會進入 FIN-WAIT-1 狀態,等待來自伺服器的確認報文。
-
伺服器收到 FIN 報文後,發回確認報文,ACK = 1, ack = x + 1,並帶上自己的序號 seq = y,然後伺服器就進入 CLOSE-WAIT 狀態。伺服器還會通知上層的應用程式對方已經釋放連線,此時 TCP 處於半關閉狀態,也就是說客戶端已經沒有資料要傳送了,但是伺服器還可以傳送資料,客戶端也還能夠接收。
-
客戶端收到伺服器的 ACK 報文段後隨即進入 FIN-WAIT-2 狀態,此時還能收到來自伺服器的資料,直到收到 FIN 報文段。
-
伺服器傳送完所有資料後,會向客戶端傳送 FIN 報文段,各欄位值如圖所示,隨後伺服器進入 LAST-ACK 狀態,等待來自客戶端的確認報文段。
-
客戶端收到來自伺服器的 FIN 報文段後,向伺服器傳送 ACK 報文,隨後進入 TIME-WAIT 狀態,等待 2MSL(2 * Maximum Segment Lifetime,兩倍的報文段最大存活時間) ,這是任何報文段在被丟棄前能在網路中存在的最長時間,常用值有 30 秒、1 分鐘和 2 分鐘。如無特殊情況,客戶端會進入 CLOSED 狀態。
-
伺服器在接收到客戶端的 ACK 報文後會隨即進入 CLOSED 狀態,由於沒有等待時間,一般而言,伺服器比客戶端更早進入 CLOSED 狀態。
常見面試題 1:為什麼 TCP 關閉連線為什麼要四次而不是三次?
答:伺服器在收到客戶端的 FIN 報文段後,可能還有一些資料要傳輸,所以不能馬上關閉連線,但是會做出應答,返回 ACK 報文段,接下來可能會繼續傳送資料,在資料傳送完後,伺服器會向客戶單傳送 FIN 報文,表示資料已經發送完畢,請求關閉連線,然後客戶端再做出應答,因此一共需要四次揮手。
常見面試題 2:客戶端為什麼需要在 TIME-WAIT 狀態等待 2MSL 時間才能進入 CLOSED 狀態?
答:按照常理,在網路正常的情況下,四個報文段傳送完後,雙方就可以關閉連線進入 CLOSED 狀態了,但是網路並不總是可靠的,如果客戶端傳送的 ACK 報文段丟失,伺服器在接收不到 ACK 的情況下會一直重複 FIN 報文段,這顯然不是我們想要的。因此客戶端為了確保伺服器收到了 ACK,會設定一個定時器,並在 TIME-WAIT 狀態等待 2MSL 的時間,如果在此期間又收到了來自伺服器的 FIN 報文段,那麼客戶端會重新設定計時器並再次等待 2MSL 的時間,如果在這段時間內沒有收到來自伺服器的 FIN 報文,那就說明伺服器已經成功收到了 ACK 報文,此時客戶端就可以進入 CLOSED 狀態了。
2.2.2 同時關閉
之前在介紹 TCP 建立連線的時候會有一種特殊情況,那就是同時開啟,與之對應地, TCP 關閉時也會有一種特殊情況,那就是同時關閉,這種情況僅作了解即可,流程圖如下:
同時關閉過程
這種情況下,雙方應用層同時發出關閉命令,這將導致雙方各發送一個 FIN,兩端均從 ESTABLISHED 變為 FIN_WAIT_1,兩個 FIN 經過網路傳送後分別到達另一端。收到 FIN 後,狀態由 FIN_WAIT_1 變遷到 CLOSING,併發送最後的 ACK,當收到最後的 ACK 時,為確保對方也收到 ACK,狀態變化為 TIME_WAIT,並等待 2MSL 時間,如果一切正常,隨後會進入 CLOSED 狀態。
三、TCP 的流量控制與滑動視窗
3.1 什麼是流量控制?
TCP 連線雙方的主機都為該連線設定了傳送快取和接收快取,這些快取起到了蓄水池的作用,我們肯定不能把上層應用程式發來的資料一股腦兒傳送到網路中,而是利用傳送快取將其快取起來,然後再按一定的速率通過網路傳送給對方,而接收快取的作用是把對方傳來的資料先快取起來,等到己方應用程式有空的時候再來取走資料。示意圖如下:
TCP 快取示意圖
在此過程中,如果接收方應用程式讀取資料的速度小於傳送方的資料傳送速度,將導致接收方的接收快取溢位,造成資料丟失,這顯然不是我們想看到的。因此 TCP 為應用程式提供了流量控制服務 (flow-control service),以消除傳送方使接收方的接收快取溢位的可能性。簡單來說流量控制的目的就是協調發送方的資料傳送速度,使其與接收方的資料處理速度相匹配,避免資料丟失,那麼如何實現流量控制呢?
3.2 早期的流量控制模式——停止-等待模式 (stop-wait)
PAR 示意圖
這種實現很簡單,傳送方在傳送資料包 (圖中的 msg)時會設定一個計時器,然後等待接收方的 ACK,接收方在收到資料後會返回 ACK 作為應答,傳送方在收到 ACK 後會傳送下一個資料包。如果由於網路原因造成資料包或者 ACK 丟失時,計時器會超時,然後傳送方會重新發送未被確認的資料包。可以看到,這種模式雖然可以確保資料傳輸的可靠性,但是有個致命的缺點,那就是效率太低?如果是你,你會怎麼對這個方案進行優化呢?
既然每次傳送只一個數據包效率太低,那就多傳送幾個,然後給這些資料包編上號,接收端必須對每一個包進行確認,這樣裝置 A 一次多傳送幾個片段,而不必等候 ACK,同時接收端也要告知它能夠收多少,這樣傳送端發起來也有個限制,當然還需要保證順序性,不要亂序,對於亂序的狀況,我們可以允許等待一定情況下的亂序,比如說先快取提前到的資料,然後去等待需要的資料,如果一定時間沒來就丟掉亂序的資料,來保證順序性,這樣的話,資料傳輸效率就可以大大提高。不過 TCP 也沒有采用這種方案,而是在此基礎上實現更加複雜的滑動視窗。
3.3 滑動視窗
我們可以把傳送方的傳送快取中的位元組分為以下四類,每個編號對應一個位元組:
傳送快取中的位元組分類
-
第一類:已傳送且已確認,這些資料已經發送成功並已經被確認的資料,比如圖中的前 31 個 bytes,這些資料其實的位置是在視窗之外了,下一步將被移出傳送快取。視窗內順序最低的位元組被確認之後,視窗左邊界會向右移動,稱為視窗合攏。
-
第二類:已傳送但未收到確認,這部分資料已經被髮送出去,但是還沒有收到接收端的 ACK,認為並沒有完成傳送,這部分資料屬於視窗內的資料。
-
第三類:未傳送但是接收方已經準備好接收,這部分是儘快傳送的資料,這部分資料已經被載入到快取中,也在傳送視窗中,正在等待發送,其實這個視窗是完全有接收方告知的,接收方告知當前可以接受這些資料,所以傳送方需要儘快的傳送。
-
第四類:未傳送且接收方未準備好接收,這些資料屬於未傳送,同時接收端也不允許傳送的,因為這些資料已經超出了傳送端所接收的範圍。
3.3.1 傳送視窗和接收視窗
傳送視窗
傳送視窗:圖中的黑色框就是傳送方的傳送視窗,其大小由兩個因素決定:1、接收方的提供的視窗大小 (TCP 報文段首部中的 window 欄位),傳送方在三次握手階段首次得到這個值,之後的通訊過程中接收方會根據自己的可用快取對這個值進行動態調整;2、傳送方會根據網路情況維護一個擁塞視窗變數 (後文介紹)。傳送視窗的大小取這兩個值的最小值。對於傳送方來說,傳送視窗分為兩部分,分別是已經發送的部分(已經發送了,但是沒有收到 ACK)和可用視窗,接收端允許傳送但是沒有傳送的那部分稱為可用視窗。
接收視窗:對於接收端也是有一個接收視窗的,類似傳送端,接收端的資料有 3 個分類,因為接收端並不需要等待 ACK 所以它沒有類似的接收並確認了的分類,情況如下
-
Received and ACK Not Send to Process:這部分資料屬於接收了資料但是還沒有被上層的應用程式接收;
-
Received Not ACK: 已經接收,但是還沒有回覆 ACK;
-
Not Received:有空位,還沒有被接收的資料。
3.3.2 滑動視窗是如何滑動的?
滑動視窗的滑動過程
累積確認概念:TCP 並不是每一個報文段都會回覆一個 ACK ,可能會對兩個報文段傳送一個 ACK,也可能會對多個報文段傳送 1 個 ACK,這稱為累積確認。比如說傳送方有 1/2/3 3 個報文段,先發送了 2,3 兩個報文段,但是接收方期望收到 1 報文段,這個時候 2/3 報文段就只能放在快取中等待報文 1 的空洞被填上,如果報文段 1 一直不來,報文 2/3 也將被丟棄,如果報文 1 來了,那麼會發送一個 ACK 對第 3 個報文段進行確認,就代表對這三個報文段全部進行了確認。
下面舉例說明一下視窗滑動的過程:
-
在握手過程中,接收方通告的視窗大小為 20 位元組,所以傳送方將傳送視窗大小設定為 20 位元組。
-
從圖中的"上一個傳送視窗的位置"(灰色虛線框)說起, 32-51 號位元組恰好處於傳送視窗中,恰好 20 個位元組,假設 TCP 將其分為 4 個報文段進行傳送,每個報文段 5 個位元組資料,分別記為 seg1 32-36, seg2 37-41, seg3 42-46, seg4 47-51。
-
TCP 將有序傳送 seg1、seg2、seg3 和 seg4 四個報文段,如果這四個報文段都順利到達接收方 (圖中並不是這樣),接收方將發回一個累積確認的 ACK 報文段,其中 ack = 52,代表希望收到下一個報文段的起始位元組編號,報文段中也會繼續通告視窗大小,如果還是 20 位元組,那麼傳送方的視窗將整體向右移動 20 位元組,如果通告的視窗值變小,比如變成 15,那麼傳送視窗左邊界移動 20 位元組,右邊界移動 15 位元組。
-
如果在傳送過程中 seg2 報文段丟失,而其他三個報文段正常到達接收方,那麼接收方會現接受這三個報文段,然後返回 ACK 報文段,ack = 37,表示希望收到的下一個報文段的起始位元組號為 37,也就是 seg2 報文段。如果通告視窗值未發生變化,傳送方在收到 ACK 後會將視窗整體右移 5 個位元組,也就變成了圖中的位置。
-
由於 seg2 還未收到 ACK,當重傳計時器超時後,傳送方會重新發送 seg2,此時 52-56 號位元組又落到了傳送視窗中,TCP 將其封裝成 報文段進行傳送,如果接收方全部順利收到,會返回一個累積確認的 ACK,ack = 57,表示希望收到的喜愛個報文段的起始位元組號為 57。
接下來就是重複上述過程,直到 TCP 位元組流的所有資料傳送完畢。在這個過程中,接收方會根據自己接收快取的剩餘空間動態調整視窗值,對傳送方進行流量控制。文字描述可能不夠直觀,大家可以參考上文推薦的視訊。另外推薦一個動圖演示的網站 動畫地址 ,可以觀看滑動視窗的動態效果,如下圖 (此演示未考慮丟包的情況):
滑動視窗動畫效果
四、TCP 的擁塞控制
4.1 什麼是擁塞控制?
當資料從一個大的管道 (比如一個快速區域網)向一個較小的管道 (比如較慢的廣域網)傳送的時候就會發生擁塞,還有一種情況就是當多個輸入流到達一個路由器,而路由器的輸出流小於這些輸入流的總和時,也會發生擁塞。舉個例子就好理解了,第一種情況就好像源源不斷的車流從八車道進入四車道,如果不進行控制,必然造成道路擁堵;第二種情況類似於很多車輛匯入十字路口,如果進的速度大於出的速度,再不加以控制,必然也會造成擁堵。於是 TCP 提供了相應的機制來應對這種情況,也就是 TCP 的擁塞控制。
4.2 如何實現擁塞控制?
TCP 一共使用了四種演算法來實現擁塞控制:1、慢開始 (slow-start);2、擁塞避免 (congestion avoidance);3、快速重傳 (fast retransmit);4、快速恢復 (fast recovery)。
這裡先介紹一下擁塞視窗 (congestion window,簡寫為 cwnd)的概念:擁塞視窗是由傳送方根據網路狀況維護的一個變數,用於控制自己的資料傳送速率。前文提到了傳送方的傳送視窗受兩個變數約束,一是接收方通告的視窗大小值,二就是傳送方自身的擁塞視窗,實際的傳送視窗大小取二者最小值。
慢開始和擁塞避免
4.2.1 慢開始(慢啟動)
如圖所示,在剛開始,TCP 採用慢開始演算法。慢開始不是指擁塞視窗的增長速度慢(增長速度是指數增長,非常快),而是指 TCP 開始傳送設定 cwnd=1。思路就是不要一開始就傳送大量的資料,先探測一下網路的擁塞程度,也就是說由小到大 逐漸增加擁塞視窗的大小。這裡用報文段的個數的擁塞視窗大小舉例說明慢啟動演算法,實時擁塞視窗大小是以位元組為單位的。為了防止 cwnd 增長過大引起網路擁塞,設定一個慢開始門限(slow start threshold,簡寫為 ssthresh) ,
當 cnwd < ssthresh,使用慢開始演算法
當 cnwd = ssthresh,既可使用慢開始演算法,也可以使用擁塞避免演算法
當 cnwd > ssthresh,使用擁塞避免演算法
4.2.2 擁塞避免
當擁塞視窗大小達到初始 ssthresh 值時,轉而採用擁塞避免演算法。擁塞避免並非完全能夠避免擁塞,是說在擁塞避免階段將擁塞視窗控制為按線性規律增長,使網路比較不容易出現擁塞,思路:讓擁塞視窗 cwnd 緩慢地增大,即每經過一個往返時間 RTT 就把傳送方的擁塞視窗加一。無論是在慢開始階段還是在擁塞避免階段,只要傳送方判斷網路出現擁塞(其根據就是沒有收到確認,雖然沒有收到確認可能是其他原因的分組丟失,但是因為無法判定,所以都當做擁塞來處理),就把慢開始門限設定為出現擁塞時的傳送視窗大小的一半。然後把擁塞視窗設定為 1,執行慢開始演算法。
4.2.3 快速重傳
TCP Reno 應用了四種演算法
有時候的傳送方未收到某個報文段的確認也並一定就說明一定是出現了網路擁塞,也可能是其他原因,所以直接執行慢開始演算法會影響整體效率,後來的 TCP Reno 版本解決了這一問題,那就是採用快速重傳和快速恢復演算法。
快速重傳要求接收方在收到一個失序的報文段後就立即發出重複確認(為的是使傳送方及早知道有報文段沒有到達對方),而不要等到自己傳送資料時捎帶確認。快重傳演算法規定,傳送方只要一連收到三個重複確認就應當立即重傳對方尚未收到的報文段,而不必繼續等待設定的重傳計時器時間到期。由於不需要等待設定的重傳計時器到期,能儘早重傳未被確認的報文段,能提高整個網路的吞吐量。
4.2.4 快速恢復
當傳送方連續收到三個重複確認時,就執行“乘法減小”演算法,把 ssthresh 門限減半。 但是接下去並不執行慢開始演算法。考慮到如果網路出現擁塞的話就不會收到好幾個重複的確認,所以傳送方現在認為網路可能沒有出現擁塞。所以此時不執行慢開始演算法,而是將 cwnd 設定為 ssthresh 的大小, 然後執行擁塞避免演算法。
五、TCP 粘包與拆包
5.1 TCP 粘包和拆包的原因
我們知道 TCP 是以位元組流的方式傳輸資料,傳輸的最小單位為一個報文段(segment)。TCP 首部 中有個選項 (Options)的欄位,常見的選項為 MSS (Maximum Segment Size 最大訊息長度),它是收發雙方協商通訊時每一個報文段所能承載的最大有效資料的長度。資料鏈路層每次傳輸的資料有個最大限制 MTU (Maximum Transmission Unit),一般是 1500 位元,超過這個量要分成多個報文段,MSS 則是這個最大限制減去 TCP 的首部,光是要傳輸的資料的大小,一般為 1460 位元。換算成位元組,也就是 180 多位元組。
MSS = MTU - Header
TCP 為提高效能,傳送端會將需要傳送的資料傳送到傳送快取,等待快取滿了之後,再將快取中的資料傳送到接收方。同理,接收方也有接收快取這樣的機制,來接收資料。
上面這些是發生 TCP 粘包和拆包的前提,下面是具體的原因:
-
要傳送的資料大於 TCP 傳送緩衝區剩餘空間大小,將會發生拆包。
-
待發送資料大於 MSS(最大報文長度),TCP 在傳輸前將進行拆包。
-
應用程式寫入資料小於剩餘快取大小,網絡卡將應用多次寫入的資料先快取起來,然後一起傳送到網路上,這將會發生粘包。
-
接收資料端的應用層沒有及時讀取接收快取中的資料,將發生粘包。
5.2 TCP 粘包和拆包的解決方案
-
設定定長訊息,服務端每次讀取既定長度的內容作為一條完整訊息。
-
設定訊息邊界,資料結尾尾增加特殊字元分割。
-
使用帶訊息頭的協議,訊息頭儲存訊息開始標識及訊息長度資訊,接收方獲取訊息頭的時候解析出訊息長度,然後向後讀取該長度的內容。