本文轉載自微信公眾號「小菜學編程」,作者fasionchan。轉載本文請聯系小菜學編程公眾號。
上一小節,我們通過一個實驗,深入地研究了 TCP 三次握手建立連接的過程。
我們退出 telnet 命令后,TCP 將關閉連接。于此同時,我們通過 tcpdump 也觀察到 TCP 關閉連接的通信過程。本節,我們繼續深入研究 TCP 關閉連接的通信細節。
上節實驗中的通信過程,已經被抓包并保存起來,我們直接用 tcpdump 命令將其打開( tcp.pcap ):
- root@client[~]?tcpdump-nrtcp.pcap
- readingfromfiletcp.pcap,link-typeLINUX_SLL(Linuxcookedv1)
- 17:31:57.624391ARP,Requestwho-has10.0.0.2tell10.0.0.3,length28
- 17:31:57.624439ARP,Reply10.0.0.2is-at0e:bd:60:1c:69:9d,length28
- 17:31:57.624450IP10.0.0.3.55692>10.0.0.2.22:Flags[S],seq386101196,win29200,options[mss1460,sackOK,TSval811948031ecr0,nop,wscale7],length0
- 17:31:57.624495IP10.0.0.2.22>10.0.0.3.55692:Flags[S.],seq1155103769,ack386101197,win28960,options[mss1460,sackOK,TSval3541712191ecr811948031,nop,wscale7],length0
- 17:31:57.624522IP10.0.0.3.55692>10.0.0.2.22:Flags[.],ack1,win229,options[nop,nop,TSval811948031ecr3541712191],length0
- 17:31:57.635739IP10.0.0.2.22>10.0.0.3.55692:Flags[P.],seq1:42,ack1,win227,options[nop,nop,TSval3541712202ecr811948031],length41
- 17:31:57.635778IP10.0.0.3.55692>10.0.0.2.22:Flags[.],ack42,win229,options[nop,nop,TSval811948042ecr3541712202],length0
- 17:31:59.808411IP10.0.0.3.55692>10.0.0.2.22:Flags[F.],seq1,ack42,win229,options[nop,nop,TSval811950215ecr3541712202],length0
- 17:31:59.809175IP10.0.0.2.22>10.0.0.3.55692:Flags[.],ack2,win227,options[nop,nop,TSval3541714376ecr811950215],length0
- 17:31:59.809464IP10.0.0.2.22>10.0.0.3.55692:Flags[F.],seq42,ack2,win227,options[nop,nop,TSval3541714376ecr811950215],length0
- 17:31:59.809483IP10.0.0.3.55692>10.0.0.2.22:Flags[.],ack43,win229,options[nop,nop,TSval811950216ecr3541714376],length0
很顯然,最后四個包就是四次揮手關閉連接的過程:
當我們按下 Ctrl-D 退出 telnet 命令時,客戶機向服務器發出一個 FIN 包。這是一個設置了 FIN 標志位的 TCP 分組,它告訴服務器,客戶機這端的數據已經發完,準備關閉連接:
服務器收到 FIN 包后,將回復一個 ACK 進行確認。注意到,確認號在 FIN 包序號的基礎上加一,因為 FIN 也要占用一個序號,跟 SYN 一樣。
于此同時,服務器將連接讀端關閉的情況通知上層應用程序—— SSH 服務進程。至此,TCP 連接中從客戶機到服務器的傳輸方向已經關閉,連接處于 半關閉 狀態。
上圖灰色部分就是其中已關閉的傳輸方向,它對客戶機來說是 寫端 ,對服務器來說是 讀端 。另一個方向的數據傳輸仍可正常進行,因此服務器可以繼續發送數據(寫端),客戶機也可以接收服務器發來的數據(讀端)。
SSH 服務進程獲悉客戶機關閉連接后,便準備結束服務并關閉連接。如果這時它還有數據沒發完,仍可通過半開連接發往客戶機。等所有數據都發送完畢,服務器同樣發送 FIN 分組,告訴客戶端連接關閉。
一個 TCP 連接包含兩個方向的傳輸通道,因此需要兩對 FIN/ACK 分組,各自負責關閉對應的方向。因此,這兩對 FIN/ACK 交互也被形象地稱為 四次揮手 。
狀態變遷
TCP 建立連接需要三次握手,關閉連接需要四次揮手,步驟相對繁瑣。這意味著一個 TCP 連接應該有很多中間狀態,接下來我們深入研究一下:
上圖是 TCP 連接全生命周期時序圖,左邊是客戶端的時間軸,右邊是服務端的時間軸。時間軸上的不同顏色,則分別表示客戶端和服務端連接所處的狀態:
- 客戶端發出 SYN 分組,連接進入 SYN_SENT 狀態;
- 服務端收到客戶端發來的 SYN 分組,它回復 SYN/ACK 分組,連接進入 SYN_RECV 狀態;
- 客戶端收到服務器的 SYN/ACK 分組,它回復 ACK 分組,連接進入 ESTABLISHED 狀態;
- 服務器收到客戶端的 ACK 分組,服務端連接也進入 ESTABLISHED 狀態;
- 當連接處于 ESTABLISHED 狀態時,客戶端和服務端可以互相傳輸數據;
- 時序圖中間的數據分組及其后的 ACK 分組,為實驗中 SSH 服務向客戶機返回自己的版本信息(這部分數據被 telnet 命令直接輸出到屏幕中);
- 客戶端準備退出時,它通過 FIN 分組通知服務端,連接進入 FIN_WAIT1 狀態;
- 服務器收到客戶端發來的 FIN 分組,它回復 ACK 分組進行確認,連接進入 CLOSE_WAIT 狀態;
- 客戶端收到服務器發來的 ACK 分組,連接進入 FIN_WAIT2 狀態;
- 這時連接處于半關閉狀態,服務器仍可以向客戶端發送數據;
- 服務器發完剩余數據后,向客戶端發送 FIN 分組,通知客戶端關閉連接,服務端連接便進入 LAST_ACK 狀態;
- 客戶端收到服務器發來的 FIN 分組,回復 ACK 分組進行確認,客戶端連接進行 TIME_WAIT 狀態;
- 服務端收到 ACK 分組后,連接徹底關閉;
- 由于最后一個 ACK 分組可能會丟,客戶端必須在 TIME_WAIT 狀態等待一段時間,以便對服務器重傳的 FIN 分組進行確認;
至此,我們可以得到一個完整的 TCP 狀態變遷圖:
TCP 主動連接方(客戶端)和被動連接方(服務端)的狀態變遷路徑是不一樣的:圖中的綠色路徑是客戶端正常情況的狀態變遷路徑;而紅色路徑是服務端正常情況下的變遷路徑。從圖中可以看到,主動關閉方的狀態變遷,也比被動關閉方要復雜得多。
根據 TCP/IP詳解 的介紹,TCP 協議也支持從服務端發起建立連接。但由于這種實現實際上非常罕見,這里就不作深入介紹了。
狀態變遷是 TCP 協議的一個重要知識點,特別是對 TIME_WAIT 狀態的理解,在后端技術面試中經??疾?。
TIME_WAIT 狀態
TCP 主動關閉方最終會進入 TIME_WAIT 狀態,并維持 2MSL 時長,為什么呢?
如上圖,假設四次揮手中最后一個 ACK 分組在網絡中丟失了,會發生什么事情呢?這時被動關閉的服務端會重傳 FIN 分組。因此主動關閉方不能直接關閉,而應該在 TIME_WAIT 狀態下維持一段時間,以便向重傳的 FIN 分組回復 ACK 分組。否則對端仍會重傳 FIN 分組,并在重試若干次后放棄,這時連接只能異常關閉。
另一方面,一個 TCP 連接由通信雙方的 IP 地址和端口組成的四元組唯一確定。如果主動關閉方不在 TIME_WAIT 狀態下等待一段時間,而是快速關閉釋放資源,又會發生什么事情呢?這時原來的四元組有可能被新的連接復用,而舊連接重傳的 FIN 分組可能因網絡原因延遲到達,最終對新連接產生沖突干擾。
那么,為什么是維持 TIME_WAIT 狀態 2MSL 時長呢?
MSL 是最大分組壽命( maximum segment lifetime )的簡稱,即一個 TCP 分組被丟棄前能夠在網絡中存在的最長時間。這個時間肯定是有限的,因為負責傳輸 TCP 分組的 IP 包中有限制存活時間的 TTL 字段。由于 IP 包對存活時間的限制是基于跳數的,因此兩者不對等。不同的網絡協議棧實現,MSL的取值也不盡相同:30秒、1分鐘或2分鐘都有。
將 TIME_WAIT 狀態維持 2MSL 時長是出于這樣的考慮:
假設最后一個 ACK 分組剛好在存活時間耗盡前到達對端主機,這時已經過了 MSL 時間。對端收到 ACK 后,就會立即關閉連接,不可能再發送 FIN 分組。但如果對端在收到 ACK 前剛剛重傳了 FIN 分組,就必須再經過 MSL 時間才能保證 FIN 分組從網絡中消失。因此,連接必須維持 TIME_WAIT 狀態 2MSL 時間后才能釋放,否則就可能對潛在的新連接造成干擾。
你可能會問,如果最后一個 ACK 分組丟了,對端不是還會繼續重傳 FIN 分組嗎?不用再繼續等待 FIN 失效嗎?
是的,對端肯定會重傳 FIN 分組,而且通常 很快 就開始重傳。MSL 一般是幾十秒,而網絡往返時間要小得多,通常只是毫秒級,TCP 重傳時間數量級也是差不多。因此,在 MSL 時間內,TCP 可以重傳 FIN 分組好幾次了!如果其中有一個 FIN 分組可以到達,TCP 會重置定時器,將 TIME_WAIT 狀態再維持 2MSL 時長。如果幾個 FIN 分組都丟了,那再等下去也沒啥意義了!
- 如果最后一個 ACK 分組可以到達對端,最多只需要等待 2MSL 時間即可保證網絡中沒有對端重傳的 FIN 分組;
-
如果最后一個 ACK 分組丟失了,對端在 MSL 內已經重傳 好多次 了;
- 如果重傳的 FIN 分組有一個可以到達本端,TCP 回復 ACK 后會重置定時器在 TIME_WAIT 繼續等待 2MSL 時長;
- 如果重傳的 FIN 分組都丟了,說明網絡質量很差,再等下去也沒有意義了;
因此,主動關閉方收到對端 FIN 分組后,必須在 TIME_WAIT 狀態等待 2MSL ,才能釋放連接。這樣既保證對重傳 FIN 分組的回復,又保證重傳的 FIN 分組從網絡中消失,不會對復用四元組的新連接造成沖突干擾。
主動關閉方一般是客戶端,并發一般不高,因此 TIME_WAIT 狀態基本不會造成任何影響。如果一個高并發服務(比如 Web 服務)存在大量短連接,則可能留下很多 TIME_WAIT 狀態的連接。由于 TIME_WAIT 狀態套接字無法立即回收,它們將占用大量的系統資源,對服務的性能造成嚴重影響。
這時,系統管理員可以選擇系統的 2MSL 時長適當調短,加快 TIME_WAIT 連接的清理速度。此外,在 Linux 系統中,可以開啟 tcp_tw_recycle 和 tcp_tw_reuse 內核選項,以復用 TIME_WAIT 狀態的套接字。這些屬于 TCP 和系統調優的范疇,后續有機會再專門展開介紹。
原文鏈接:https://mp.weixin.qq.com/s/NjjMzzktGGnkdI3mzjRkog