四次揮手,TCP連接的關(guān)閉
本文轉(zhuǎn)載自微信公眾號(hào)「小菜學(xué)編程」,作者fasionchan。轉(zhuǎn)載本文請(qǐng)聯(lián)系小菜學(xué)編程公眾號(hào)。
上一小節(jié),我們通過一個(gè)實(shí)驗(yàn),深入地研究了 TCP 三次握手建立連接的過程。
我們退出 telnet 命令后,TCP 將關(guān)閉連接。于此同時(shí),我們通過 tcpdump 也觀察到 TCP 關(guān)閉連接的通信過程。本節(jié),我們繼續(xù)深入研究 TCP 關(guān)閉連接的通信細(xì)節(jié)。
上節(jié)實(shí)驗(yàn)中的通信過程,已經(jīng)被抓包并保存起來,我們直接用 tcpdump 命令將其打開( tcp.pcap ):
- root@client [ ~ ] ➜ tcpdump -nr tcp.pcap
- reading from file tcp.pcap, link-type LINUX_SLL (Linux cooked v1)
- 17:31:57.624391 ARP, Request who-has 10.0.0.2 tell 10.0.0.3, length 28
- 17:31:57.624439 ARP, Reply 10.0.0.2 is-at 0e:bd:60:1c:69:9d, length 28
- 17:31:57.624450 IP 10.0.0.3.55692 > 10.0.0.2.22: Flags [S], seq 386101196, win 29200, options [mss 1460,sackOK,TS val 811948031 ecr 0,nop,wscale 7], length 0
- 17:31:57.624495 IP 10.0.0.2.22 > 10.0.0.3.55692: Flags [S.], seq 1155103769, ack 386101197, win 28960, options [mss 1460,sackOK,TS val 3541712191 ecr 811948031,nop,wscale 7], length 0
- 17:31:57.624522 IP 10.0.0.3.55692 > 10.0.0.2.22: Flags [.], ack 1, win 229, options [nop,nop,TS val 811948031 ecr 3541712191], length 0
- 17:31:57.635739 IP 10.0.0.2.22 > 10.0.0.3.55692: Flags [P.], seq 1:42, ack 1, win 227, options [nop,nop,TS val 3541712202 ecr 811948031], length 41
- 17:31:57.635778 IP 10.0.0.3.55692 > 10.0.0.2.22: Flags [.], ack 42, win 229, options [nop,nop,TS val 811948042 ecr 3541712202], length 0
- 17:31:59.808411 IP 10.0.0.3.55692 > 10.0.0.2.22: Flags [F.], seq 1, ack 42, win 229, options [nop,nop,TS val 811950215 ecr 3541712202], length 0
- 17:31:59.809175 IP 10.0.0.2.22 > 10.0.0.3.55692: Flags [.], ack 2, win 227, options [nop,nop,TS val 3541714376 ecr 811950215], length 0
- 17:31:59.809464 IP 10.0.0.2.22 > 10.0.0.3.55692: Flags [F.], seq 42, ack 2, win 227, options [nop,nop,TS val 3541714376 ecr 811950215], length 0
- 17:31:59.809483 IP 10.0.0.3.55692 > 10.0.0.2.22: Flags [.], ack 43, win 229, options [nop,nop,TS val 811950216 ecr 3541714376], length 0
很顯然,最后四個(gè)包就是四次揮手關(guān)閉連接的過程:
當(dāng)我們按下 Ctrl-D 退出 telnet 命令時(shí),客戶機(jī)向服務(wù)器發(fā)出一個(gè) FIN 包。這是一個(gè)設(shè)置了 FIN 標(biāo)志位的 TCP 分組,它告訴服務(wù)器,客戶機(jī)這端的數(shù)據(jù)已經(jīng)發(fā)完,準(zhǔn)備關(guān)閉連接:
服務(wù)器收到 FIN 包后,將回復(fù)一個(gè) ACK 進(jìn)行確認(rèn)。注意到,確認(rèn)號(hào)在 FIN 包序號(hào)的基礎(chǔ)上加一,因?yàn)?FIN 也要占用一個(gè)序號(hào),跟 SYN 一樣。
于此同時(shí),服務(wù)器將連接讀端關(guān)閉的情況通知上層應(yīng)用程序—— SSH 服務(wù)進(jìn)程。至此,TCP 連接中從客戶機(jī)到服務(wù)器的傳輸方向已經(jīng)關(guān)閉,連接處于 半關(guān)閉 狀態(tài)。
上圖灰色部分就是其中已關(guān)閉的傳輸方向,它對(duì)客戶機(jī)來說是 寫端 ,對(duì)服務(wù)器來說是 讀端 。另一個(gè)方向的數(shù)據(jù)傳輸仍可正常進(jìn)行,因此服務(wù)器可以繼續(xù)發(fā)送數(shù)據(jù)(寫端),客戶機(jī)也可以接收服務(wù)器發(fā)來的數(shù)據(jù)(讀端)。
SSH 服務(wù)進(jìn)程獲悉客戶機(jī)關(guān)閉連接后,便準(zhǔn)備結(jié)束服務(wù)并關(guān)閉連接。如果這時(shí)它還有數(shù)據(jù)沒發(fā)完,仍可通過半開連接發(fā)往客戶機(jī)。等所有數(shù)據(jù)都發(fā)送完畢,服務(wù)器同樣發(fā)送 FIN 分組,告訴客戶端連接關(guān)閉。
一個(gè) TCP 連接包含兩個(gè)方向的傳輸通道,因此需要兩對(duì) FIN/ACK 分組,各自負(fù)責(zé)關(guān)閉對(duì)應(yīng)的方向。因此,這兩對(duì) FIN/ACK 交互也被形象地稱為 四次揮手 。
狀態(tài)變遷
TCP 建立連接需要三次握手,關(guān)閉連接需要四次揮手,步驟相對(duì)繁瑣。這意味著一個(gè) TCP 連接應(yīng)該有很多中間狀態(tài),接下來我們深入研究一下:
上圖是 TCP 連接全生命周期時(shí)序圖,左邊是客戶端的時(shí)間軸,右邊是服務(wù)端的時(shí)間軸。時(shí)間軸上的不同顏色,則分別表示客戶端和服務(wù)端連接所處的狀態(tài):
- 客戶端發(fā)出 SYN 分組,連接進(jìn)入 SYN_SENT 狀態(tài);
- 服務(wù)端收到客戶端發(fā)來的 SYN 分組,它回復(fù) SYN/ACK 分組,連接進(jìn)入 SYN_RECV 狀態(tài);
- 客戶端收到服務(wù)器的 SYN/ACK 分組,它回復(fù) ACK 分組,連接進(jìn)入 ESTABLISHED 狀態(tài);
- 服務(wù)器收到客戶端的 ACK 分組,服務(wù)端連接也進(jìn)入 ESTABLISHED 狀態(tài);
- 當(dāng)連接處于 ESTABLISHED 狀態(tài)時(shí),客戶端和服務(wù)端可以互相傳輸數(shù)據(jù);
- 時(shí)序圖中間的數(shù)據(jù)分組及其后的 ACK 分組,為實(shí)驗(yàn)中 SSH 服務(wù)向客戶機(jī)返回自己的版本信息(這部分?jǐn)?shù)據(jù)被 telnet 命令直接輸出到屏幕中);
- 客戶端準(zhǔn)備退出時(shí),它通過 FIN 分組通知服務(wù)端,連接進(jìn)入 FIN_WAIT1 狀態(tài);
- 服務(wù)器收到客戶端發(fā)來的 FIN 分組,它回復(fù) ACK 分組進(jìn)行確認(rèn),連接進(jìn)入 CLOSE_WAIT 狀態(tài);
- 客戶端收到服務(wù)器發(fā)來的 ACK 分組,連接進(jìn)入 FIN_WAIT2 狀態(tài);
- 這時(shí)連接處于半關(guān)閉狀態(tài),服務(wù)器仍可以向客戶端發(fā)送數(shù)據(jù);
- 服務(wù)器發(fā)完剩余數(shù)據(jù)后,向客戶端發(fā)送 FIN 分組,通知客戶端關(guān)閉連接,服務(wù)端連接便進(jìn)入 LAST_ACK 狀態(tài);
- 客戶端收到服務(wù)器發(fā)來的 FIN 分組,回復(fù) ACK 分組進(jìn)行確認(rèn),客戶端連接進(jìn)行 TIME_WAIT 狀態(tài);
- 服務(wù)端收到 ACK 分組后,連接徹底關(guān)閉;
- 由于最后一個(gè) ACK 分組可能會(huì)丟,客戶端必須在 TIME_WAIT 狀態(tài)等待一段時(shí)間,以便對(duì)服務(wù)器重傳的 FIN 分組進(jìn)行確認(rèn);
至此,我們可以得到一個(gè)完整的 TCP 狀態(tài)變遷圖:
TCP 主動(dòng)連接方(客戶端)和被動(dòng)連接方(服務(wù)端)的狀態(tài)變遷路徑是不一樣的:圖中的綠色路徑是客戶端正常情況的狀態(tài)變遷路徑;而紅色路徑是服務(wù)端正常情況下的變遷路徑。從圖中可以看到,主動(dòng)關(guān)閉方的狀態(tài)變遷,也比被動(dòng)關(guān)閉方要復(fù)雜得多。
根據(jù) TCP/IP詳解 的介紹,TCP 協(xié)議也支持從服務(wù)端發(fā)起建立連接。但由于這種實(shí)現(xiàn)實(shí)際上非常罕見,這里就不作深入介紹了。
狀態(tài)變遷是 TCP 協(xié)議的一個(gè)重要知識(shí)點(diǎn),特別是對(duì) TIME_WAIT 狀態(tài)的理解,在后端技術(shù)面試中經(jīng)??疾臁?/p>
TIME_WAIT 狀態(tài)
TCP 主動(dòng)關(guān)閉方最終會(huì)進(jìn)入 TIME_WAIT 狀態(tài),并維持 2MSL 時(shí)長,為什么呢?
如上圖,假設(shè)四次揮手中最后一個(gè) ACK 分組在網(wǎng)絡(luò)中丟失了,會(huì)發(fā)生什么事情呢?這時(shí)被動(dòng)關(guān)閉的服務(wù)端會(huì)重傳 FIN 分組。因此主動(dòng)關(guān)閉方不能直接關(guān)閉,而應(yīng)該在 TIME_WAIT 狀態(tài)下維持一段時(shí)間,以便向重傳的 FIN 分組回復(fù) ACK 分組。否則對(duì)端仍會(huì)重傳 FIN 分組,并在重試若干次后放棄,這時(shí)連接只能異常關(guān)閉。
另一方面,一個(gè) TCP 連接由通信雙方的 IP 地址和端口組成的四元組唯一確定。如果主動(dòng)關(guān)閉方不在 TIME_WAIT 狀態(tài)下等待一段時(shí)間,而是快速關(guān)閉釋放資源,又會(huì)發(fā)生什么事情呢?這時(shí)原來的四元組有可能被新的連接復(fù)用,而舊連接重傳的 FIN 分組可能因網(wǎng)絡(luò)原因延遲到達(dá),最終對(duì)新連接產(chǎn)生沖突干擾。
那么,為什么是維持 TIME_WAIT 狀態(tài) 2MSL 時(shí)長呢?
MSL 是最大分組壽命( maximum segment lifetime )的簡(jiǎn)稱,即一個(gè) TCP 分組被丟棄前能夠在網(wǎng)絡(luò)中存在的最長時(shí)間。這個(gè)時(shí)間肯定是有限的,因?yàn)樨?fù)責(zé)傳輸 TCP 分組的 IP 包中有限制存活時(shí)間的 TTL 字段。由于 IP 包對(duì)存活時(shí)間的限制是基于跳數(shù)的,因此兩者不對(duì)等。不同的網(wǎng)絡(luò)協(xié)議棧實(shí)現(xiàn),MSL的取值也不盡相同:30秒、1分鐘或2分鐘都有。
將 TIME_WAIT 狀態(tài)維持 2MSL 時(shí)長是出于這樣的考慮:
假設(shè)最后一個(gè) ACK 分組剛好在存活時(shí)間耗盡前到達(dá)對(duì)端主機(jī),這時(shí)已經(jīng)過了 MSL 時(shí)間。對(duì)端收到 ACK 后,就會(huì)立即關(guān)閉連接,不可能再發(fā)送 FIN 分組。但如果對(duì)端在收到 ACK 前剛剛重傳了 FIN 分組,就必須再經(jīng)過 MSL 時(shí)間才能保證 FIN 分組從網(wǎng)絡(luò)中消失。因此,連接必須維持 TIME_WAIT 狀態(tài) 2MSL 時(shí)間后才能釋放,否則就可能對(duì)潛在的新連接造成干擾。
你可能會(huì)問,如果最后一個(gè) ACK 分組丟了,對(duì)端不是還會(huì)繼續(xù)重傳 FIN 分組嗎?不用再繼續(xù)等待 FIN 失效嗎?
是的,對(duì)端肯定會(huì)重傳 FIN 分組,而且通常 很快 就開始重傳。MSL 一般是幾十秒,而網(wǎng)絡(luò)往返時(shí)間要小得多,通常只是毫秒級(jí),TCP 重傳時(shí)間數(shù)量級(jí)也是差不多。因此,在 MSL 時(shí)間內(nèi),TCP 可以重傳 FIN 分組好幾次了!如果其中有一個(gè) FIN 分組可以到達(dá),TCP 會(huì)重置定時(shí)器,將 TIME_WAIT 狀態(tài)再維持 2MSL 時(shí)長。如果幾個(gè) FIN 分組都丟了,那再等下去也沒啥意義了!
- 如果最后一個(gè) ACK 分組可以到達(dá)對(duì)端,最多只需要等待 2MSL 時(shí)間即可保證網(wǎng)絡(luò)中沒有對(duì)端重傳的 FIN 分組;
- 如果最后一個(gè) ACK 分組丟失了,對(duì)端在 MSL 內(nèi)已經(jīng)重傳 好多次 了;
- 如果重傳的 FIN 分組有一個(gè)可以到達(dá)本端,TCP 回復(fù) ACK 后會(huì)重置定時(shí)器在 TIME_WAIT 繼續(xù)等待 2MSL 時(shí)長;
- 如果重傳的 FIN 分組都丟了,說明網(wǎng)絡(luò)質(zhì)量很差,再等下去也沒有意義了;
因此,主動(dòng)關(guān)閉方收到對(duì)端 FIN 分組后,必須在 TIME_WAIT 狀態(tài)等待 2MSL ,才能釋放連接。這樣既保證對(duì)重傳 FIN 分組的回復(fù),又保證重傳的 FIN 分組從網(wǎng)絡(luò)中消失,不會(huì)對(duì)復(fù)用四元組的新連接造成沖突干擾。
主動(dòng)關(guān)閉方一般是客戶端,并發(fā)一般不高,因此 TIME_WAIT 狀態(tài)基本不會(huì)造成任何影響。如果一個(gè)高并發(fā)服務(wù)(比如 Web 服務(wù))存在大量短連接,則可能留下很多 TIME_WAIT 狀態(tài)的連接。由于 TIME_WAIT 狀態(tài)套接字無法立即回收,它們將占用大量的系統(tǒng)資源,對(duì)服務(wù)的性能造成嚴(yán)重影響。
這時(shí),系統(tǒng)管理員可以選擇系統(tǒng)的 2MSL 時(shí)長適當(dāng)調(diào)短,加快 TIME_WAIT 連接的清理速度。此外,在 Linux 系統(tǒng)中,可以開啟 tcp_tw_recycle 和 tcp_tw_reuse 內(nèi)核選項(xiàng),以復(fù)用 TIME_WAIT 狀態(tài)的套接字。這些屬于 TCP 和系統(tǒng)調(diào)優(yōu)的范疇,后續(xù)有機(jī)會(huì)再專門展開介紹。