socket close和shutdown的區別,TIME_WAIT和CLOSE_WAIT,SO_REUSEADDR

一匹夫發表於2024-09-21

TCP主動關閉連線

appl: close(), --> FIN FIN_WAIT_1 //主動關閉socket方,呼叫close關閉socket,發FIN
<-- ACK FIN_WAIT_2 //對方作業系統的TCP層,給ACK響應。然後給FIN
<-- FIN
--> ACK "TIME_WAIT" -- 2MSL timeout -->CLOSED //TIME_WAIT,防止ACK沒有給到對方。

TCP被動關閉連線

<-- FIN "CLOSE_WAIT" //被動方,收到對方的FIN,處於CLOSE_WAIT狀態
--> ACK //被動方的TCP層,給ACK響應
app2: close(), --> FIN LAST_ACK //被動方呼叫close,從CLOSE_WAIT轉到LAST_ACK 不調close,將一直在CLOSE_WAIT狀態。
<-- ACK --> CLOSED tcp是全雙工:: 因此close()關閉讀寫。 shutdown()可以選擇關閉讀或寫。 time_wait的時間會非常長,因此server儘量減少主動關閉連線。


int close(int sockfd); int shutdown(int sockfd, int howto); // howto: SHUT_RD, SHUT_WR, SHUT_RDWR
shutdown()函式的兩個作用:
close()將描述字的引用計數減1,當引用計數為0時,才關閉socket。
如fork()模式中,父程序在accept()返回後,fork()子程序,由子程序處理connfd,而父程序將close(connfd);但此時父程序的close()並不引發FIN。

shutdown()則不管socket的引用計數,直接發生FIN。
shutdown()可控制read/write兩個方向的管道。
SHUT_RD shutdown(sockfd, SHUT_RD);後,來自對端的資料都被確認,然後悄然丟棄。 SHUT_WR half close狀態。

close()引發的4次互動:(這裡的close是client發起的)

client server
FIN_WAIT_1 ---- FIN M ------> (Server端作業系統的TCP層響應ACK包)
<---- ACK M+1---- CLOSE_WAIT FIN_WAIT_2 (這裡必須呼叫close,才能從CLOSE_WAIT到LAST_ACK)
<------ FIN N ----- LAST_ACK TIME_WAIT (TIME_WAIT有一個重要的作用就是防止最後一個ACK丟失)
------- ACK N+1 ----------> CLOSE
TIME_WAIT 是主動關閉連結時形成的,等待2MSL時間,約4分鐘。

主要是防止最後一個ACK丟失。 由於time_wait的時間會非常長,因此server端應儘量減少主動關閉連線

CLOSE_WAIT是被動關閉連結是形成的 ,

按狀態機,我方收到FIN,則由TCP實現傳送ACK,因此進入CLOSE_WAIT狀態。

但如果我方不執行close(),就不能由CLOSE_WAIT遷移到LAST_ACK,則系統中會存在很多CLOSE_WAIT狀態的連線。

此時,可能是系統忙於處理讀、寫操作,而未將已收到FIN的連線,進行close。此時,recv/read已收到FIN的連線socket,會返回0。

大量TIME_WAIT和CLOSE_WAIT的存在,會產生怎樣的影響?

核心維護更多的狀態。收到ip包,做hash運算,hlist衝突的機率更大。

1. 首先明確下什麼是2MSL: TCP四次揮手斷開連線時,主動斷開連線的一方(這裡我們稱為客戶端A)在收到對端(這裡我們稱為服務端B)傳送的FIN包後,會立馬傳送ACK響應包並等待一段時間以確保自己傳送的ACK包能夠成功透過網路傳輸到達服務端B端,從而幫助B端正常斷開連線(B服務端只有收到自己發出的FIN包對應的ACK包後,才能正常釋放資源斷開連線),這一等待時間的時長為2MSL,是從客戶端A發出ACK包開始計算的,如果A在傳送完ACK包後又收到了B端傳送的新的FIN包,則會再次傳送新的ACK包並重新計時(B端可能會因為超時或丟包等各種原因重新傳送FIN包);
2. 其次明確下什麼是MSL:MSL 即 Maximum Segment Lifetime,它是任何 TCP segment 在網路上存在的最長時間,超過這個時間的 TCP 報文就會被丟棄,RFC793定義了MSL為2分鐘,但這完全是從工程上來考慮的,對於現在的網路,不同作業系統的TCP實現,可以根據具體網路情況配置使用更小的MSL;

3. 再次明確下為什麼TCP第四次揮手需要等待2MSL:客戶端A等待2MSL可以確保客戶端A和服務端B之間的資料包可以完成一個完整的來回的傳輸即 round trip,所以如果A先前傳送的ACK包因為某些原因在網路上丟失了,服務端B在超時沒有收到客戶端A的ACK包後會重新傳送FIN包,2MSL的等待時間能夠確保客戶端A收到服務端B傳送的新的FIN包以傳送新的ACK包並重新計時;

4. 最後說下如何在LINUX作業系統中檢視和配置MSL: 其實在LINUX作業系統中並沒有直接配置 MSL 而是配置了 tcp_fin_timeout,由於 tcp_fin_timeout=2MSL,所以我們可以檢視tcp_fin_timeout並據此推斷MSL: 可以透過命令 sysctl net.ipv4.tcp_fin_timeout 檢視 tcp_fin_timeout,可以透過命令 sysctl -w net.ipv4.tcp_fin_timeout=30 修改 tcp_fin_timeout;

簡單概括下,因為網路是不可靠的,資料包在傳輸過程中可能丟失可能超時可能亂序,所以TCP為了在邏輯的虛擬的連線的基礎上提供可靠性,在四次揮手斷開連線時主動斷開連線的一方需要2MSL的等待時間(當然三次握手和四次揮手,以及連線過程中的確認/累計確認/選擇確認等機制,也都是這個原因)

TCP四次揮手也遵循相似的套路。

主動斷開的一側為A,被動斷開的一側為B。

第一個訊息:A發FIN

第二個訊息:B回覆ACK

第三個訊息:B發出FIN

此時此刻:B單方面認為自己與A達成了共識,即雙方都同意關閉連線。

此時,B能釋放這個TCP連線佔用的記憶體資源嗎?不能,B一定要確保A收到自己的ACK、FIN。

所以B需要靜靜地等待A的第四個訊息的到來:

第四個訊息:A發出ACK,用於確認收到B的FIN

當B接收到此訊息,即認為雙方達成了同步:雙方都知道連線可以釋放了,此時B可以安全地釋放此TCP連線所佔用的記憶體資源、埠號。

所以被動關閉的B無需任何wait time,直接釋放資源。

但,A並不知道B是否接到自己的ACK,A是這麼想的:

1)如果B沒有收到自己的ACK,會超時重傳FiN

那麼A再次接到重傳的FIN,會再次傳送ACK

2)如果B收到自己的ACK,也不會再發任何訊息,包括ACK

無論是1還是2,A都需要等待,要取這兩種情況等待時間的最大值,以應對最壞的情況發生,這個最壞情況是:

去向ACK訊息最大存活時間(MSL) + 來向FIN訊息的最大存活時間(MSL)。

這恰恰就是2MSL( Maximum Segment Life)。

等待2MSL時間,A就可以放心地釋放TCP佔用的資源、埠號,此時可以使用該埠號連線任何伺服器。

為何一定要等2MSL?

如果不等,釋放的埠可能會重連剛斷開的伺服器埠,這樣依然存活在網路裡的老的TCP報文可能與新TCP連線報文衝突,造成資料衝突,

為避免此種情況,需要耐心等待網路老的TCP連線的活躍報文全部死翹翹,2MSL時間可以滿足這個需求(儘管非常保守)!

一般來說,一個埠釋放後會等待兩分鐘之後才能再被使用,SO_REUSEADDR是讓埠釋放後立即就可以被再次使用。

SO_REUSEADDR用於對TCP套接字處於TIME_WAIT狀態下的socket,才可以重複繫結使用。server程式總是應該在呼叫bind()之前設定SO_REUSEADDR套接字選項。TCP,先呼叫close()的一方會進入TIME_WAIT狀態

一個套接字由相關五元組構成,協議、本地地址、本地埠、遠端地址、遠端埠。SO_REUSEADDR 僅僅表示可以重用本地本地地址、本地埠,整個相關五元組還是唯一確定的。所以,重啟後的服務程式有可能收到非期望資料。必須慎重使用SO_REUSEADDR 選項。

我們假設是客戶執行主動關閉並進入 TIME_WAIT ,這是正常的情況,因為伺服器通常執行被動關閉,不會進入 TIME_WAIT 狀態。這暗示如果我們終止一個客戶程式,並立即重新啟動這個客戶程式,則這個新客戶程式將不能重用相同的本地埠。這不會帶來什麼問題,因為客戶使用本地埠,而並不關心這個埠號是什麼。然而,對於伺服器,情況就有所不同,因為伺服器使用周知埠。如果我們終止一個已經建立連線的伺服器程式,並試圖立即重新啟動這個伺服器程式,伺服器程式將不能把它的這個周知埠賦值給它的端點,因為那個埠是處於 2MSL 連線的一部分。在重新啟動伺服器程式前,它需要在 1~4 分鐘。這就是很多網路伺服器程式被殺死後不能夠馬上重新啟動的原因(錯誤提示為“ Address already in use ”)。

相關文章