TCP狀態(tài)遷移
大家對netstat -a命令很熟悉,但是,你有沒有注意到STATE一欄呢,基本上顯示著established,time_wait,close_wait等,這些到底是 什么意思呢,在這篇文章,我將會詳細的闡述。
大家很明白TCP初始化連接三次握手吧:發(fā)SYN包,然后返回SYN/ACK包,再發(fā)ACK包,連接正式建立。但是這里有點出入,當請求者收到SYS /ACK包后,就開始建立連接了,而被請求者第三次握手結(jié)束后才建立連接。但是大家明白關(guān)閉連接的工作原理嗎?關(guān)閉連接要四次握手:發(fā)FIN包,ACK 包,F(xiàn)IN包,ACK包,四次握手??!為什么呢,因為TCP連接是全雙工,我關(guān)了你的連接,并不等于你關(guān)了我的連接。
客戶端TCP狀態(tài)遷移:
CLOSED->SYN_SENT->ESTABLISHED->FIN_WAIT_1->FIN_WAIT_2->TIME_WAIT->CLOSED
服務器TCP狀態(tài)遷移:
CLOSED->LISTEN->SYN收到 ->ESTABLISHED->CLOSE_WAIT->LAST_ACK->CLOSED
當客戶端開始連接時,服務器還處于LISTENING,
客戶端發(fā)一個SYN包后,他就處于SYN_SENT狀態(tài),服務器就處于SYS收到狀態(tài),
然后互相確認進入連接狀態(tài)ESTABLISHED.
當客戶端請求關(guān)閉連接時,客戶端發(fā)送一個FIN包后,客戶端就進入FIN_WAIT_1狀態(tài),等待對方的確認包,
服務器發(fā)送一個ACK包給客戶,客戶端收到ACK包后結(jié)束FIN_WAIT_1狀態(tài),進入FIN_WAIT_2狀態(tài),等待服務器發(fā)過來的關(guān)閉請求,
服務器發(fā)一個FIN包后,進入CLOSE_WAIT狀態(tài),
當客戶端收到服務器的FIN包,FIN_WAIT_2狀態(tài)就結(jié)束,然后給服務器端的FIN包給以一個確認包,客戶端這時進入TIME_WAIT,
當服務器收到確認包后,CLOSE_WAIT狀態(tài)結(jié)束了,
這時候服務器端真正的關(guān)閉了連接.但是客戶端還在TIME_WAIT狀態(tài)下,
什么時候結(jié)束呢.我在這里再講到一個新名詞:2MSL等待狀態(tài),其實TIME_WAIT就是2MSL等待狀態(tài),
為什么要設置這個狀態(tài),原因是有足夠的時間讓ACK包到達服務器端,如果服務器端沒收到ACK包,超時了,然后重新發(fā)一個FIN包,直到服務器收到ACK 包.
TIME_WAIT狀態(tài)等待時間是在TCP重新啟動后不連接任何請求的兩倍.
大家有沒有發(fā)現(xiàn)一個問題:如果對方在第三次握手的時候出問題,如發(fā)FIN包的時候,不知道什么原因丟了這個包,然而這邊一直處在FIN_WAIT_2狀 態(tài),而且TCP/IP并沒有設置這個狀態(tài)的過期時間,那他一直會保留這個狀態(tài)下去,越來越多的FIN_WAIT_2狀態(tài)會導致系統(tǒng)崩潰.
上面我碰到的這個問題主要因為TCP的結(jié)束流程未走完,造成連接未釋放?,F(xiàn)設客戶端主動斷開連接,流程如下
如上圖所示,
Client 消息 Server
close()
------ FIN ------->
FIN_WAIT1 CLOSE_WAIT
<----- ACK -------
FIN_WAIT2
close()
<------ FIN ------
TIME_WAIT LAST_ACK
------ ACK ------->
CLOSED
CLOSED
由于Server的Socket在客戶端已經(jīng)關(guān)閉時而沒有調(diào)用關(guān)閉,
造成服務器端的連接處在“掛起”狀態(tài),而客戶端則處在等待應答的狀態(tài)上。
此問題的典型特征是:
一端處于FIN_WAIT2 ,而另一端處于CLOSE_WAIT.
不過,根本問題還是程序?qū)懙牟缓?,有待提?/font>
-------------------------------------------------------------------------
CLOSE_WAIT,TCP的癌癥,TCP的朋友。
CLOSE_WAIT狀態(tài)的生成原因
首先我們知道,如果我們的服務器程序APACHE處于CLOSE_WAIT狀態(tài)的話,說明套接字是被動關(guān)閉的!
因為如果是CLIENT端主動斷掉當前連接的話,那么雙方關(guān)閉這個TCP連接共需要四個packet:
Client ---> FIN ---> Server
Client <--- ACK <--- Server
這時候Client端處于FIN_WAIT_2狀態(tài);而Server 程序處于CLOSE_WAIT狀態(tài)。
Client <--- FIN <--- Server
這時Server 發(fā)送FIN給Client,Server 就置為LAST_ACK狀態(tài)。
Client ---> ACK ---> Server
Client回應了ACK,那么Server 的套接字才會真正置為CLOSED狀態(tài)。
Server 程序處于CLOSE_WAIT狀態(tài),而不是LAST_ACK狀態(tài),說明還沒有發(fā)FIN給Client,那么可能是在關(guān)閉連接之前還有許多數(shù)據(jù)要發(fā)送或者其 他事要做,導致沒有發(fā)這個FIN packet。
通常來說,一個CLOSE_WAIT會維持至少2個小時的時間。如果有個流氓特地寫了個程序,給你造成一堆的 CLOSE_WAIT,消耗你的資源,那么通常是等不到釋放那一刻,系統(tǒng)就已經(jīng)解決崩潰了。
只能通過修改一下TCP/IP的參數(shù),來縮短這個時間:修改tcp_keepalive_*系列參數(shù)有助于解決這個 問題。
解決這個問題的方法是修改系統(tǒng)的參數(shù),系統(tǒng)默認超時時間的是7200秒,也就是2小時, 這個太大了,可以修改如下幾個參數(shù):
sysctl -w net.ipv4.tcp_keepalive_time=30
sysctl -w net.ipv4.tcp_keepalive_probes=2
sysctl -w net.ipv4.tcp_keepalive_intvl=2
然后,執(zhí)行sysctl命令使修改生效。
連接進程是通過一系列狀態(tài)表示的,這些狀態(tài)有:
LISTEN,SYN-SENT,SYN-RECEIVED,ESTABLISHED,F(xiàn)IN-WAIT-1,F(xiàn)IN-WAIT-2,CLOSE- WAIT,CLOSING,LAST-ACK,TIME-WAIT和CLOSED。
各個狀態(tài)的意義如下:
LISTEN - 偵聽來自遠方TCP端口的連接請求;
SYN-SENT -在發(fā)送連接請求后等待匹配的連接請求;
SYN-RECEIVED - 在收到和發(fā)送一個連接請求后等待對連接請求的確認;
ESTABLISHED- 代表一個打開的連接,數(shù)據(jù)可以傳送給用戶;
FIN-WAIT-1 - 等待遠程TCP的連接中斷請求,或先前的連接中斷請求的確認;
FIN-WAIT-2 - 從遠程TCP等待連接中斷請求;
CLOSE-WAIT - 等待從本地用戶發(fā)來的連接中斷請求;
CLOSING -等待遠程TCP對連接中斷的確認;
LAST-ACK - 等待原來發(fā)向遠程TCP的連接中斷請求的確認;
TIME-WAIT -等待足夠的時間以確保遠程TCP接收到連接中斷請求的確認;
CLOSED - 沒有任何連接狀態(tài);
TCP連接過程是狀態(tài)的轉(zhuǎn)換,促使發(fā)生狀態(tài)轉(zhuǎn)換的是用戶調(diào)用:
OPEN,SEND,RECEIVE,CLOSE,ABORT和STATUS;
傳送過來的數(shù)據(jù)段,特別那些包括以下標記的數(shù)據(jù)段SYN,ACK,RST和FIN;
還有超時,上面所說的都會時TCP狀態(tài)發(fā)生變化。
斷開連接的時候, 當發(fā)起主動關(guān)閉的左邊這方發(fā)送一個FIN過去后,
右邊被動關(guān)閉的這方要回應一個ACK,這個ACK是TCP回應的,而不是應用程序發(fā)送的,
此時,被動關(guān)閉的一方就處于CLOSE_WAIT狀態(tài)了。
如果此時被動關(guān)閉的這一方不再繼續(xù)調(diào)用closesocket,那么他就不會發(fā)送接下來的FIN,導致自己老是處于CLOSE_WAIT。
只有被動關(guān)閉的這一方調(diào)用了 closesocket,才會發(fā)送一個FIN給主動關(guān)閉的這一方,同時也使得自己的狀態(tài)變遷為LAST_ACK。
比如被動關(guān)閉的是客戶端.
當對方調(diào)用closesocket的時候,你的程序正在
int nRet = recv(s,....);
if (nRet == SOCKET_ERROR)
{
// closesocket(s);
return FALSE;
}
很多人就是忘記了那句closesocket,這種代碼太常見了。
我的理解,
當主動關(guān)閉的一方發(fā)送FIN到被動關(guān)閉這邊后,被動關(guān)閉這邊的TCP馬上回應一個ACK過去,同時向上面應用程序提交一個ERROR,
導致上面的SOCKET的send或者recv返回SOCKET_ERROR.
正常情況下,如果上面在返回SOCKET_ERROR后調(diào)用了closesocket, 那么被動關(guān)閉的者一方的TCP就會發(fā)送一個FIN過去,自己的狀態(tài)就變遷到LAST_ACK.
服務器上出現(xiàn)大量的close_wait的例子和解決方法(例子從網(wǎng)上找的,基本差不多)
oracle9i@RHEL3 oracle9i]$ /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)
[oracle9i@RHEL3 oracle9i]$ kill -9 22725
# 22725, 22726就是使用該6800端口的進程號(PID)。
[oracle9i@RHEL3 oracle9i]$ /usr/sbin/lsof -i | grep 6800
進程被kill時,會釋放占用的所有鏈接句柄。
該問題的出現(xiàn)原因網(wǎng)上到處都是,也就是Socket的Client端出現(xiàn)異常沒有Close就退出了。
------------------------------------------------------
本文的資料收集整理自網(wǎng)絡.