CLOSE_WAIT狀態(tài)的生成原因 收藏
關(guān)閉socket分為主動(dòng)關(guān)閉(Active closure)和被動(dòng)關(guān)閉(Passive closure)兩種情況。前者是指有本地主機主動(dòng)發(fā)起的關(guān)閉;而后者則是指本地主機檢測到遠程主機發(fā)起關(guān)閉之后,作出回應,從而關(guān)閉整個(gè)連接。
其狀態(tài)圖如下圖所示:
起初每個(gè)socket都是CLOSED狀態(tài),當客戶(hù)端初使化一個(gè)連接,他發(fā)送一個(gè)SYN包到服務(wù)器,客戶(hù)端進(jìn)入SYN_SENT狀態(tài)。
服務(wù)器接收到SYN包,反饋一個(gè)SYN-ACK包,客戶(hù)端接收后返饋一個(gè)ACK包客戶(hù)端變成ESTABLISHED狀態(tài),如果長(cháng)時(shí)間沒(méi)收到SYN-ACK包,客戶(hù)端超時(shí)進(jìn)入CLOSED狀態(tài)。
當服務(wù)器綁定并監聽(tīng)某一端口時(shí),socket的狀態(tài)是LISTEN,當客戶(hù)企圖建立連接時(shí),服務(wù)器收到一個(gè)SYN包,并反饋SYN-ACK包。服務(wù)器狀態(tài)變成SYN_RCVD,當客戶(hù)端發(fā)送一個(gè)ACK包時(shí),服務(wù)器socket變成ESTABLISHED狀態(tài)。
當一個(gè)程序在ESTABLISHED狀態(tài)時(shí)有兩種圖徑關(guān)閉它, 第一是主動(dòng)關(guān)閉,第二是被動(dòng)關(guān)閉。如果你要主動(dòng)關(guān)閉的話(huà),發(fā)送一個(gè)FIN包。當你的程序closesocket或者shutdown(標記),你的程序發(fā)送一個(gè)FIN包到peer,你的socket變成FIN_WAIT_1狀態(tài)。peer反饋一個(gè)ACK包,你的socket進(jìn)入FIN_WAIT_2狀態(tài)。如果peer也在關(guān)閉連接,那么它將發(fā)送一個(gè)FIN包到你的電腦,你反饋一個(gè)ACK包,并轉成TIME_WAIT狀態(tài)。
TIME_WAIT狀態(tài)又號2MSL等待狀態(tài)。MSL意思是最大段生命周期(Maximum Segment Lifetime)表明一個(gè)包存在于網(wǎng)絡(luò )上到被丟棄之間的時(shí)間。每個(gè)IP包有一個(gè)TTL(time_to_live),當它減到0時(shí)則包被丟棄。每個(gè)路由器使TTL減一并且傳送該包。當一個(gè)程序進(jìn)入TIME_WAIT狀態(tài)時(shí),他有2個(gè)MSL的時(shí)間,這個(gè)充許TCP重發(fā)最后的ACK,萬(wàn)一最后的ACK丟失了,使得FIN被重新傳輸。在2MSL等待狀態(tài)完成后,socket進(jìn)入CLOSED狀態(tài)。
被動(dòng)關(guān)閉:當程序收到一個(gè)FIN包從peer,并反饋一個(gè)ACK包,于是程序的socket轉入CLOSE_WAIT狀態(tài)。因為peer已經(jīng)關(guān)閉了,所以不能發(fā)任何消息了。但程序還可以。要關(guān)閉連接,程序自已發(fā)送給自已FIN,使程序的TCP socket狀態(tài)變成LAST_ACK狀態(tài),當程序從peer收到ACK包時(shí),程序進(jìn)入CLOSED狀態(tài)。
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
root權限執行 lsof -n httpd | grep TCP ,可以查看是否有大量CLOSE_WAIT
摘要:本文闡述了為何socket連接鎖定在CLOSE_WAIT狀態(tài),以及通過(guò)什么措施力求避免這種情況。
不久前,我的Socket Client程序遇到了一個(gè)非常尷尬的錯誤。它本來(lái)應該在一個(gè)socket長(cháng)連接上持續不斷地向服務(wù)器發(fā)送數據,如果socket連接斷開(kāi),那么程序會(huì )自動(dòng)不斷地重試建立連接。
有一天發(fā)現程序在不斷嘗試建立連接,但是總是失敗。用netstat查看,這個(gè)程序竟然有上千個(gè)socket連接處于CLOSE_WAIT狀態(tài),以至于達到了上限,所以無(wú)法建立新的socket連接了。
為什么會(huì )這樣呢?
它們?yōu)槭裁磿?huì )都處在CLOSE_WAIT狀態(tài)呢?
CLOSE_WAIT狀態(tài)的生成原因
首先我們知道,如果我們的Client程序處于CLOSE_WAIT狀態(tài)的話(huà),說(shuō)明套接字是被動(dòng)關(guān)閉的!
因為如果是Server端主動(dòng)斷掉當前連接的話(huà),那么雙方關(guān)閉這個(gè)TCP連接共需要四個(gè)packet:
Server ---> FIN ---> Client
Server <--- ACK <--- Client
這時(shí)候Server端處于FIN_WAIT_2狀態(tài);而我們的程序處于CLOSE_WAIT狀態(tài)。
Server <--- FIN <--- Client
這時(shí)Client發(fā)送FIN給Server,Client就置為L(cháng)AST_ACK狀態(tài)。
Server ---> ACK ---> Client
Server回應了ACK,那么Client的套接字才會(huì )真正置為CLOSED狀態(tài)。
我們的程序處于CLOSE_WAIT狀態(tài),而不是LAST_ACK狀態(tài),說(shuō)明還沒(méi)有發(fā)FIN給Server,那么可能是在關(guān)閉連接之前還有許多數據要發(fā)送或者其他事要做,導致沒(méi)有發(fā)這個(gè)FIN packet。
原因知道了,那么為什么不發(fā)FIN包呢,難道會(huì )在關(guān)閉己方連接前有那么多事情要做嗎?
elssann舉例說(shuō),當對方調用closesocket的時(shí)候,我的程序正在調用recv中,這時(shí)候有可能對方發(fā)送的FIN包我沒(méi)有收到,而是由TCP代回了一個(gè)ACK包,所以我這邊套接字進(jìn)入CLOSE_WAIT狀態(tài)。
所以他建議在這里判斷recv函數的返回值是否已出錯,是的話(huà)就主動(dòng)closesocket,這樣防止沒(méi)有接收到FIN包。
因為前面我們已經(jīng)設置了recv超時(shí)時(shí)間為30秒,那么如果真的是超時(shí)了,這里收到的錯誤應該是WSAETIMEDOUT,這種情況下也可以主動(dòng)關(guān)閉連接的。
還有一個(gè)問(wèn)題,為什么有數千個(gè)連接都處于這個(gè)狀態(tài)呢?難道那段時(shí)間內,服務(wù)器端總是主動(dòng)拆除我們的連接嗎?
不管怎么樣,我們必須防止類(lèi)似情況再度發(fā)生!
首先,我們要保證原來(lái)的端口可以被重用,這可以通過(guò)設置SO_REUSEADDR套接字選項做到:
重用本地地址和端口
以前我總是一個(gè)端口不行,就換一個(gè)新的使用,所以導致讓數千個(gè)端口進(jìn)入CLOSE_WAIT狀態(tài)。如果下次還發(fā)生這種尷尬狀況,我希望加一個(gè)限定,只是當前這個(gè)端口處于CLOSE_WAIT狀態(tài)!
在調用
sockConnected = socket(AF_INET, SOCK_STREAM, 0);
之后,我們要設置該套接字的選項來(lái)重用:
/// 允許重用本地地址和端口:
/// 這樣的好處是,即使socket斷了,調用前面的socket函數也不會(huì )占用另一個(gè),而是始終就是一個(gè)端口
/// 這樣防止socket始終連接不上,那么按照原來(lái)的做法,會(huì )不斷地換端口。
int nREUSEADDR = 1;
setsockopt(sockConnected,
SOL_SOCKET,
SO_REUSEADDR,
(const char*)&nREUSEADDR,
sizeof(int));
教科書(shū)上是這么說(shuō)的:這樣,假如服務(wù)器關(guān)閉或者退出,造成本地地址和端口都處于TIME_WAIT狀態(tài),那么SO_REUSEADDR就顯得非常有用。
也許我們無(wú)法避免被凍結在CLOSE_WAIT狀態(tài)永遠不出現,但起碼可以保證不會(huì )占用新的端口。
其次,我們要設置SO_LINGER套接字選項:
從容關(guān)閉還是強行關(guān)閉?
LINGER是“拖延”的意思。
默認情況下(Win2k),SO_DONTLINGER套接字選項的是1;SO_LINGER選項是,linger為{l_onoff:0,l_linger:0}。
如果在發(fā)送數據的過(guò)程中(send()沒(méi)有完成,還有數據沒(méi)發(fā)送)而調用了closesocket(),以前我們一般采取的措施是“從容關(guān)閉”:
因為在退出服務(wù)或者每次重新建立socket之前,我都會(huì )先調用
/// 先將雙向的通訊關(guān)閉
shutdown(sockConnected, SD_BOTH);
/// 安全起見(jiàn),每次建立Socket連接前,先把這個(gè)舊連接關(guān)閉
closesocket(sockConnected);
我們這次要這么做:
設置SO_LINGER為零(亦即linger結構中的l_onoff域設為非零,但l_linger為0),便不用擔心closesocket調用進(jìn)入“鎖定”狀態(tài)(等待完成),不論是否有排隊數據未發(fā)送或未被確認。這種關(guān)閉方式稱(chēng)為“強行關(guān)閉”,因為套接字的虛電路立即被復位,尚未發(fā)出的所有數據都會(huì )丟失。在遠端的recv()調用都會(huì )失敗,并返回WSAECONNRESET錯誤。
在connect成功建立連接之后設置該選項:
linger m_sLinger;
m_sLinger.l_onoff = 1; // (在closesocket()調用,但是還有數據沒(méi)發(fā)送完畢的時(shí)候容許逗留)
m_sLinger.l_linger = 0; // (容許逗留的時(shí)間為0秒)
setsockopt(sockConnected,
SOL_SOCKET,
SO_LINGER,
(const char*)&m_sLinger,
sizeof(linger));
總結
也許我們避免不了CLOSE_WAIT狀態(tài)凍結的再次出現,但我們會(huì )使影響降到最小,希望那個(gè)重用套接字選項能夠使得下一次重新建立連接時(shí)可以把CLOSE_WAIT狀態(tài)踢掉。