天天看點

ZZ TCP狀态遷移 close_wait狀态

TCP狀态遷移

大家對netstat -a指令很熟悉,但是,你有沒有注意到STATE一欄呢,基本上顯示着established,time_wait,close_wait等,這些到底是 什麼意思呢,在這篇文章,我将會詳細的闡述。

大家很明白TCP初始化連接配接三次握手吧:發SYN包,然後傳回SYN/ACK包,再發ACK包,連接配接正式建立。但是這裡有點出入,當請求者收到SYS /ACK包後,就開始建立連接配接了,而被請求者第三次握手結束後才建立連接配接。但是大家明白關閉連接配接的工作原理嗎?關閉連接配接要四次握手:發FIN包,ACK 包,FIN包,ACK包,四次握手!!為什麼呢,因為TCP連接配接是全雙工,我關了你的連接配接,并不等于你關了我的連接配接。

用戶端TCP狀态遷移:

CLOSED->SYN_SENT->ESTABLISHED->FIN_WAIT_1->FIN_WAIT_2->TIME_WAIT->CLOSED

伺服器TCP狀态遷移:

CLOSED->LISTEN->SYN收到 ->ESTABLISHED->CLOSE_WAIT->LAST_ACK->CLOSED

當用戶端開始連接配接時,伺服器還處于LISTENING,

用戶端發一個SYN包後,他就處于SYN_SENT狀态,伺服器就處于SYS收到狀态,

然後互相确認進入連接配接狀态ESTABLISHED.

當用戶端請求關閉連接配接時,用戶端發送一個FIN包後,用戶端就進入FIN_WAIT_1狀态,等待對方的确認包,

伺服器發送一個ACK包給客戶,用戶端收到ACK包後結束FIN_WAIT_1狀态,進入FIN_WAIT_2狀态,等待伺服器發過來的關閉請求,

伺服器發一個FIN包後,進入CLOSE_WAIT狀态,

當用戶端收到伺服器的FIN包,FIN_WAIT_2狀态就結束,然後給伺服器端的FIN包給以一個确認包,用戶端這時進入TIME_WAIT,

當伺服器收到确認包後,CLOSE_WAIT狀态結束了,

這時候伺服器端真正的關閉了連接配接.但是用戶端還在TIME_WAIT狀态下,

什麼時候結束呢.我在這裡再講到一個新名詞:2MSL等待狀态,其實TIME_WAIT就是2MSL等待狀态,

為什麼要設定這個狀态,原因是有足夠的時間讓ACK包到達伺服器端,如果伺服器端沒收到ACK包,逾時了,然後重新發一個FIN包,直到伺服器收到ACK 包.

TIME_WAIT狀态等待時間是在TCP重新啟動後不連接配接任何請求的兩倍.

大家有沒有發現一個問題:如果對方在第三次握手的時候出問題,如發FIN包的時候,不知道什麼原因丢了這個包,然而這邊一直處在FIN_WAIT_2狀 态,而且TCP/IP并沒有設定這個狀态的過期時間,那他一直會保留這個狀态下去,越來越多的FIN_WAIT_2狀态會導緻系統崩潰.

上面我碰到的這個問題主要因為TCP的結束流程未走完,造成連接配接未釋放。現設用戶端主動斷開連接配接,流程如下:

Client 消息 Server  close() ------ FIN -------> FIN_WAIT1 CLOSE_WAIT <----- ACK ------- FIN_WAIT2  close() <------ FIN ------  TIME_WAIT LAST_ACK   ------ ACK ------->  CLOSED CLOSED

由于Server的Socket在用戶端已經關閉時而沒有調用關閉,

造成伺服器端的連接配接處在“挂起”狀态,而用戶端則處在等待應答的狀态上。

此問題的典型特征是:

一端處于FIN_WAIT2 ,而另一端處于CLOSE_WAIT.

不過,根本問題還是程式寫的不好,有待提高

-------------------------------------------------------------------------

CLOSE_WAIT,TCP的癌症,TCP的朋友。

CLOSE_WAIT狀态的生成原因

首先我們知道,如果我們的伺服器程式APACHE處于CLOSE_WAIT狀态的話,說明套接字是被動關閉的!

因為如果是CLIENT端主動斷掉目前連接配接的話,那麼雙方關閉這個TCP連接配接共需要四個packet:

Client ---> FIN ---> Server

Client <--- ACK <--- Server

這時候Client端處于FIN_WAIT_2狀态;而Server 程式處于CLOSE_WAIT狀态。

Client <--- FIN <--- Server

這時Server 發送FIN給Client,Server 就置為LAST_ACK狀态。

Client ---> ACK ---> Server

Client回應了ACK,那麼Server 的套接字才會真正置為CLOSED狀态。

Server 程式處于CLOSE_WAIT狀态,而不是LAST_ACK狀态,說明還沒有發FIN給Client,那麼可能是在關閉連接配接之前還有許多資料要發送或者其 他事要做,導緻沒有發這個FIN packet。

通常來說,一個CLOSE_WAIT會維持至少2個小時的時間。如果有個流氓特地寫了個程式,給你造成一堆的 CLOSE_WAIT,消耗你的資源,那麼通常是等不到釋放那一刻,系統就已經解決崩潰了。

隻能通過修改一下TCP/IP的參數,來縮短這個時間:修改tcp_keepalive_*系列參數有助于解決這個 問題。

解決這個問題的方法是修改系統的參數,系統預設逾時時間的是7200秒,也就是2小時, 這個太大了,可以修改如下幾個參數:

sysctl -w net.ipv4.tcp_keepalive_time=30 sysctl -w net.ipv4.tcp_keepalive_probes=2 sysctl -w net.ipv4.tcp_keepalive_intvl=2

然後,執行sysctl指令使修改生效。

連接配接程序是通過一系列狀态表示的,這些狀态有:

LISTEN,SYN-SENT,SYN-RECEIVED,ESTABLISHED,FIN-WAIT-1,FIN-WAIT-2,CLOSE-WAIT,CLOSING,LAST-ACK,TIME-WAIT和CLOSED

各個狀态的意義如下: 

LISTEN - 偵聽來自遠方TCP端口的連接配接請求; 

SYN-SENT -在發送連接配接請求後等待比對的連接配接請求; 

SYN-RECEIVED - 在收到和發送一個連接配接請求後等待對連接配接請求的确認; 

ESTABLISHED- 代表一個打開的連接配接,資料可以傳送給使用者; 

FIN-WAIT-1 - 等待遠端TCP的連接配接中斷請求,或先前的連接配接中斷請求的确認;

FIN-WAIT-2 - 從遠端TCP等待連接配接中斷請求; 

CLOSE-WAIT - 等待從本地使用者發來的連接配接中斷請求; 

CLOSING -等待遠端TCP對連接配接中斷的确認; 

LAST-ACK - 等待原來發向遠端TCP的連接配接中斷請求的确認; 

TIME-WAIT -等待足夠的時間以確定遠端TCP接收到連接配接中斷請求的确認; 

CLOSED - 沒有任何連接配接狀态;

TCP連接配接過程是狀态的轉換,促使發生狀态轉換的是使用者調用:

OPEN,SEND,RECEIVE,CLOSE,ABORT和STATUS

傳送過來的資料段,特别那些包括以下标記的資料段SYN,ACK,RST和FIN;

還有逾時,上面所說的都會時TCP狀态發生變化。

這個圖n多人都 知道,它對排除和定 位網絡或系統故障時大有幫助,但是怎樣牢牢地将這張圖刻在腦中呢?那麼你就一定要對 這張圖的每一個狀态,及轉換的過程有深刻地認識,不能隻停留在一知半解之中。下面對這張圖的11種狀 态詳細解釋一下,以便加強記憶!不過在這之前,先回顧一下TCP建立連接配接的三次握手過程,以及關閉連接配接的四次握手過程。

1、建立連接配接協定(三次握手)

(1)客戶 端發送一個帶SYN标志的TCP封包到伺服器。這是三次握手過程中的封包1。

(2) 伺服器端回應用戶端的,這是三次握手中的第2個封包,這個封包同時帶ACK标志和SYN标 志。是以它表示對剛才用戶端SYN封包的回應;同時又标志SYN給用戶端,詢問用戶端是否準備好進行資料通 訊。

(3) 客戶必須再次回應服務段一個ACK封包,這是封包段3。

2、連接配接終止協定(四次握手)

   由于TCP連 接是全雙工的,是以每個方向都必須單獨進行關閉。這原則是當一方完成它的資料發送任務後就能發送一個FIN來終 止這個方向的連接配接。收到一個 FIN隻意味着這一方向上沒有資料流動,一個TCP連接配接 在收到一個FIN後仍能發送資料。首先進行關閉的一方将執行主動關閉,而另一方執行被動關閉。

 (1) TCP客 戶端發送一個FIN,用來關閉客戶到伺服器的資料傳送(封包段4)。

 (2) 伺服器收到這個FIN,它發回一個ACK,确認序号為收到的序号加1(封包段5)。和SYN一 樣,一個FIN将占用一個序号。

 (3) 伺服器關閉用戶端的連接配接,發送一個FIN給用戶端(封包段6)。

 (4) 客戶段發回ACK封包确認,并将确認序号設定為收到序号加1(封包段7)。

CLOSED: 這個沒什麼好說的了,表示初始狀态。

LISTEN: 這個也是非常容易了解的一個狀态,表示伺服器端的某個SOCKET處 于監聽狀态,可以接受連接配接了。

SYN_RCVD: 這個狀态表示接受到了SYN報 文,在正常情況下,這個狀态是伺服器端的SOCKET在建立TCP連接配接時的三次握手會話過程中的一個中間狀态,很短暫,基本上用netstat你是很難看到這種狀态的,除非你特意寫了一個用戶端測試程式,故意将三次TCP握手 過程中最後一個ACK封包不予發送。是以這種狀态時,當收到用戶端的ACK封包 後,它會進入到ESTABLISHED狀态。

SYN_SENT: 這個狀态與SYN_RCVD遙想呼應,當用戶端SOCKET執行CONNECT連接配接時,它首先發送SYN封包,是以也随即它會進入到了SYN_SENT狀态,并等待服務端的發送三次握手中的第2個封包。SYN_SENT狀态表示用戶端已發送SYN封包。

ESTABLISHED:這個容易了解了,表示連接配接已經建立了。

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連接配接,但另外還告訴對方,我暫時還有點 資料需要傳送給你,稍後再關閉連接配接。

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可用狀态了。

最後有2個問題 的回答,我自己分析後的結論(不一定保證100%正确)

1、 為什麼建立連接配接協定是三次握手,而關閉連接配接卻是四次握手呢?

這是因為服務端的LISTEN狀态下的SOCKET當收到SYN封包的建連請求後,它可以把ACK和SYN(ACK起 應答作用,而SYN起同步作用)放在一個封包裡來發送。但關閉連接配接時,當收到對方的FIN封包 通知時,它僅僅表示對方沒有資料發送給你了;但未必你所有的資料都全部發送給對方了,是以你可以未必會馬上會關閉SOCKET,也即你可能還需要發送一些資料給對方之後,再發送FIN封包給對方來表示你同意現在可以關閉連接配接了,是以它這裡的ACK封包 和FIN封包多數情況下都是分開發送的。

2、 為什麼TIME_WAIT狀态還需要等2MSL後才能傳回到CLOSED狀 态?

這是因為:雖然雙方 都同意關閉連接配接了,而且握手的4個封包也都協調和發送完畢,按理可以直接回到CLOSED狀 态(就好比從SYN_SEND狀态到ESTABLISH狀态那樣);但是因為我們必須要假想網絡是不可靠的,你無法保證你最後發送的ACK報 文會一定被對方收到,是以對方處于LAST_ACK狀态下的SOCKET可能會因為逾時未收到ACK封包,而重發FIN報 文,是以這個TIME_WAIT狀态的作用就是用來重發可能丢失的ACK報 文,并保證于此。

     斷開連接配接的時候, 當發起主動關閉的左邊這方發送一個FIN過去後,

右邊被動關閉的這方要回應一個ACK,這個ACK是TCP回應的,而不是應用程式發送的,

此時,被動關閉的一方就處于CLOSE_WAIT狀态了。

如果此時被動關閉的這一方不再繼續調用closesocket,那麼他就不會發送接下來的FIN,導緻自己老是處于CLOSE_WAIT。

隻有被動關閉的這一方調用了 closesocket,才會發送一個FIN給主動關閉的這一方,同時也使得自己的狀态變遷為LAST_ACK。 

比如被動關閉的是用戶端.

當對方調用closesocket的時候,你的程式正在 

int nRet = recv(s,....); if (nRet == SOCKET_ERROR) {     // closesocket(s);&#160;     return FALSE; }

很多人就是忘記了那句closesocket,這種代碼太常見了。 

我的了解,

當主動關閉的一方發送FIN到被動關閉這邊後,被動關閉這邊的TCP馬上回應一個ACK過去,同時向上面應用程式送出一個ERROR,

導緻上面的SOCKET的send或者recv傳回SOCKET_ERROR.

正常情況下,如果上面在傳回SOCKET_ERROR後調用了closesocket, 那麼被動關閉的者一方的TCP就會發送一個FIN過去,自己的狀态就變遷到LAST_ACK.

伺服器上出現大量的close_wait的例子和解決方法(例子從網上找的,基本差不多)

$ /usr/sbin/lsof -i | grep 6800

$ /usr/sbin/lsof -i | grep 6800

oracle    22725 oracle9i    3u IPv4 18621468       TCP RHEL3:6800 (LISTEN)

oracle    22725 oracle9i    4u IPv4 18621469       TCP RHEL3:6800->RHEL3:2174 (CLOSE_WAIT)

oracle    22725 oracle9i    8u IPv4 18621568       TCP RHEL3:6800->RHEL3:2175 (CLOSE_WAIT)

oracle    22725 oracle9i    9u IPv4 18621578       TCP RHEL3:6800->RHEL3:2176 (CLOSE_WAIT)

oracle    22726 oracle9i    3u IPv4 18621468       TCP RHEL3:6800 (LISTEN)

oracle    22726 oracle9i    4u IPv4 18621469       TCP RHEL3:6800->RHEL3:2174 (CLOSE_WAIT)

oracle    22726 oracle9i    8u IPv4 18621568       TCP RHEL3:6800->RHEL3:2175 (CLOSE_WAIT)

oracle    22726 oracle9i    9u IPv4 18621578       TCP RHEL3:6800->RHEL3:2176 (CLOSE_WAIT)

$ kill -9 22725 # 22725, 22726就是使用該6800端口的程序号(PID)。 $ /usr/sbin/lsof -i | grep 6800

程序被kill時,會釋放占用的所有連結句柄。 

該問題的出現原因網上到處都是,也就是Socket的Client端出現異常沒有Close就退出了。