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