大家好,我是小林。
之前我在公眾號解答了一位讀者面試騰訊的面試題,問題如下:
針對這個問題,我也沒辦法用實驗來驗證我的結論,所以當時結論是基于啃 TCP 源碼得出來的。
但是,就在昨天!
有位讀者在工作中抓到跟這個面試題場景類似的抓包圖,我看了下,現象跟我之前啃 TCP 源碼得出的結論是符合的。
這種被印證的感覺真爽!
我覺得這個案例還是挺有意思的,因為很好的說明是 TCP 傳輸協議是按序接收的。
所以,先來回顧騰訊一面的這個問題,再來看看跟這個問題相似的抓包圖。
回顧問題這道鵝廠的網絡題可能是提問的讀者表述有問題。
因為如果 FIN 報文比數據包先抵達客戶端,此時 FIN 報文其實是一個亂序的報文,此時客戶端的 TCP 連接并不會從 FIN_WAIT_2 狀態轉換到 TIME_WAIT 狀態。
因此,我們要關注到點是看「在 FIN_WAIT_2 狀態下,是如何處理收到的亂序的 FIN 報文,然后 TCP 連接又是什么時候才進入到 TIME_WAIT 狀態?」。
我這里先直接說結論:
在 FIN_WAIT_2 狀態時,如果收到亂序的 FIN 報文,那么就被會加入到內核中的「亂序隊列」,并不會進入到 TIME_WAIT 狀態。
等再次收到前面被網絡延遲的數據包時,會判斷亂序隊列有沒有數據,然后會檢測亂序隊列中是否有可用的數據,如果能在亂序隊列中找到與當前報文的序列號保持的順序的報文,就會看該報文是否有 FIN 標志,如果發現有 FIN 標志,這時才會進入 TIME_WAIT 狀態。
我也畫了一張圖,大家可以結合著圖來理解。
神一般的抓包圖下圖是昨天一位讀者發給我的抓包圖,圖中的異常情況,跟前面這個問題的現象有點類似:
你可能會有疑問為什么 TCP 握手時,雙方的 seq 都是 0 開始的?這個是抓包圖做了優化,顯示的是相對值,而不是真實值,顯示相對值方便分析。
為了方便文字描述,我針對異常部分的報文進行編號如下:
圖中端口號為 11710 的為客戶端,端口號為 8080 的為服務端。另外,編號 4 是客戶端發送的 http 請求,抓包圖沒有顯示 TCP 信息,這里文字補充下:編號 4 數據報文 seq = 1,ack = 1,len = 27。
編號 6 是 FIN 報文,也就是服務端向客戶端發送的 FIN 報文(第一次揮手),但是是一個亂序的 FIN 報文,因為從編號 4 報文中的 ack = 1 知道,客戶端期望下一次收到的報文的序列號為 1,而當前收到的 FIN 報文的 seq = 177,這并不是客戶端下一次期望收到的報文,所以是亂序的。
客戶端收到亂序 FIN 報文后,并不會從 establish 轉為 close_wait 狀態,而是把這個亂序的 FIN 報文放到內核中的亂序隊列。因為如果這時候就進入了 close_wait 狀態,就會馬上發送 FIN 報文了(第三次揮手),而不會有客戶端后面發送的編號 8 和 9 報文的事情了。
編號 8 是應答報文,是客戶端對編號 6 亂序 FIN 報文的應答報文,可以看到這個應答報文中 seq = 28,ack=1,因為并不是我期望的下一個報文,所以應答報文中 ack 還是為 1。
編號 7 是數據報文,也就是服務端向客戶端發送的數據報文,該報文 seq = 1,ack=28,len=176,因為 seq 為 1,所以是客戶端期望收到的報文??蛻舳耸盏皆搱笪暮?,就回了編號 9 應答報文,此應答報文 seq = 28,ack = 178,其中 ack = 178 是告訴服務端:“你發的 seq = 178 之前(不包括seq=178)的報文,我都收到了,我下次期望收到的報文的 seq 為 178”。
客戶端收到編號 7 數據報文時還會做一件事情,會檢測「亂序隊列」中是否有可用的數據,如果能在亂序隊列中找到與當前收到報文的序列號「保持的順序」的報文,就把處于亂序隊列的報文移到可以被正常處理的數據隊列。比如,這次的案例中,編號 7 報文 seq = 1,len=176 的數據范圍是 1~176,而亂序隊列中的 FIN 報文的 seq = 177,這兩個報文的 seq 正好是保持順序的,所以會把 FIN 報文從亂序隊列中拿出來一起處理,然后發現有 FIN 標志,于是就會轉換狀態。
所以,客戶端在應答完編號 7 數據報文后,就立馬發送 FIN 報文了(第三次揮手),接著服務端應答了該報文,至此四次揮手結束。
這個抓包圖跟前面這個騰訊的面試題有一點差異,差異在于:
騰訊的面試題是在 FIN_WAIT_2 狀態下收到亂序的 FIN 報文(第三次揮手);抓包圖是在 establish 狀態收到了亂序的 FIN 報文(第一次揮手);上面這兩種 TCP 狀態,收到亂序的 FIN 報文,并不會立即轉換狀態,只會被內核放到一個亂序隊列里。等收到一個序列號符合「接收方」期望收到的序列號的數據包時,會檢測「亂序隊列」中是否有可用的數據,如果能在亂序隊列中找到與當前收到報文的序列號「保持的順序」的報文,就會看該報文是否有 FIN 標志,如果發現有 FIN 標志,就會轉換狀態。
所以,從這里可以看到, TCP 傳輸協議是按序接收,如果收到一個亂序的報文時,并且在接收窗口范圍內(序列號超過接收窗口范圍外的報文就會被丟棄),就會緩存在內核中的亂序隊列,不做其他處理。等收到能與亂序隊列中報文的序列號保持順序的報文,才會一起被處理。
TCP 層必須保證收到的字節數據是完整且有序的,所以如果序列號較低的 TCP 報文在網絡傳輸中丟失了,即使序列號較高的 TCP 報文已經被接收了,應用層也無法從內核中讀取到這部分數據。
舉個例子,如下圖:
圖中發送方發送了很多個 packet,每個 packet 都有自己的序號,你可以認為是 TCP 的序列號,其中 packet 3 在網絡中丟失了,即使 packet 4-6 被接收方收到后,由于內核中的 TCP 數據不是連續的,于是接收方的應用層就無法從內核中讀取到,只有等到 packet 3 重傳后,接收方的應用層才可以從內核中讀取到數據。
X 關閉
X 關閉