主動關閉鏈接的一方,在四次揮手最後一次發送 ACK 後,進入 TIME_WAIT 狀態。在這個狀態裏,主動關閉鏈接一方等待 2MSL(Maximum Segment Life,報文段最大生存時間,在RFC793 中定義爲 2 min,而在 Linux 中定義爲 30s),若這段時間內未收到被動關閉一方重發的 FIN,則由 TIME_WAIT 狀態轉到 CLOSED 狀態。html
祭上狀態機圖:node
在這裏爲了討論方便,假設主動關閉鏈接的一方均爲本地客戶端,被動關閉鏈接的一方均爲服務端,以客戶端與服務端 TCP 狀態的變化來討論。linux
爲何 TCP 須要設置 TIME-WAIT 狀態等待 2MSL 才能轉到 CLOSED 狀態關閉鏈接呢?shell
避免在同一四元組(源地址、源端口、目的地址、目的端口)上的新鏈接收到舊鏈接的數據。
以下圖所示,服務端第一次發送的序號爲 3 的數據包因延時未送達客戶端,服務端重發第二次序號爲 3 的數據包後客戶端接收到並主動斷開鏈接。編程
在很短期內,客戶端從新向服務端發起鏈接,這時服務端發送序號 一、序號 2 的數據給客戶端,但同時客戶端也收到了在網絡上延時到達的服務端第一次發送的序號爲 3 的數據包。緩存
RFC793 中描述了 ISN 每 4 微秒會自增 1,達到 2^32 後又從 0 開始。這樣周始往復,一個 ISN 的週期大約是 4.55 個小時。因此雖然 TCP 每次創建鏈接時的 SYN 序列號都不會相同,但若若是在接收窗口很大的狀況下,快速從新創建的鏈接使用的序列號可能會有一部分與舊鏈接使用過的序列號重合,所以新鏈接誤接收舊鏈接相同序列號的數據包是有機率發生的。服務器
客戶端經過 TIME-WAIT 等待 2MLS 的時間能夠避免這個問題:1)收到的延遲數據包被丟棄;2)2MLS 的時間會讓 ISN 與舊鏈接使用過的序列號重合範圍減少甚至不重合。TIME-WAIT 爲新鏈接準備了時間緩衝帶,舊鏈接的數據包與新鏈接的數據包所以有足夠的界限。網絡
服務端若是沒有收到四次揮手中的最後一個 ACK,將會一直處於 LAST-ACK 狀態,並一直重傳 FIN 報文,有三種可能的狀況發生:數據結構
放棄重傳 FIN,並移除該鏈接;負載均衡
收到 ACK,狀態轉爲 CLOSED,並正常關閉鏈接;
收到 RST,並移除該鏈接。
客戶端經過 TIME-WAIT 等待 2MLS 時間確保服務端正確關閉鏈接。如若在 TIME-WAIT 收到服務端重傳的 FIN,說明最後發送的 ACK 在網絡中丟失了,須要重發 ACK 以確保服務端能收到 ACK 並正確關閉鏈接。這也是爲何 TIME-WAIT 等待時間是 2MLS 的緣由,若是服務端重傳 FIN,客戶端一定在 2MLS 期間內收到:即便服務端收到 ACK 再重傳 FIN, 這個過程也只須要 2MLS 時間。
TIME-WAIT 狀態雖好,可是當大量的鏈接處於 TIME-WAIT 狀態而未被及時關閉,它會致使如下問題:
TIME-WAIT 狀態在 Linux 下會持續 60s,在這 60s 內,不能創建相同相同四元組(源地址、源端口、目的地址、目的端口)的新鏈接。
在內核中,一個 TIME-WAIT 狀態的 socket 與三個結構體相關,而這些數據結構在內存中都佔用必定的空間:
struct tcp_timewait_sock
每當收到一個新的報文時,會在名爲 「TCP established」 的哈希表中查找這個鏈接。該哈希表中的每一個桶不只包含 TIME-WAIT 狀態的鏈接鏈表,還包含其它正常狀態的鏈接鏈表。其中,TIME-WAIT 狀態的鏈表元素數據結構是 tcp_timewait_sock(168 bytes),而其它正常狀態的鏈表元素結構是 struct tcp_sock。
struct tcp_timewait_sock { struct inet_timewait_sock tw_sk; u32 tw_rcv_nxt; u32 tw_snd_nxt; u32 tw_rcv_wnd; u32 tw_ts_offset; u32 tw_ts_recent; long tw_ts_recent_stamp; }; struct inet_timewait_sock { struct sock_common __tw_common; int tw_timeout; volatile unsigned char tw_substate; unsigned char tw_rcv_wscale; __be16 tw_sport; unsigned int tw_ipv6only : 1, tw_transparent : 1, tw_pad : 6, tw_tos : 8, tw_ipv6_offset : 16; unsigned long tw_ttd; struct inet_bind_bucket *tw_tb; struct hlist_node tw_death_node; };
struct hlist_node
struct inet_timewait_sock 的數據成員 tw_death_node,用來跟蹤 TIME-WAIT 狀態的鏈接的存活時間,存活時間越長排在鏈表越靠後的位置。
struct inet_bind_socket
綁定端口的哈希表,保存本地被綁定的端口及相關聯的參數,用於:1)判斷是否能夠在給定的端口上綁定;2)尋找未被綁定的可用的端口。
哈希表的每一個元素數據結構爲 inet_bind_socket(48 bytes)。
在 CPU 使用上,查找一個可用的本地端口的代價可能有一丟丟大。這項工做由函數 inet_csk_get_port() 完成:鎖住並迭代本地的全部端口,直到找到一個未使用的端口。
具體來講:
客戶端設置 net.ipv4.ip_local_port_range 來擴充客戶端端口範圍;
客戶端使用更多的 IP 地址,例如,在負載均衡器上配置更多的 IP;
服務端監聽更多的端口,如 81,82,83 等;
服務端監聽更多的 IP 地址。
默認狀況下,應用程序調用 close() 關閉 socket 後會當即返回,TCP 模塊會把發送緩存中的殘餘的數據繼續發送完,最終轉到 TIME-WAIT 狀態。
SO_LINGER 是 socket 的一個選項,當 socket 被 close 時,該選項控制 socket 是否延遲關閉,以及如何處理髮送緩存中的殘餘數據。
經過調用 setsockopt 來設置 socket 選項:
#include <sys/socket.h> int setsockopt( int sockfd, int level, int option_name, const void* option_value, socklen_t option_len );
sockfd 參數指定被操做的目標 socket,level 參數指定要操做哪一個協議的選項,好比 IPv四、IPv六、TCP 等,option_name 參數則指定選項的名字。option_value 和 option_len 參數分別是被操做選項的值和長度。詳情見 setsockopt。
設置 SO_LINGER 選項的值時,咱們須要給 setsockopt 傳遞一個 linger 類型的結構體,其定義以下:
#include <sys/socket.h> struct linger { int l_onoff; /* 開啓:非 0,關閉:0 */ int l_linger; /* 延遲時間 */ };
根據 linger 結構體中兩個成員變量的不一樣值,close() 會產生以下三種行爲之一:
l_onff 爲 0:SO_LINGER 關閉,close 用默認行爲來關閉 socket;
l_onff 非 0:SO_LINGER 開啓,
l_linger == 0:客戶端應用程序調用 close() 後當即返回,發送緩存中的殘餘數據被丟棄,同時發送一個 RST 給服務端來異常終止當前鏈接;
l_linger > 0:
socket 是阻塞的:客戶端應用程序調用 close() 後等待 l_linger 的時間,直到發送完全部緩存中的殘餘數據並獲得遠端的確認。若是這段時間內尚未發送完殘餘數據,close() 返回 -1 並設置 errno 爲 EWOULDBLOCK;
socket 是非阻塞的:客戶端應用程序調用 close() 後當即返回,根據 close() 的返回值與 error 來判斷殘餘數據是否已經發送完畢。
所以,開啓 SO_LINGER 並將 l_linger 設置爲 0 時,服務端會收到 RST 並關閉鏈接。至關於跳過 TIME_WAIT 狀態直接關閉服務端的鏈接。可是,SO_LINGER 並無解決新鏈接收到舊鏈接數據包的問題。
開啓 SO_REUSEADDR 選項或者配置 net.ipv4.tcp_tw_reuse 爲 1 後,Linux 將能夠複用處於 TIME-WAIT 狀態的鏈接。前面咱們說到,TIME-WAIT 狀態存在的目的一是爲了不在新鏈接上收到舊鏈接的數據,二是爲了確保被動關閉方正確關閉鏈接,那麼咱們開啓 SO_REUSEADDR 複用鏈接不是一切回到原點了嗎?這一切都是 TCP 時間戳選項的功勞。
RFC 1323 描述了一套如何在大寬帶高速網絡下提高性能的 TCP 擴展,在這其中,新定義一個新的 TCP 時間戳選項:
Kind
1 字節,固定爲 8。
Length
1 字節,固定爲 10。
Timestamp Value (TSval)
4 字節, TCP 發送此選項時的當前時間戳。
TImestamp Echo Reply (TSecr)
4 字節,僅在 ACK 中有效,把收到的 TSval 回填到 TSecr 中發回給遠端。當此報文不是 ACK,即 TSercr 無效時,TSecr 的值必須是 0 。
咱們來看時間戳如何接手 TIME-WAIT 的問題:
1)避免在新鏈接上收到舊鏈接的數據
舊鏈接的數據會由於時間戳過於老舊而被丟棄;
2)確保服務端正確關閉鏈接
一旦客戶端用新的鏈接替代了 TIME-WAIT 狀態的鏈接,客戶端發出 SYN 報文後服務端重傳 FIN 報文(由於時間戳的關係,服務端識別出是新的鏈接,客戶端的 SYN 報文被忽略)。由於客戶端當前處於 SYN-SENT 狀態,因此會回覆 RST,這使得服務端能正確脫離 LAST-ACK 狀態並關閉鏈接。這以後,SYN 初始化報文會重發,從新進入新鏈接的創建流程:
net.ipv4.tcp_tw_recycle 配置爲 1 會開啓系統對 TIME-WAIT 狀態的 socket 的快速回收。
net.ipv4.tcp_tw_recycle 一樣利用 TCP 的時間戳選項來優化 TIME-WAIT:Linux 每收到一個遠端(IP)的數據包,都記錄它的時間戳。當處於 TIME-WAIT 狀態的 socket 收到的同一遠端的數據包時間戳小於記錄值,Linux 直接丟棄該數據包並回收 socket。
可是,net.ipv4.tcp_tw_recycle 並不被推薦(Linux 從 4.12 內核版本開始移除了 tcp_tw_recycle 配置),它可能會致使不少難以排查的古怪問題。特別是服務器或者客戶端在 NAT 網絡中,多個服務器或客戶端共用 NAT 設備的時間戳,數據包可能會被丟棄。
表示系統同時保持 TIME-WAIT 套接字的最大數量,若是超過這個數字,TIME-WAIT 套接字將馬上被清除並打印警告信息。默認值爲180000。