TCP和UDP相關知識
TCP
和UDP
都是傳輸層的協議,但二者具有不同的特性,也適用於不同的場景。如下表所示:
TCP |
UDP |
|
---|---|---|
可靠性 | 可靠 | 不可靠 |
連線性 | 面向連線 | 無連線 |
資料經過傳輸之後可能是無序的,TCP 協議會將這些無序的資料重新進行組裝成有序的,供給上層呼叫者使用 |
無序 | |
報文 | 面向位元組流 | 面向報文 |
效率 | 傳輸效率低 | 傳輸效率高 |
雙工性 | 全雙工 | 一對一、一對多、多對一、多對多 |
流量控制 | 滑動視窗 | 無 |
擁塞控制 | 慢開始、擁塞避免、快重傳、快恢復 | 無 |
無,若是接收端快取區足夠大,可將傳送端多次傳送的資料一次性接收,然後傳給上層應用 | 有,傳送端傳送一次,接收端就要接收一次,傳送多少次就要接收多少次 | |
傳輸速度 | 慢 | 快 |
重量級,資料報報頭大小為20個位元組 | 輕量級,資料報報頭大小為8個位元組 | |
應用場景 | 對效率要求低、對可靠性要求高或者要求有連線的場景,如檔案傳輸,郵件傳送等 | 對效率要求高、對準確性要求低,如即時通訊,QQ,視訊通話等 |
基於連線 vs 無連線
TCP
是面向連線的協議,UDP
是無連線的協議。因而,當使用TCP
協議傳輸資料時,客戶端與服務端之間必須通過三次握手來建立連線。
可靠性
TCP
提供交付保證,有許多機制用於保證訊息的可靠性。
- 校驗和:每個報文的報頭部分都有一個校驗和,防止在傳輸途中資料被損壞。如果收到一個校驗和有差錯的報文,
TCP
直接丟棄,不會進行確認; - 序列號:每個報文的報頭部分都有一個序列號,藉助於這個序列號,可以將不同報文中的資料重新按照順序進行組裝,從而保證訊息的有序性;
- 超時重傳:傳送完訊息之後,
TCP
會啟動一個定時器,等待對端確認這個資料包。如果在指定時間內沒有確認,則會進行重傳,再等待一段時間,往復幾次,直到重傳次數超過一定次數之後,就會丟棄這個包; - 流量控制和擁塞控制:下面會詳細介紹到。
有序性
TCP
協議會根據報頭的序列號將傳輸過來的無序資料整理成有序的,而UDP
不會。
效率
TCP
比較慢,而UDP
比較快。因為TCP
必須要先建立連線,以保證訊息的可靠性和有序性,需要進行的內部操作比UDP
多很多。TCP
適合大量資料的傳輸,UDP
量級
TCP
是重量級協議,而UDP
是輕量級協議。一個TCP
資料報的報頭至少為20個位元組,UDP
資料報報頭固定是8個位元組。如下所示:
UDP
報頭
報文
TCP
是面向位元組流的協議,無邊界記錄。而UDP
傳送的每個資料是記錄型的資料報,所謂記錄型資料報就是接收程序可以識別到接收到的資料報的記錄邊界。
TCP提供了一種位元組流服務,而收發雙方都不保持記錄的邊界,應用程式應該如何提供他們自己的記錄標識呢?
因為傳送視窗(接收主機能夠接收的資料量),擁塞視窗(對網路擁塞的估計),路徑上的最大傳輸單元(傳輸的最大資料量),慢啟動。所以不能確定TCP的分包個數與大小。
1,定長報文,讀取報文中的固定位元組
2,可以用結束標記(回車、換行)來分隔記錄
TCP
報頭分析
TCP
(Transmission Control Protocol
傳輸控制協議)是一種面向連線的、可靠的、基於位元組流的傳輸層通訊協議。
TCP
報文段的報頭有 10 個必需的欄位和 1 個可選欄位。報頭至少為 20 位元組。報頭後面的資料是可選項。
源埠和目的埠
各佔2個位元組位元組,分別是源埠號和目的埠號
序列號
4個位元組,使用mod
計算,TCP
協議是面向位元組流的,在TCP
連線中傳輸的位元組流的每一個位元組都是按照順序編號的。
確認號
4個位元組,表示期望收到對方下一個報文段的第一個資料位元組的序號。若確認號為N
,則表示到序號N-1
為止的所有資料都已經確認收到。
資料偏移
4位,TCP 報文段的資料起始處距離 TCP 報文段的起始處有多遠,即首部長度。 由於 TCP
報頭的長度隨 TCP
選項欄位內容的不同而變化,因此報頭中包含一個指定報頭欄位的欄位。TCP
報頭最小20個位元組,最大60個位元組。
保留
6位,目前還未使用,待以後使用,目前都是0。
控制位
6位
URG
當URG=1
時表示緊急指標欄位有效。這個時候傳送方TCP
就會把緊急資料插入到本報文段資料的最前面,而在緊急資料後面的資料仍然是普通資料。
ACK
當ACK=1
時,確認欄位有效。當ACK=0
,確認欄位無效。**TCP
規定,在連線建立後所有傳送的報文段都必須置為1。 **
PUSH
接收方TCP
收到PUSH=1
的報文段時,就會盡快交付給上層應用程式,而不是等到整個快取區都填滿了之後再交付。
RST
當RST=1
時,表明TCP
連接出現嚴重差錯,必須釋放連線,然後再重新建立連線。(先釋放連線,再重新建立連線)。
SYN
在建立連線時用來同步序號。當SYN=1
,ACK=0
時,表明這是一個用來建立連線的請求報文。對方若是同意建立連線,則會迴應一個SYN=1
,ACK=1
的報文。故SYN=1
,表明這是一個連線請求或者是連線接收報文。
FIN
用於釋放連線。當FIN=1
時,表明傳送方的資料已傳送完畢,並要求釋放連線。
視窗
2個位元組,此欄位用來進行流量控制,這個值是本機期望一次接收的位元組數,告訴對方在不等待確認的情況下,可以發來多大的資料。這裡表示的最大長度是2^16 - 1 =65535,如需要使用更大的視窗大小,需要使用選項中的視窗擴大因子選項。指傳送本報文段的一方的接收視窗(而不是自己的傳送視窗)。
校驗和
源主機和目的主機根據TCP
報文段以及偽報頭的內容計算校驗和。偽報頭中存放著來自IP
報頭以及TCP
報文段長度資訊。與UDP
一樣,偽報頭並不在網路中傳輸,並且在校驗和中包含偽報頭的目的是為了防止目的主機錯誤地接收存在路由的錯誤資料報。
偽首部
偽首部,又稱偽報頭:指在TCP
的分段或者是UDP
的資料報格式中,在資料報首部增加,源IP
地址,目的IP
地址,IP
分組協議欄位,TCP
或者UDP
資料報的總長度,構成的擴充套件首部結構,共12個位元組。偽首部是個臨時結構,不向下也不向上傳遞,僅僅是為了保證校驗套接字的正確性。
其資料結構如下所示:
//偽頭部:用於TCP/UDP計算CheckSum
//填充欄位值來自IP層
typedef struct tag_pseudo_header
{
u_int32_t source_address; //源IP地址
u_int32_t dest_address; //目的IP地址
u_int8_t placeholder; //必須置0,用於填充對齊
u_int8_t protocol; //協議號(IPPROTO_TCP=6,IPPROTO_UDP=17)
u_int16_t tcplength; //UDP/TCP頭長度(不包含資料部分)
}PseudoHeader_S;
偽首部的結構示意圖如下所示:
緊急指標
僅在 URG = 1
時才有意義,它指出本報文段中的緊急資料的位元組數(緊急資料結束後就是普通資料),即指出了緊急資料的末尾在報文中的位置,注意:即使視窗為零時也可傳送緊急資料。例如,**如果報文段的序號是 1000,前 8 個位元組都是緊急資料,那麼緊急指標就是 8 **。緊急指標一般用途是使使用者可中止程序。
可選項
可選項的格式如下所示:
常用的可選項有以下幾種:
timestamp
時間戳MSS
允許接收的最大報文段SACK
選擇確認選項Window Scale
視窗縮放因子
可選項的長度可變,最長可達 40 位元組,當沒有使用可選項時,TCP 首部長度是 20 位元組。
填充
用於保證選項大小為32位的整數倍。
資料
真正需要進行傳輸的資料。
UDP
報頭
UDP
是 User Datagram Protocol
的簡稱, 中文名是使用者資料報協議,是一種無連線的傳輸層協議,提供面向事務的簡單不可靠資訊傳送服務。
源埠
2個位元組,傳送方埠號
目的埠號
2個位元組,接收方埠號
報文長度
2個位元組,UDP
使用者資料報的總長度,以位元組為單位。
校驗和
2個位元組,檢測UDP
使用者資料報在傳輸過程中是否出錯,出錯直接丟棄。
偽首部
資料
真正需要傳輸的資料。需要注意的是,UDP
的資料部分如果不為偶數需要用 0 填補,就是說,如果資料長度為奇數,資料長度加“1”。
TCP 比 UDP 安全,為什麼還要用UDP?
- 無需建立連線(延遲小);
- 無需維護連線狀態;
- 頭部開銷小,固定8個位元組;
- 應用層可以更好地控制要傳送的資料和傳送時間。
UDP為什麼那麼快?
- 不需要建立連線;
- 不需要進行確認;
- 沒有超時重發機制;
- 沒有流量控制和擁塞控制;
TCP 流量控制
TCP
協議其實是一個傳輸控制協議,顧名思義,就是用來傳輸資料的。如果從socket
角度來看TCP
的話,就是下面這樣的:
傳送端傳送的資料放入傳送快取區Send Buffer
中,接收端要把接收到的資料放入接收快取區Receive Buffer
中,上層應用程式們不停地塞取資料,完成資料的傳輸。
在這個傳輸過程中,有個很明顯的問題,就是Send Buffer
和Receive Buffer
都是有限的,萬一滿了溢位了怎麼辦。這個時候就是流量控制派上用場的時候了。
流量控制做的事情就是,如果Receive Buffer
接收快取區滿了之後,傳送端此時就要停止傳送資料,以便接收端及時處理完已接收的資料。那傳送端如何知曉接收端的緩衝區已滿了呢?
為了控制傳送端的傳送速率,接收端會告知傳送端自己的接收視窗大小,即Receive Buffer
接收快取區中空閒的部分,如下所示:
接收端在每次接收完傳送端傳送的資料之後,都會應答一個ACK
報文,在這個報文中,接收端會告知傳送端此時自己的接收視窗。
滑動視窗
TCP
中的視窗並不是固定的,由於報頭、網路阻塞等影響,TCP
中視窗的大小一般會變化。這就是滑動視窗的由來,可以根據實際環境因素,動態地調整。滑動視窗分為兩種,傳送視窗和接收視窗。
資料包的狀態
TCP
報文中的資料,傳送端的資料根據狀態不同可以分為以下四種:
- 1.已傳送且已經被接收端
ACK
的資料包; - 2.已傳送但尚未收到接收端
ACK
的資料包,如果在一段時間內都沒有收到接收端的ACK
,則會進行重傳; - 3.未傳送但接收端表示有空間可以接收的資料包;
- 4.未傳送且接收端沒有空間進行接收的資料包;
傳送視窗
傳送視窗表示某個時刻,傳送端能擁有的最大的可以傳送但允許未被確認的資料包大小,也是傳送端被允許傳送的最大的資料包大小,就是上圖中2與3的和。
可用視窗
可用視窗是指傳送端還能傳送的最大資料包大小,等於傳送視窗的大小減去已傳送但未被確認的資料包大小,即上圖中3的部分,未傳送但可以被接收端接收的資料包大小。
對於傳送視窗來說,其左邊界為成功傳送且已經被接收方確認的最大位元組序號,視窗的右邊界是傳送方當前可以傳送的最大位元組序號,滑動視窗的大小即為右邊界減去左邊界。
當上圖的可用視窗的6個位元組資料(46~51)傳送出去,可用視窗大小就變成了0,這個時候除非接收到來自接收端的ACK
,否則傳送端不再接收資料。
每次成功傳送資料之後,傳送視窗就會在傳送快取區中按順序移動,將新的資料包含到視窗中準備傳送。
接收視窗
TCP`報文中的資料,接收端的資料根據狀態不同可以分為以下三種:
- 已接收並回復給傳送端
ACK
的資料包; - 準備接收發送端傳送的資料包;
- 尚未準備接收的快取區;
當收到資料包後,將視窗向前移動一個位置,併發回ACK
,若收到的資料落在接收視窗之外,則一律丟棄。
流量控制過程
這裡我們不用太複雜的例子,以一個最簡單的來回來模擬一下流量控制的過程,方便大家理解。
首先雙方三次握手,初始化各自的視窗大小,均為 200 個位元組。
假如當前傳送端給接收端傳送 100 個位元組,那麼此時對於傳送端而言,SND.NXT 當然要右移 100 個位元組,也就是說當前的可用視窗
減少了 100 個位元組,這很好理解。
現在這 100 個到達了接收端,被放到接收端的緩衝佇列中。不過此時由於大量負載的原因,接收端處理不了這麼多位元組,只能處理 40 個位元組,剩下的 60
個位元組被留在了緩衝佇列中。
注意了,此時接收端的情況是處理能力不夠用啦,你傳送端給我少發點,所以此時接收端的接收視窗應該縮小,具體來說,縮小 60 個位元組,由 200 個位元組變成了 140 位元組,因為緩衝佇列還有 60 個位元組沒被應用拿走。
因此,接收端會在 ACK 的報文首部帶上縮小後的滑動視窗 140 位元組,傳送端對應地調整發送視窗的大小為 140 個位元組。
此時對於傳送端而言,已經發送且確認的部分增加 40 位元組,也就是 SND.UNA 右移 40 個位元組,同時傳送視窗縮小為 140 個位元組。
這也就是流量控制的過程。儘管回合再多,整個控制的過程和原理是一樣的。
擁塞控制
前面說的流量控制一般發生在傳送端和接收端之間,並沒有考慮整個網路環境的影響。當網路特別差時,容易發生丟包現象,此時傳送端就要進行一定的控制,防止過多的資料注入到網路中。這個時候,擁塞控制就派上用場了。
對於傳送端來說,它需要維護兩個狀態變數,擁塞視窗(Congestion Window,cwnd
)和慢啟動閾值(Slow Start Threshold
)。
擁塞視窗是指發生端目前還能傳輸的資料量大小。它與之前介紹的接收視窗有所不同:
- 接收視窗:接收端給的限制,用於限制接收視窗的大小;
- 擁塞視窗:傳送端給的限制,用於限制傳送視窗的大小;
在之前介紹傳送視窗的時候,我們也提到過,傳送視窗會根據接收端返回的ACK
資訊進行調整,那麼這兩個視窗傳送視窗(swnd
)呢?答案是取二者之中最小的那個,如下所示:
swnd=min(rwnd , cwnd)
擁塞控制就是用來控制擁塞視窗cwnd
的大小。擁塞控制過程中涉及到幾種演算法,分別是:
- 慢啟動 & 擁塞避免
- 快重傳 & 快恢復
慢啟動
當傳送端和接收端通過三次握手建立連線後,接下來就要開始傳輸資料了。由於傳送端此時不知道現在的網路處於什麼狀況,如果一下子傳送大量的資料,則可能會大量丟包。因此,擁塞控制首先採用一種名為慢啟動的演算法來試探網路的擁塞情況。主要原理就是,當主機開始傳送資料時,由小到大逐漸增大擁塞視窗數值(即 傳送視窗數值),從而逐漸增大發送報文段,流程如下:
- 首先,三次握手之後,傳送端和接收端明確自己的接收視窗大小;
- 雙方初始化自己的擁塞視窗大小,一般設定的比較小,如一個
MSS
; - 在最初開始傳輸的一段時間內,傳送端每收到一個
ACK
,則將擁塞視窗大小加倍。就是說,每經過一個輪次,即RTT(route-trip time)
,擁塞視窗cwnd
加倍;
如下圖所示:
需要注意的是,這裡的慢並不是指傳輸慢或者是增長速率慢,而是指一開始傳送報文段時擁塞視窗cwnd
設定的比較小,使得傳送方在開始時只發送一個報文段,主要是試探一下網路的擁塞情況。
難道一直會一直翻倍下去?肯定不會啊,所以慢啟動閾值就派上用場了。當擁塞視窗的大小達到這個閾值之後,擁塞避免就要開始上場了。
擁塞避免
擁塞避免演算法可以使得擁塞視窗按線性規律緩慢增長。慢啟動階段每一個輪次擁塞視窗加倍,但到了擁塞避免階段,擁塞視窗每一個輪次都只增加1,如下圖所示:
需要注意的是,擁塞避免並不能避免擁塞,只是將擁塞視窗的增長減緩,減少網路當中的資料數量。
例項分析
藉助於下圖進行慢啟動和擁塞避免的具體分析。
過程分析:
- 當擁塞視窗
cwnd
小於慢啟動閾值ssthres
時,使用慢啟動演算法; - 當擁塞視窗
cwnd
大於慢啟動閾值ssthres
時,停止使用慢啟動,開始使用擁塞避免演算法; - 當出現網路擁塞時,將慢啟動閾值
ssthres
設定為出現擁塞時傳送視窗的一半,但必須大於等於2;把擁塞視窗重新設定為1; - 此時,擁塞視窗
cwnd
小於慢啟動閾值ssthres
,開始使用慢啟動演算法; - 當擁塞視窗
cwnd
大於慢啟動閾值時,停止使用慢啟動,開始使用擁塞避免演算法;
這兩種演算法結合可以在出現網路擁塞時迅速減少主機發送到網路中的分組數,使得傳送擁塞的路由器有足夠的時間把佇列中積壓的分組處理完畢。
注
- 乘法減小:出現網路擁塞時,慢啟動閾值設定為出現擁塞時傳送視窗的一般;
- 加法增大:擁塞避免時的擁塞視窗緩慢增大
二者合併為AIMD演算法,即加法增大,乘法減小。
快重傳
在TCP
傳輸資料的過程,如果發生了丟包,即接收端發現數據段不是按序到達的,此時快重傳就要上場了,以便及時提醒傳送端存在丟失報文段的現象,提高整個網路的吞吐量。
原理:
- 接收方每收到一個失序的報文段,就立即發出重複確認,為了及時告知傳送端有資料丟失,而不用等待到自己要傳送資料時才進行確認;
- 傳送方只有連續收到3個重複確認就立即重傳對方尚未收到的報文段,而不必等到設定的重傳計時器到期;
快重傳的過程如下所示:
-
在傳輸的過程中,接收端在接收了
M1
、M2
後,緊接著收到了M4
,則M4
就是失序報文段; -
此時接收端就接著傳送對
M2
的重複確認; -
接著
M5
,M6
又到達了接收端,則接收端繼續傳送兩個對M2
的重複確認; -
至此,傳送端收到四個對
M2
的確認,後三個都是重複確認; -
接著傳送端就會立即重傳接收端尚未收到的
M3
,而不必等到設定的重傳計時器到期;
好了,上面說到如果出現丟失報文段,傳送端就會進行重傳,那麼重傳哪段呢?
選擇性重傳
在上面快重傳的流程分析過程中,當M3
丟失之後,接收端會告知傳送端進行重發,但重發哪一段呢?答案是隻會重傳丟失的那一段,如何實現呢?
接收端傳送的重複確認的報文中,在報文首部中有個SACK
選項,通過left edge
和right edge
這兩個值來告知傳送端已經接收到了哪些區間的資料報。因此,即使M3
報文丟失了,當收到M4
、M5
、M6
後,接收端會告知傳送端,這幾個報文正常接收到了,剩下M2
這個報文沒到,就只需要重傳M3
這個報文即可。這個過程也稱之為**選擇性重傳(SACK,Selective Acknowledgment)
,解決如何重傳的問題。
快恢復
當傳送端連續收到3個重複確認之後,發現出現丟包現象,覺得現在的網路已經有些擁塞,自己會進入快恢復階段,如下所示:
流程如下:
TCP
連線剛剛建立,使用慢啟動演算法;- 擁塞視窗大於慢啟動閾值,開始使用擁塞避免演算法;
- 整個過程都是用快重傳演算法;收到三個連續的重複確認,開始執行快重傳演算法,即立即回傳對方尚未收到的報文段;
- 執行快恢復演算法,將慢啟動閾值設定為擁塞時傳送視窗的一半,擁塞視窗設定為此時的慢啟動閾值大小,接著使用擁塞避免演算法,使擁塞視窗緩慢線性增大;
注:快恢復演算法中,慢啟動的應用場景,TCP
連線建立和網路超時兩種情況。
擁塞控制總結
快重傳和快回復是對慢啟動和擁塞避免的改進。
傳輸效率
流量控制過程中,也要考慮到傳輸效率的問題。
Nagle 演算法
Negle
演算法主要是為了避免網路中存在大量的小包(TCP
報頭佔比過大)造成擁塞。Negle
演算法就是為了儘可能傳送大塊資料,避免網路中充斥著許多小資料塊。Negle
演算法要求一個TCP
連線上最多隻有一個未被確認的未完成的小分組,在該組的確認未到達之前不能傳送其他的小分組。
Negle
演算法主要是為了提高網路的吞吐量,總結如下:
-
第一次傳送資料時,不用管包大小,不用等待,直接傳送;
-
後面滿足下面的條件之一就可以繼續傳送:
-
之前所有的包都
ACK
-
資料包的大小達到最大段大小(
MSS
); -
等待一定的時間(一般為200
ms
); -
緊急傳送;
-
糊塗視窗綜合症
設想一種場景:當接收端的快取已滿,但是上層的應用程式每次只讀取一個位元組的資料,然後向傳送端迴應一個ACK
,並將接收視窗設定為1個位元組。於是發生端接著傳送了一個位元組的資料過來,接收端繼續確認,迴圈往復,致使傳輸的效率很低。
解決辦法:讓接收端等待一段時間,使其滿足下面條件之一:
- 接收端快取區有足夠空間可以容納一個最長的報文段;
- 接收端快取區的一半大小;
只有滿足其中一個條件,接收端就可以迴應ACK
,向傳送端通知自己當前的接收視窗大小。另外,傳送端也不要傳送太小的資料包,而是把資料積累成足夠大的報文段,或者是達到接收端緩衝區一半大小。
延遲確認
試想這樣一個場景,當我收到了傳送端的一個包,然後在極短的時間內又接收到了第二個包,那我是一個個地回覆,還是稍微等一下,把兩個包的 ACK 合併後一起回覆呢?
延遲確認(delayed ack)所做的事情,就是後者,稍稍延遲,然後合併 ACK,最後才回復給傳送端。TCP 要求這個延遲的時延必須小於500ms,一般作業系統實現都不會超過200ms。
不過需要主要的是,有一些場景是不能延遲確認的,收到了就要馬上回復:
- 接收到了大於一個 frame 的報文,且需要調整視窗大小;
- TCP 處於 quickack 模式(通過
tcp_in_quickack_mode
設定); - 發現了亂序包;
TCP中的Keep-alive
TCP 層面也是有keep-alive
機制,而且跟應用層不太一樣。
試想一個場景,當有一方因為網路故障或者宕機導致連線失效,由於 TCP 並不是一個輪詢的協議,在下一個資料包到達之前,對端對連線失效的情況是一無所知的。
這個時候就出現了 keep-alive, 它的作用就是探測對端的連線有沒有失效。
在 Linux 下,可以這樣檢視相關的配置:
sudo sysctl -a | grep keepalive
// 每隔 7200 s 檢測一次
net.ipv4.tcp_keepalive_time = 7200
// 一次最多重傳 9 個包
net.ipv4.tcp_keepalive_probes = 9
// 每個包的間隔重傳間隔 75 s
net.ipv4.tcp_keepalive_intvl = 75
不過,現狀是大部分的應用並沒有預設開啟 TCP 的keep-alive
選項,為什麼?
站在應用的角度:
- 7200s 也就是兩個小時檢測一次,時間太長
- 時間再短一些,也難以體現其設計的初衷, 即檢測長時間的死連線
因此是一個比較尷尬的設計。
TCP 粘包問題
由於TCP
連線是無邊界的,這就導致資料在傳輸過程中出現粘包問題。什麼是粘包?TCP
報文粘連是指,本來發送的多個TCP
報文,但是在接收端收到的卻是一個報文,把多個報文合併成了一個。由於UDP
傳輸的報文是有邊界的(兩段訊息間存在界限),所以其不會出現粘包現象。粘包有可能發生在傳送端,也有可能發生在接收端。TCP
產生粘包的原因有兩種,下面詳細解釋一下。
接收端問題
接收端會把接收到的資料存放在快取區當中,然後通知上層應用去取資料。當應用層由於某些原因不能及時的把資料取走,則會造成接收快取區存放了多條報文,由此產生報文粘連現象。
Negle 演算法
上面介紹到的Negle
演算法時說到,Negle
演算法是為了解決網路中存在大量的小包(資料報首部佔比過大),從而導致的網路擁塞。因此,當開啟Negle
演算法時,當有資料需要傳送的時候,先不傳送,而是稍微等待一會,看看在這一小段時間內,還有沒有其他需要傳送的資料,然後再把需要傳送的資料一次性發送出去。這樣雖然可以提高網路的吞吐量和利用率,但是當傳送端快取區存放著幾條報文時,就有可能產生報文粘連的現象。
總結下來就是,傳送端沒有及時傳送,接收端沒有及時清除,這樣才導致了粘包現象。
解決辦法
- 關閉
Negle
演算法,在SOCKET
選項中,TCP_NODELAY
表示關閉Negle
演算法; - 上層應用盡快將快取區中的資料讀取使用;
- 在傳送的資料中,新增識別符號,標識著資料的開始和結束,當收到訊息,通過這些標識來處理報文粘連;
TCP快速開啟原理
TCP
每次建立連線的時候都要進行三次握手,很是麻煩,於是後面出現了TCP
握手流程的優化,即TCP
快速開啟(TCP Fast Open
, TFO
)。
TFO 流程
首輪三次握手
首先客戶端傳送SYN
給服務端,服務端接收到。
注意哦!現在服務端不是立刻回覆 SYN
+ ACK
,而是通過計算得到一個SYN Cookie
, 將這個Cookie
放到 TCP 報文的 Fast Open
選項中,然後才給客戶端返回。
客戶端拿到這個 Cookie
的值快取下來。後面正常完成三次握手。
首輪三次握手就是這樣的流程。而後面的三次握手就不一樣啦!
後面的三次握手
在後面的三次握手中,客戶端會將之前快取的 Cookie
、SYN
和HTTP請求
(是的,你沒看錯)傳送給服務端,服務端驗證了 Cookie 的合法性,如果不合法直接丟棄;如果是合法的,那麼就正常返回SYN
+ ACK
。
重點來了,現在服務端能向客戶端發 HTTP 響應了!這是最顯著的改變,三次握手還沒建立,僅僅驗證了 Cookie 的合法性,就可以返回 HTTP 響應了。
當然,客戶端的ACK
還得正常傳過來,不然怎麼叫三次握手嘛。
流程如下:
注意: 客戶端最後握手的 ACK
不一定要等到服務端的 HTTP
響應到達才傳送,兩個過程沒有任何關係。
TFO 的優勢
TFO
的優勢並不在與首輪三次握手,而在於後面的握手,在拿到客戶端的 Cookie
並驗證通過以後,可以直接返回 HTTP
響應,充分利用了1 個RTT(Round-Trip Time
,往返時延)的時間提前進行資料傳輸,積累起來還是一個比較大的優勢。
TCP報文中時間戳的作用?
timestamp
是 TCP 報文首部的一個可選項,一共佔 10 個位元組,格式如下:
kind(1 位元組) + length(1 位元組) + info(8 個位元組)
其中 kind
= 8, length
= 10, info
有兩部分構成: timestamp
和timestamp echo
,各佔 4 個位元組。
那麼這些欄位都是幹嘛的呢?它們用來解決那些問題?
接下來我們就來一一梳理,TCP 的時間戳主要解決兩大問題:
- 計算往返時延
RTT
(Round-Trip Time
) - 防止序列號的迴繞問題
計算往返時延 RTT
在沒有時間戳的時候,計算 RTT
會遇到的問題如下圖所示:
如果以第一次發包為開始時間的話,就會出現左圖的問題,RTT
明顯偏大,開始時間應該採用第二次的;
如果以第二次發包為開始時間的話,就會導致右圖的問題,RTT
明顯偏小,開始時間應該採用第一次發包的。
實際上無論開始時間以第一次發包還是第二次發包為準,都是不準確的。
那這個時候引入時間戳就很好的解決了這個問題。
比如現在 a
向 b
傳送一個報文 s1
,b
向 a
回覆一個含 ACK
的報文 s2
那麼:
- step 1:
a
向b
傳送的時候,timestamp
中存放的內容就是a
主機發送時的核心時刻ta1
。 - step 2:
b
向a
回覆s2
報文的時候,timestamp
中存放的是b
主機的時刻tb
,timestamp echo
欄位為從s1
報文中解析出來的ta1
。 - step 3:
a
收到b
的s2
報文之後,此時a
主機的核心時刻是ta2
, 而在s2
報文中的timestamp echo
選項中可以得到ta1
, 也就是s2
對應的報文最初的傳送時刻。然後直接採用ta2 - ta1
就得到了RTT
的值。
防止序列號迴繞問題
現在我們來模擬一下這個問題。
序列號的範圍其實是在0 ~ 2 ^ 32 - 1
, 為了方便演示,我們縮小一下這個區間,假設範圍是 0 ~ 4
,那麼到達 4 的時候會回到 0。
第幾次發包 | 傳送位元組 | 對應序列號 | 狀態 |
---|---|---|---|
1 | 0 ~ 1 | 0 ~ 1 | 成功接收 |
2 | 1 ~ 2 | 1 ~ 2 | 滯留在網路中 |
3 | 2 ~ 3 | 2 ~ 3 | 成功接收 |
4 | 3 ~ 4 | 3 ~ 4 | 成功接收 |
5 | 4 ~ 5 | 0 ~ 1 | 成功接收,序列號從0開始 |
6 | 5 ~ 6 | 1 ~ 2 | ??? |
假設在第 6 次的時候,之前還滯留在網路中的包回來了,那麼就有兩個序列號為1 ~ 2
的資料包了,怎麼區分誰是誰呢?這個時候就產生了序列號迴繞的問題。
那麼用timestamp
就能很好地解決這個問題,因為每次發包的時候都是將發包機器當時的核心時間記錄在報文中,那麼兩次發包序列號即使相同,時間戳也不可能相同,這樣就能夠區分開兩個資料包了。
TCP 的超時重傳時間是如何計算的?
TCP 具有超時重傳機制,即間隔一段時間沒有等到資料包的回覆時,重傳這個資料包。
那麼這個重傳間隔是如何來計算的呢?
今天我們就來討論一下這個問題。
這個重傳間隔也叫做超時重傳時間(Retransmission TimeOut
, 簡稱RTO
),它的計算跟上一節提到的 RTT
密切相關。這裡我們將介紹兩種主要的方法,一個是經典方法,一個是標準方法。
經典方法
經典方法引入了一個新的概念——SRTT
(Smoothed round trip time
,即平滑往返時間),每產生一次新的 RTT
就根據一定的演算法對 SRTT
進行更新,具體而言,計算方式如下(SRTT
初始值為0):
SRTT = (α * SRTT) + ((1 - α) * RTT)
其中,α
是平滑因子,建議值是0.8
,範圍是0.8 ~ 0.9
。
拿到 SRTT
,我們就可以計算 RTO
的值了:
RTO = min(ubound, max(lbound, β * SRTT))
β
是加權因子,一般為1.3 ~ 2.0
, lbound
是下界,ubound
是上界。
其實這個演算法過程還是很簡單的,但是也存在一定的侷限,就是在 RTT
穩定的地方表現還可以,而在 RTT
變化較大的地方就不行了,因為平滑因子 α
的範圍是0.8 ~ 0.9
, RTT
對於 RTO
的影響太小。
標準方法
為了解決經典方法對於 RTT
變化不敏感的問題,後面又引出了標準方法,也叫Jacobson / Karels 演算法
。
一共有三步。
第一步: 計算SRTT
,公式如下:
SRTT = (1 - α) * SRTT + α * RTT
注意這個時候的 α
跟經典方法中的α
取值不一樣了,建議值是1/8
,也就是0.125
。
第二步: 計算RTTVAR
(round-trip time variation
)這個中間變數。
RTTVAR = (1 - β) * RTTVAR + β * (|RTT - SRTT|)
β
建議值為 0.25。這個值是這個演算法中出彩的地方,也就是說,它記錄了最新的 RTT
與當前 SRTT
之間的差值,給我們在後續感知到 RTT
的變化提供了抓手。
第三步: 計算最終的RTO
:
RTO = µ * SRTT + ∂ * RTTVAR
µ
建議值取1
, ∂
建議值取4
。
這個公式在 SRTT
的基礎上加上了最新 RTT
與它的偏移,從而很好的感知了 RTT
的變化,這種演算法下,RTO
與RTT
變化的差值關係更加密切。