1. 程式人生 > >TCP四次揮手和TIME_WAIT

TCP四次揮手和TIME_WAIT



FIN_WAIT_1 : FIN_WAIT_1和FIN_WAIT_2狀態的真正含義都是表示等待對方的FIN報文。而這兩種狀態的區別是: FIN_WAIT_1狀態實際上是當SOCKET在ESTABLISHED狀態時,它想主動關閉連線,向對方傳送了FIN報文,此時該SOCKET即進入到FIN_WAIT_1狀態。而當對方迴應ACK報文後,則進入到FIN_WAIT_2狀態,當然在實際的正常情況下,無論對方何種情況下,都應該馬上回應ACK報文,所以FIN_WAIT_1狀態一般是比較難見到的,而FIN_WAIT_2狀態還有時常常可以用netstat看到。(主動方)

FIN_WAIT_2 :實際上FIN_WAIT_2狀態下的SOCKET,表示半連線 ,也即有一方要求close連線,但另外還告訴對方,我暫時還有點資料需要傳送給你(ACK資訊),稍後再關閉連線。

TIME_WAIT: 表示收到了對方的FIN報文,併發送出了ACK報文 ,就等2MSL後即可回到CLOSED可用狀態了。如果FIN_WAIT_1狀態下,收到了對方同時帶FIN標誌和ACK標誌的報文時,可以直接進入到TIME_WAIT狀態,而無須經過FIN_WAIT_2狀態。(主動方)

CLOSING(比較少見) : 這種狀態比較特殊,實際情況中應該是很少見。正常情況下,當你傳送FIN報文後,按理來說是應該先收到(或同時收到)對方的 ACK報文,再收到對方的FIN報文。但是CLOSING狀態表示你傳送FIN報文後,並沒有收到對方的ACK報文,反而卻也收到了對方的FIN報文。如果雙方几乎在同時close一個SOCKET的話,那麼就出現了雙方同時傳送FIN報文的情況,也即會出現CLOSING狀態,表示雙方都正在關閉SOCKET連線。

CLOSE_WAIT : 表示在等待關閉。當對方close一個SOCKET後傳送FIN報文給自己,你係統毫無疑問地會迴應一個ACK報文給對方,此時則進入到CLOSE_WAIT狀態。接下來呢,實際上你真正需要考慮的事情是察看你是否還有資料傳送給對方,如果沒有的話,那麼你也就可以 close這個SOCKET,傳送FIN報文給對方,也即關閉連線。所以你在CLOSE_WAIT狀態下,需要完成的事情是等待你去關閉連線。

LAST_ACK: 這個狀態還是比較容易好理解的,它是被動關閉一方在傳送FIN報文後,最後等待對方的ACK報文。當收到ACK報文後,也即可以進入到CLOSED可用狀態了。

CLOSED: 表示連線中斷。

為什麼需要四次揮手?

那可能有人會有疑問,在tcp連線握手時為何ACK是和SYN一起傳送,這裡ACK卻沒有和FIN一起傳送呢。原因是因為tcp是全雙工模式,接收到FIN時意味將沒有資料再發來,但是還是可以繼續傳送資料(解決全雙工問題)。

TIME_WAIT

TIME_WAIT狀態會持續2MSL的時間才會轉換到CLOSE狀態,一般是1-4分鐘(現在對於linux是30秒)。MSL(最大分段生存期):指明TCP報文在Internet上最長生存時間。

只有主動關閉的一方才會進入TIME_WAIT狀態。那麼埠不夠用就是檔案描述符不夠用了,因為檔案描述符只有在從TIME_WAIT狀態轉換到CLOSE狀態後才會真正被系統收回。

問題


如果執行主動關閉的一方HOST1不進入到TIME_WAIT狀態就關閉連線那會發生什麼呢?

當重傳的FIN訊息到達時,因為TCP已經不再有連線的資訊了,所以就用RST(重新啟動)訊息應答,導致HOST2進入錯誤的狀態而不是有序終止狀態(如果主動關閉的一方又開啟了一個新的連結,則重發的FIN會將新連線給關閉掉),如果傳送最後ACK訊息的一方HOST1處於TIME_WAIT狀態並仍然記錄著連線的資訊,它就可以正確的響應HOST2的FIN訊息了。(最後一個ACK可能丟失並導致HOST2重新發送FIN訊息,導致老連線的包會干擾新連線

TIME_WAIT佔用的資源

1、TW會佔用記憶體,但是佔用記憶體很小(1W的TW連線佔用1M左右)

2、對CPU也是有影響的,比如TW的埠太多,導致選擇可用埠時,需要很多次選擇才能成功;但這個影響也是很小的。

3、TW連線會佔用大量埠。

4、被佔用的是一個五元組:(協議,本地IP,本地埠,遠端IP,遠端埠)。對於Web 伺服器,協議是TCP,本地IP通常也只有一個,本地埠預設的80或者443。只剩下遠端 IP和遠端埠可以變了。如果遠端IP相同的話,就只有遠端埠可以變了。這個只有幾萬個(net.ipv4.ip_local_port_range),所以當同一客戶端向伺服器建立了大量連線之後,會耗盡可用的五元組導致問題。

TIME_WAIT優化

tcp_timestamps

只有該選項開啟了,tcp_tw_reuse和tcp_tw_recycle才能起作用。另外tcp_timestamps可以解決PAWS和計算RTT的問題。

tcp_tw_reuse

需要開啟tcp_timestamps時才有效。針對於一個連線,如果開啟了該開關,即便該連線處於TW狀態,收到SYN包之後,也能建立一條新的連線。該連線跟之前的TW連線5元組相同。但是需要滿足下面條件的其中一個:

1)初始化序列號比老的TW序列號大。

2)新來的連線的時間戳比老的TW的時間戳大。

tcp_tw_recycle

快速回收TW,應該是RTO時間內回收。需要開啟tcp_timestamps時才有效。

即使快速回收之後,也保留了一些資訊,保留的資訊有:對端IP、最後的對端過來的時間戳等。對於新來的連線,同時滿足下面3個條件時,連線會被拒絕;否則連線不會被拒絕:

1)來自該IP的TCP連線請求帶有時間戳資訊;

2)在MSL時間內,收到過該IP過來的資料;

3)新連線的時間戳小於儲存的TW的時間戳;

tcp_max_tw_buckets

這個是控制併發的TIME_WAIT的數量,預設值是50幾W,如果超限,那麼,系統會把多的給destory掉,然後在日誌裡打一個警告。

SO_LINGER

struct linger {

    int l_onoff; /* 0 = off, nozero = on */

    int l_linger; /* linger time */

};

1)設定 l_onoff為0,則該選項關閉,l_linger的值被忽略,等於核心預設情況,close呼叫會立即返回給呼叫者,如果可能將會傳輸任何未傳送的資料。

2)設定 l_onoff為非0,l_linger為0,則套介面關閉時TCP夭折連線,TCP將丟棄保留在套介面傳送緩衝區中的任何資料併發送一個RST給對方,而不是通常的四分組終止序列,這避免了TIME_WAIT狀態;(說明如果沒有資料被丟棄,也是正常的四次揮手;不是rst)

3)設定 l_onoff 為非0,l_linger為非0,當套介面關閉時核心將拖延一段時間(由l_linger決定)。

TCP關閉連線的方式

正常關閉

呼叫close()關閉socket、沒close但程序正常結束(當然這是不應該的做法)、程序core掉、在shell 命令列中kill掉程序,都可抽象成“正常”關閉。因為即使core掉,核心也會馬上幫應用程式回收(close)socket檔案描述符。

不正常關閉

客戶端崩潰了,此時肯定發不出FIN包了(當然啦,核心都沒機會幫應用程式回收資源了)。這種情況,伺服器端有如下兩種情況。

1、伺服器send資料,因為客戶端已經崩潰,伺服器收不到ACK自然會不停的重傳。Berkeley的重傳機制,重傳8次,相對第一次傳的15分鐘後仍沒收到ACK,則返回ETIMEDOUT或EHOSTUNREAC錯誤。如果伺服器不理會這個錯誤,再次呼叫send,則立馬返回Broken Pipe錯誤。

2、伺服器不發任何資料了,那只有靠應用層心跳檢測機制或Keepalive,來發覺TCP斷連了。

參考資料

https://zhuanlan.zhihu.com/p/29334504

https://zhuanlan.zhihu.com/p/32386693