TCP的連接&斷連&狀態轉移過程

本篇文章參考Linux高性能服務器編程(作者:遊雙)一書的第三章。

首先來看下TCP的連接和斷連:


上圖示意了TCP連接的三次握手和斷連時的四次握手。

首先在連接時,客戶端向服務端發送一個SYN(序號爲x)的同步報文,然後服務端對客戶端發來的報文進行確認,向客戶端發送ACK報文(x+1表示對客戶端發來的序號爲x的報文進行確認)和SYN報文(序號爲y)。注意這裏ACK報文和SYN報文是同一個報文,只是將此報文的TCP頭部的ACK位和SYN爲都置位。然後客戶端在對服務器端發送來的報文進行確認,向服務器段發送ACK報文(y+1表示對服務器端序號爲y的SYN報文進行確認),而SEQ=x+1表示這個報文是客戶端發送的序號爲x+1的報文。至此TCP通過三次握手,建立連接。

再來看TCP斷連的4次握手過程:

首先客戶端發送一個FIN終止連接報文(序號爲u),然後服務器端對客戶端發送來的序號爲u的報文予以確認,向客戶端發送ACK報文(u+1表示對對客戶端的序號爲u的報文予以確認)。這時TCP連接的這對套接字出於半關閉半連接的狀態。然後服務器端在向客戶端發送一個FIN終止連接報文(序號爲y),客戶端對服務器端的FIN報文予以確認返回ACK報文(y+1表示對服務器端的y號報文予以確認)。至此,TCP斷連的四次握手完成。

接下來我們來看一下TCP的狀態轉移過程,如下爲TCP狀態轉移過程圖:

                 

上面虛線表示典型的服務器端連接的狀態轉移,實線表示典型的客戶端連接的狀態轉移。CLOSED是一個假想的起始點,並不是一個實際的狀態。

我們先討論服務器的典型狀態轉移過程,此時我們說的連接狀態都是指該連接的服務器端的狀態。

服務器通過listen系統調用進入LISTEN狀態,被動等待客戶端連接,因此執行的是所謂的被動打開。服務器一旦監聽到某個連接請求(收到同步報文段),就將該連接放入內核等待隊列中,並向客戶端發送帶SYN標誌的確認報文段。此時該連接出於SYN_RCVD狀態。如果服務器成功地接收到客戶端發送回的確認報文段,則該連接轉移到ESTABLISHED狀態。ESTABLISHED狀態是連接雙發能夠進行雙向數據傳輸的狀態。

當客戶端主動關閉連接時(通過close或shutdown系統調用向服務器發送結束報文段),服務器通過返回確認報文段使連接進入CLOSE_WAIT狀態。這個狀態的含義很明確:等待服務器應用程序關閉連接。通常,服務器檢測到客戶端關閉連接後,也會立即給客戶端發送一個結束報文段來關閉連接。這將使連接轉移到LAST_ACK狀態,以等待客戶端對結束報文段的最後一次確認。一旦確認完成,連接就徹底關閉了。

下面討論客戶端的典型狀態轉移過程,此時我們說的連接狀態都是指該連接的客戶端的狀態。

客戶端通過connect系統調用主動與服務器建立連接。connect系統調用首先給服務器發送一個同步報文段,使連接轉移到SYN_SENT狀態。此後,connect系統調用可能因爲如下兩個原因失敗返回。

1、如果connect連接的目標端口不存在(未被任何進程監聽),或者該端口仍被處於TIME_WAIT狀態的連接所佔用(見下文),則服務器將給客戶端發送一個復位報文段,connect調用失敗。

2、如果目標端口存在,但connect在超時時間內未收到服務器的確認報文段,則connect調用失敗。

connect調用失敗將使連接立即返回到初始的CLOSED狀態。如果客戶端成功收到服務器的同步報文段和確認,則connect調用成功返回,連接轉移至ESTABLISHED狀態。


當客戶端執行主動關閉時,它將向服務器發送一個結束報文段,同時連接進入FIN_WAIT_1狀態。若此時客戶端收到服務器專門用於確認目的的確認報文段,則連接轉移至FIN_WAIT_2狀態。當客戶端出於FIN_WAIT_2狀態時,服務器出於CLOSE_WAIT狀態,這一對狀態時可能發生半關閉的狀態。此時如果服務器也關閉連接(發送結束報文段),則客戶端將給予確認並進入TIME_WAIT狀態。

上圖還給出了客戶端從FIN_WAIT_1狀態直接進入TIME_WAIT狀態的一條線路(不經過FIN_WAIT_2狀態),前提是處於FIN_WAIT_1狀態的服務器直接接收到帶確認信息的結束報文段(而不是先收到確認報文段,再收到結束報文段)。既是確認報文段和結束報文段同在一個報文段中發送(實質就是同時置此報文段TCP頭部的ACK和FIN位)。

前面說過,處於FIN_WAIT_2狀態的客戶端需要等待服務器發送結束報文段,才能轉移至TIME_WAIT狀態,否則他將一直停留在這個狀態。如果不是爲了在半關閉狀態下繼續接受數據,連接長時間地停留在FIN_WAIT_2狀態並無益處。連接停留在FIN_WAIT_2狀態的情況可能發生在:客戶端執行半關閉後,未等到服務器關閉連接就強行退出了。此時客戶端連接由內核來接管,可稱之爲孤兒連接(和孤兒進程類似)。Linux爲了防止孤兒連接長時間存留在內核中,定義了兩個內核變量:/proc/sys/net/ipv4/tcp_max_orphans和/proc/sys/net/ipv4/tcp_fin_timeout。前者指定內核能接管的孤兒連接數目,後者指定孤兒連接在內核中生存的時間。

至此,我們簡單地討論了服務器和客戶端程序的典型TCP狀態轉移路線。對應於下圖所示的TCP連接的建立與斷開過程,客戶端與服務器端的狀態轉移如下圖所示:




TIME_WAIT狀態

從上圖來看,客戶端連接在收到服務器的結束報文段(TCP報文段6)之後,並沒有直接進入CLOSED狀態,而是轉移到TIME_WAIT狀態。在這個狀態,客戶端連接要等待一段長爲2MSL(Maximum Segment Life,報文段最大生存時間)的時間,才能完全關閉。MSL是TCP報文段在網絡中的最大生存時間,標準文檔RFC1122的建議值是2min。

TIME_WAIT狀態存在的原因有兩點:

1、可靠的終止TCP連接

2、保證讓遲來的TCP報文段有足夠的時間被識別並丟棄。

第一個原因很好理解。假設上圖中用於確認服務器結束報文段6的TCP報文段7丟失,那麼服務器將重發結束報文段。因此客戶端需要停留在某個狀態以處理重複收到的結束報文段(即向服務器發送確認報文段)。否則,客戶端將以復位報文段來回應服務器,服務器則認爲這是一個錯誤,因爲它期望的是一個像TCP報文段7那樣的確認報文段。

在Linux系統上,一個TCP端口不能被同時打開多次(兩次及以上)。當一個TCP連接出於TIME_WAIT狀態時,我們將無法立即使用該連接佔用着的端口來建立一個新連接。反過來思考,如果不存在TIME_WAIT狀態,則應用程序能夠建立一個和剛關閉的連接相似的連接(這裏說的相似,是指它們具有相同的IP地址和端口號)。這個新的、和原來相似的連接被稱爲原來的連接的化身。新的化身可能接受到屬於原來的連接的、攜帶應用程序數據的TCP報文段(遲到的報文段),這顯然是不應該發生的。這就是TIME_WAIT狀態存在的第二個原因。

另外,因爲TCP報文段的最大生存時間是MSL,所以堅持2MSL時間的TIME_WAIT狀態能夠確保網絡上兩個傳輸方向上尚未被接受到的、遲到的TCP報文段都已經消失(被中轉路由器丟棄)。因此,一個連接的新的化身可以在2MSL時間之後安全的建立,而絕對不會接受到屬於原來連接的應用程序數據,這就是TIME_WAIT狀態要持續2MSL時間地原因。

有時候我們希望避免TIME_WAIT狀態,因爲當程序退出後,我們希望能夠立即重啓它。但由於處在TIME_WAIT狀態的連接還佔用着端口,程序將無法啓動(直到2MSL超時時間結束)。

對於客戶端程序來說,我們通常不用擔心上面描述的重啓問題。因爲客戶端一般使用系統自動分配的臨時端口號來建立連接,而由於隨機性,臨時端口號一般和程序上一次使用的端口號(還處於TIME_WAIT狀態的那個連接使用的端口號)不同,所以客戶端程序一般可以立即重啓。

但如果是服務器主動關閉連接後異常終止,則因爲它總是使用同一個知名服務端口號,所以連接的TIME_WAIT狀態將導致它不能立即重啓。不過,我們可以通過socket選項SO_REUSEADDR來強制進程立即使用出於TIME_WAIT狀態的連接佔用的端口。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章