摘要
- TCP斷開連接
- TIME_WAIT
- TIME_WAIT優化
- TCP保活
- Sokcet編程
TCP斷開連接
TCP斷開連接,需要經歷四次揮手,通信的雙方都可主動斷開連接,斷開連接通信的雙方占用的資源將會被釋放。
- 客戶端會發送一個FIN報文給服務端,然后進入FIN_WAIT_1狀態
- 服務端在收到FIN報文后,會回復客戶端段一個ACK報文,然后進入CLOSED_WAIT狀態
- 客戶端在收到服務端的ACK報文以后會進入FIN_WAIT_2狀態
- 服務端在處理完歷史數據以后會發送FIN報文給客戶端,然后進入LAST_ACK狀態
- 客戶端在收到服務端的FIN報文以后,會發送一個ACK報文給服務端,然后進入TIME_WAIT狀態
- 服務器在收到ACK報文以后,就會真正的關閉連接,進入CLOSED狀態
- 客戶端在經過2MSL時間后,也會自動關閉連接進入CLOSED狀態
為什么回收需要四次
原因是客戶端在主動發起FIN報文以后僅表示客戶端不再主動發送數據了但是還可以接收數據。服務器在響應ACK報文以后,還有可能有數據還在處理且需要發送給客戶端,因此當服務器處理完這些數據以后才能發送FIN報文表示同意關閉連接。
因此服務端的ACK和FIN報文需要分開發送,揮手也就變成了4次。
什么是MSL和TTL
TTL是IP頭部中的一個字段,是指IP數據報可以經過的最大路由數,每經過一個路由器都需要減1,當TTL值為0時數據報就會被丟棄,同時發送ICMP報文給源主機。TTL的單位是路由跳數。
MSL是報文在網絡中存在的最長時間,超過該時間就會被丟棄。
為什么TIME_WAIT需要經歷2MSL后才可以變為CLOSED
網絡中存在的發送方數據包,首先需要發送給服務端,服務端在處理完以后又會將相應發送給客戶端,所以總共需要2個倍的時間。
2MSL的時間是從客戶端接收到FIN報文并且發送ACK報文時開始的。如果此時ACK報文沒有被服務端接收到觸發了服務端的超時重傳,客戶端又再次收到了FIN報文,那么2MSL將重新開始計時。
linux中默認一個MSL是30s,也就是說TIME_WAIT的時間是60s。
TIME_WAIT
為什么需要TIME_WAIT狀態
主動發起連接中斷的一方需要有TIME_WAIT狀態,主要是以下原因:
- 防止具有相同四元組的舊數據包被收到
- 保證最后一次ACK報文能被被動關閉連接的一方收到,也就是保證被動關閉連接的一方能被正確關閉。
防止舊連接的數據包被收到
假設沒有TIME_WAIT狀態,如果有相同的端口的TCP連接被服用后,上圖中被延遲SEQ=301的數據包抵達了客戶端,客戶端是有可能正常接收該報文的,此時就會產生數據錯亂現象。
但通過2MSL的等待時間,通信雙方的數據包都可以在網絡中消失,新的數據包一定是新連接的。
保證連接正確關閉
通過等待2MSL的時間確保最后一次ACK報文被被動斷開連接的一方收到,從而正常關閉。
上圖如果服務端沒有收到最后一個ACK報文會處于LAST_ACK狀態,如果此時客戶端發起了一個新的SYN報文請求建立連接,服務端會發送RST報文給客戶端,連接建立失敗。
但是通過等待2MSL的時間會解決上述問題,因為假設服務端沒有收到最后一次ACK報文,會觸發超時重傳重新發送FIN報文并等待新的ACK報文。客戶端在收到新的FIN報文時會重新發送ACK報文并刷新2MSL的計時,最終能夠保證服務端的連接能夠正常關閉。
TIME_WAIT過多的弊端
服務器如果有TIME_WAIT狀態的連接,說明TCP連接的斷開是由服務端發起的,此時如果TIME_WAIT的連接過多,將會出現以下問題:
- 內存資源占用
- 端口資源占用,假設端口被占滿,將無法建立新的連接
# 該參數用于指定開放的端口資源,默認是32768-61000
net.ipv4.ip_local_port_range
TIME_WAIT優化
TIME_WAIT的優化主要有以下幾種方式,每種方式都有利有弊:
- 打開net.ipv4.tcp_tw_reuse和net.ipv4.tcp_timestamps選項
- net.ipv4.tcp_max_tw_buckets
- 應用程序使用SO_LINGER,應用強制使用RST關閉
打開net.ipv4.tcp_tw_reuse和net.ipv4.tcp_timestamps選項
參數開啟以后,可以復用處于TIME_WAIT的Socket給新的連接使用。
tcp_tw_reuse的功能只能用于連接發起方,開啟該參數以后,在調用connect函數時,內核會隨機找一個time_wait超過1s的連接給新的連接復用。
net.ipv4.tcp_timestamp默認開啟,表示打開對TCP時間戳的支持。時間戳字段存儲在TCP頭部的選項字段中,用于記錄TCP發送方的時間戳和從對端接收到的最新時間戳。
net.ipv4.tcp_max_tw_buckets
當系統中的TIME_WAIT的連接數超過該項的值時,系統那個會將后面TIME_WAIT的連接重置,不推薦使用。
程序使用SO_LINGER
通過設置Sokcet的一些選項,來影響close方法的一些行為。
如果SO_LINGER中的onoff為非0,并且linger為0,調用close方法以后會立即發送一個RST報文給對方,TCP連接會直接跳過四次握手關閉。也過于暴力不推薦。
TCP保活機制
在某個時間段內,如果TCP連接上無任何活動,TCP保活機制開始生效,每隔一段時間就會發送一個探測報文,如果連續幾個探測報文都沒有收到響應,則認為TCP連接已死,系統內核會將錯誤信息通知給應用程序。
# 用于控制保活時間,如果7200s內沒有活動,則會啟動保活機制
net.ipv4.tcp_keepalive_time=7200
# 保活機制每次檢測間隔為75s
net.ipv4.tcp_keepalive_intvl=75
# 如果9次探測無響應,則認為對端不可答,中斷本次連接
net.ipv4.tcp_keepalive_probes=9
上述三個都是Linux中的默認值,也就是說Linux操作系統中至少經過2小時11分15秒才可以發現一個死亡連接。
Socket編程
public ServerSocket(int port, int backlog) throws IOException {
this(port, backlog, null);
}
JAVA中的ServerSokcet的初始化方法中有一個backlog參數,該參數在Linux2.2以前代表SYN隊列大小,但是在Linux 2.2以后就是全連接隊列的大小(accept隊列的大小)。
- 半連接隊列(SYN隊列):接收SYN請求,處于SYN_RCVD狀態的連接
- 全連接隊列(Accept隊列):完成三次握手處于ESTABLISHED狀態的連接
Socket的一些連接操作對應的tcp連接步驟
- Socket在調用connect方法時,會發送SYN包給服務端,服務端會接收到到SYN報文,并且服務端會半連接隊列里初始化一個連接。
- 服務端在處理完以后會發送ACK+SYN報文給客戶端,客戶端收到以后切實是就是connect方法的返回,同時客戶端也需要對服務端的SYN報文進行應答。
- 服務端收到ACK報文以后,半連接隊里的連接會被轉移到全連接隊列中,此時accept方法會成功拿到連接并生成一個Socket(這個就是傳輸時的Socket,不是監聽Socket)。
close方法對應的TCP四次揮手
- 客戶端調用close方法,會發送一個FIN報文給服務端
- 服務端收到FIN報文時,TCP協議棧會為該包插入一個文件結束符EOF到接收緩沖區,應用程序可以通過read方法獲取到該文件結束符。**EOF會被放在所有的數據之后。**服務端會進入CLOSED_WAIT狀態。
- 服務端處理完所有的數據以后,會讀取到EOF,此時會調用close方法關閉Socket,然后發送一個FIN包進入LAST_ACK狀態。
- 后面的其實就是TCP最終斷開連接。