大家好,我是小林。
為什么 TCP 三次握手期間,客戶端和服務端的初始化序列號要求不一樣的呢?
接下來,我一步一步給大家講明白,我覺得應該有不少人會有類似的問題,所以今天在肝一篇!
正文
為什么 TCP 三次握手期間,為什么客戶端和服務端的初始化序列號要求不一樣的呢?
主要原因是為了防止歷史報文被下一個相同四元組的連接接收。
TCP 四次揮手中的 TIME_WAIT 狀態不是會持續 2 MSL 時長,歷史報文不是早就在網絡中消失了嗎?
是的,如果能正常四次揮手,由於 TIME_WAIT 狀態會持續 2 MSL 時長,歷史報文會在下一個連接之前就會自然消失。
但是來了,我們並不能保證每次連接都能通過四次揮手來正常關閉連接。
假設每次建立連接,客戶端和服務端的初始化序列號都是從 0 開始:

過程如下:
- 客戶端和服務端建立一個 TCP 連接,在客戶端發送數據包被網絡阻塞了,而此時服務端的進程重啟了,於是就會發送 RST 報文來斷開連接。
- 緊接着,客戶端又與服務端建立了與上一個連接相同四元組的連接;
- 在新連接建立完成后,上一個連接中被網絡阻塞的數據包正好抵達了服務端,剛好該數據包的序列號正好是在服務端的接收窗口內,所以該數據包會被服務端正常接收,就會造成數據錯亂。
可以看到,如果每次建立連接,客戶端和服務端的初始化序列號都是一樣的話,很容易出現歷史報文被下一個相同四元組的連接接收的問題。
客戶端和服務端的初始化序列號不一樣不是也會發生這樣的事情嗎?
是的,即使客戶端和服務端的初始化序列號不一樣,也會存在收到歷史報文的可能。
但是我們要清楚一點,歷史報文能否被對方接收,還要看該歷史報文的序列號是否正好在對方接收窗口內,如果不在就會丟棄,如果在才會接收。
如果每次建立連接客戶端和服務端的初始化序列號都「不一樣」,就有大概率因為歷史報文的序列號「不在」對方接收窗口,從而很大程度上避免了歷史報文,比如下圖:

相反,如果每次建立連接客戶端和服務端的初始化序列號都「一樣」,就有大概率遇到歷史報文的序列號剛「好在」對方的接收窗口內,從而導致歷史報文被新連接成功接收。
所以,每次初始化序列號不一樣能夠很大程度上避免歷史報文被下一個相同四元組的連接接收,注意是很大程度上,並不是完全避免了。
那客戶端和服務端的初始化序列號都是隨機的,那還是有可能隨機成一樣的呀?
RFC793 提到初始化序列號 ISN 隨機生成算法:ISN = M + F(localhost, localport, remotehost, remoteport)。
- M是一個計時器,這個計時器每隔4毫秒加1。
- F 是一個 Hash 算法,根據源IP、目的IP、源端口、目的端口生成一個隨機數值,要保證 hash 算法不能被外部輕易推算得出。
可以看到,隨機數是會基於時鍾計時器遞增的,基本不可能會隨機成一樣的初始化序列號。
懂了,客戶端和服務端初始化序列號都是隨機生成的話,就能避免連接接收歷史報文了。
是的,但是也不是完全避免了。
為了能更好的理解這個原因,我們先來了解序列號(SEQ)和初始序列號(ISN)。
- 序列號,是 TCP 一個頭部字段,標識了 TCP 發送端到 TCP 接收端的數據流的一個字節,因為 TCP 是面向字節流的可靠協議,為了保證消息的順序性和可靠性,TCP 為每個傳輸方向上的每個字節都賦予了一個編號,以便於傳輸成功后確認、丟失后重傳以及在接收端保證不會亂序。序列號是一個 32 位的無符號數,因此在到達 4G 之后再循環回到 0。
- 初始序列號,在 TCP 建立連接的時候,客戶端和服務端都會各自生成一個初始序列號,它是基於時鍾生成的一個隨機數,來保證每個連接都擁有不同的初始序列號。初始化序列號可被視為一個 32 位的計數器,該計數器的數值每 4 微秒加 1,循環一次需要 4.55 小時。
給大家抓了一個包,下圖中的 Seq 就是序列號,其中紅色框住的分別是客戶端和服務端各自生成的初始序列號。

圖片
通過前面我們知道,序列號和初始化序列號並不是無限遞增的,會發生回繞為初始值的情況,這意味着無法根據序列號來判斷新老數據。
不要以為序列號的上限值是 4GB,就以為很大,很難發生回繞。在一個速度足夠快的網絡中傳輸大量數據時,序列號的回繞時間就會變短。如果序列號回繞的時間極短,我們就會再次面臨之前延遲的報文抵達后序列號依然有效的問題。
為了解決這個問題,就需要有 TCP 時間戳。tcp_timestamps 參數是默認開啟的,開啟了 tcp_timestamps 參數,TCP 頭部就會使用時間戳選項,它有兩個好處,一個是便於精確計算 RTT ,另一個是能防止序列號回繞(PAWS)。
試看下面的示例,假設 TCP 的發送窗口是 1 GB,並且使用了時間戳選項,發送方會為每個 TCP 報文分配時間戳數值,我們假設每個報文時間加 1,然后使用這個連接傳輸一個 6GB 大小的數據流。

32 位的序列號在時刻 D 和 E 之間回繞。假設在時刻B有一個報文丟失並被重傳,又假設這個報文段在網絡上繞了遠路並在時刻 F 重新出現。如果 TCP 無法識別這個繞回的報文,那么數據完整性就會遭到破壞。
使用時間戳選項能夠有效的防止上述問題,如果丟失的報文會在時刻 F 重新出現,由於它的時間戳為 2,小於最近的有效時間戳(5 或 6),因此防回繞序列號算法(PAWS)會將其丟棄。
防回繞序列號算法要求連接雙方維護最近一次收到的數據包的時間戳(Recent TSval),每收到一個新數據包都會讀取數據包中的時間戳值跟 Recent TSval 值做比較,如果發現收到的數據包中時間戳不是遞增的,則表示該數據包是過期的,就會直接丟棄這個數據包。
懂了,客戶端和服務端的初始化序列號都是隨機生成,能很大程度上避免歷史報文被下一個相同四元組的連接接收,然后又引入時間戳的機制,從而完全避免了歷史報文被接收的問題。
嗯嗯,沒錯。