不久前,我的Socket Client程序遇到了一個(gè)非常尷尬的錯(cuò)誤。它本來應(yīng)該在一個(gè)socket長(zhǎng)連接上持續(xù)不斷地向服務(wù)器發(fā)送數(shù)據(jù),如果socket連接斷開,那么程序會(huì)自動(dòng)不斷地重試建立連接。
有一天發(fā)現(xiàn)程序在不斷嘗試建立連接,但是總是失敗。用netstat查看,這個(gè)程序竟然有上千個(gè)socket連接處于CLOSE_WAIT狀態(tài),以至于達(dá)到了上限,所以無法建立新的socket連接了。
為什么會(huì)這樣呢?
它們?yōu)槭裁磿?huì)都處在CLOSE_WAIT狀態(tài)呢?
CLOSE_WAIT狀態(tài)的生成原因
首先我們知道,如果我們的Client程序處于CLOSE_WAIT狀態(tài)的話,說明套接字是被動(dòng)關(guān)閉的!
因?yàn)槿绻荢erver端主動(dòng)斷掉當(dāng)前連接的話,那么雙方關(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(zhǎng)AST_ACK狀態(tài)。
Server ---> ACK ---> Client
Server回應(yīng)了ACK,那么Client的套接字才會(huì)真正置為CLOSED狀態(tài)。

我們的程序處于CLOSE_WAIT狀態(tài),而不是LAST_ACK狀態(tài),說明還沒有發(fā)FIN給Server,那么可能是在關(guān)閉連接之前還有許多數(shù)據(jù)要發(fā)送或者其他事要做,導(dǎo)致沒有發(fā)這個(gè)FIN packet。
原因知道了,那么為什么不發(fā)FIN包呢,難道會(huì)在關(guān)閉己方連接前有那么多事情要做嗎?
還有一個(gè)問題,為什么有數(shù)千個(gè)連接都處于這個(gè)狀態(tài)呢?難道那段時(shí)間內(nèi),服務(wù)器端總是主動(dòng)拆除我們的連接嗎?
不管怎么樣,我們必須防止類似情況再度發(fā)生!
首先,我們要防止不斷開辟新的端口,這可以通過設(shè)置SO_REUSEADDR套接字選項(xiàng)做到:
重用本地地址和端口
以前我總是一個(gè)端口不行,就換一個(gè)新的使用,所以導(dǎo)致讓數(shù)千個(gè)端口進(jìn)入CLOSE_WAIT狀態(tài)。如果下次還發(fā)生這種尷尬狀況,我希望加一個(gè)限定,只是當(dāng)前這個(gè)端口處于CLOSE_WAIT狀態(tài)!
在調(diào)用
sockConnected = socket(AF_INET, SOCK_STREAM, 0);
之后,我們要設(shè)置該套接字的選項(xiàng)來重用:
/// 允許重用本地地址和端口:
/// 這樣的好處是,即使socket斷了,調(diào)用前面的socket函數(shù)也不會(huì)占用另一個(gè),而是始終就是一個(gè)端口
/// 這樣防止socket始終連接不上,那么按照原來的做法,會(huì)不斷地?fù)Q端口。
int nREUSEADDR = 1;
setsockopt(sockConnected,
SOL_SOCKET,
SO_REUSEADDR,
(const char*)&nREUSEADDR,
sizeof(int));
教科書上是這么說的:這樣,假如服務(wù)器關(guān)閉或者退出,造成本地地址和端口都處于TIME_WAIT狀態(tài),那么SO_REUSEADDR就顯得非常有用。
也許我們無法避免被凍結(jié)在CLOSE_WAIT狀態(tài)永遠(yuǎn)不出現(xiàn),但起碼可以保證不會(huì)占用新的端口。
其次,我們要設(shè)置SO_LINGER套接字選項(xiàng):
從容關(guān)閉還是強(qiáng)行關(guān)閉?
LINGER是“拖延”的意思。
默認(rèn)情況下(Win2k),SO_DONTLINGER套接字選項(xiàng)的是1;SO_LINGER選項(xiàng)是,linger為{l_onoff:0,l_linger:0}。
如果在發(fā)送數(shù)據(jù)的過程中(send()沒有完成,還有數(shù)據(jù)沒發(fā)送)而調(diào)用了closesocket(),以前我們一般采取的措施是“從容關(guān)閉”:
因?yàn)樵谕顺龇?wù)或者每次重新建立socket之前,我都會(huì)先調(diào)用
/// 先將雙向的通訊關(guān)閉
shutdown(sockConnected, SD_BOTH);
/// 安全起見,每次建立Socket連接前,先把這個(gè)舊連接關(guān)閉
closesocket(sockConnected);
我們這次要這么做:
設(shè)置SO_LINGER為零(亦即linger結(jié)構(gòu)中的l_onoff域設(shè)為非零,但l_linger為0),便不用擔(dān)心closesocket調(diào)用進(jìn)入“鎖定”狀態(tài)(等待完成),不論是否有排隊(duì)數(shù)據(jù)未發(fā)送或未被確認(rèn)。這種關(guān)閉方式稱為“強(qiáng)行關(guān)閉”,因?yàn)樘捉幼值奶撾娐妨⒓幢粡?fù)位,尚未發(fā)出的所有數(shù)據(jù)都會(huì)丟失。在遠(yuǎn)端的recv()調(diào)用都會(huì)失敗,并返回WSAECONNRESET錯(cuò)誤。
在connect成功建立連接之后設(shè)置該選項(xiàng):
linger m_sLinger;
m_sLinger.l_onoff = 1; // (在closesocket()調(diào)用,但是還有數(shù)據(jù)沒發(fā)送完畢的時(shí)候容許逗留)
m_sLinger.l_linger = 0; // (容許逗留的時(shí)間為0秒)
setsockopt(sockConnected,
SOL_SOCKET,
SO_LINGER,
(const char*)&m_sLinger,
sizeof(linger));
總結(jié)
也許我們避免不了CLOSE_WAIT狀態(tài)凍結(jié)的再次出現(xiàn),但我們會(huì)使影響降到最小,希望那個(gè)重用套接字選項(xiàng)能夠使得下一次重新建立連接時(shí)可以把CLOSE_WAIT狀態(tài)踢掉。
Feedback
# 回復(fù):[Socket]尷尬的CLOSE_WAIT狀態(tài)以及應(yīng)對(duì)策略 2005-01-30 3:41 PM yun.zheng
回復(fù)人: elssann(臭屁蟲和他的開心果) ( ) 信譽(yù):51 2005-01-30 14:00:00 得分: 0
我的意思是:當(dāng)一方關(guān)閉連接后,另外一方?jīng)]有檢測(cè)到,就導(dǎo)致了CLOSE_WAIT的出現(xiàn),上次我的一個(gè)朋友也是這樣,他寫了一個(gè)客戶端和 APACHE連接,當(dāng)APACHE把連接斷掉后,他沒檢測(cè)到,出現(xiàn)了CLOSE_WAIT,后來我叫他檢測(cè)了這個(gè)地方,他添加了調(diào)用 closesocket的代碼后,這個(gè)問題就消除了。
如果你在關(guān)閉連接前還是出現(xiàn)CLOSE_WAIT,建議你取消shutdown的調(diào)用,直接兩邊closesocket試試。
另外一個(gè)問題:
比如這樣的一個(gè)例子:
當(dāng)客戶端登錄上服務(wù)器后,發(fā)送身份驗(yàn)證的請(qǐng)求,服務(wù)器收到了數(shù)據(jù),對(duì)客戶端身份進(jìn)行驗(yàn)證,發(fā)現(xiàn)密碼錯(cuò)誤,這時(shí)候服務(wù)器的一般做法應(yīng)該是先發(fā)送一個(gè)密碼錯(cuò)誤的信息給客戶端,然后把連接斷掉。
如果把
m_sLinger.l_onoff = 1;
m_sLinger.l_linger = 0;
這樣設(shè)置后,很多情況下,客戶端根本就收不到密碼錯(cuò)誤的消息,連接就被斷了。
# 回復(fù):[Socket]尷尬的CLOSE_WAIT狀態(tài)以及應(yīng)對(duì)策略 2005-01-30 3:41 PM yun.zheng
elssann(臭屁蟲和他的開心果) ( ) 信譽(yù):51 2005-01-30 13:24:00 得分: 0
出現(xiàn)CLOSE_WAIT的原因很簡(jiǎn)單,就是某一方在網(wǎng)絡(luò)連接斷開后,沒有檢測(cè)到這個(gè)錯(cuò)誤,沒有執(zhí)行closesocket,導(dǎo)致了這個(gè)狀態(tài)的實(shí)現(xiàn),這在TCP/IP協(xié)議的狀態(tài)變遷圖上可以清楚看到。同時(shí)和這個(gè)相對(duì)應(yīng)的還有一種叫TIME_WAIT的。
另外,把SOCKET的SO_LINGER設(shè)置為0秒拖延(也就是立即關(guān)閉)在很多時(shí)候是有害處的。
還有,把端口設(shè)置為可復(fù)用是一種不安全的網(wǎng)絡(luò)編程方法。
# 回復(fù):[Socket]尷尬的CLOSE_WAIT狀態(tài)以及應(yīng)對(duì)策略 2005-01-30 3:42 PM yun.zheng
elssann(臭屁蟲和他的開心果) ( ) 信譽(yù):51 2005-01-30 14:48:00 得分: 0
能不能解釋請(qǐng)看這里
http://blog.csdn.net/cqq/archive/2005/01/26/269160.aspx
再看這個(gè)圖:
http://tech.ccidnet.com/pub/attachment/2004/8/322252.png
斷開連接的時(shí)候,
當(dāng)發(fā)起主動(dòng)關(guān)閉的左邊這方發(fā)送一個(gè)FIN過去后,右邊被動(dòng)關(guān)閉的這方要回應(yīng)一個(gè)ACK,這個(gè)ACK是TCP回應(yīng)的,而不是應(yīng)用程序發(fā)送的,此時(shí),被動(dòng)關(guān)閉的一方就處于CLOSE_WAIT狀態(tài)了。如果此時(shí)被動(dòng)關(guān)閉的這一方不再繼續(xù)調(diào)用closesocket,那么他就不會(huì)發(fā)送接下來的FIN,導(dǎo)致自己老是處于CLOSE_WAIT。只有被動(dòng)關(guān)閉的這一方調(diào)用了closesocket,才會(huì)發(fā)送一個(gè)FIN給主動(dòng)關(guān)閉的這一方,同時(shí)也使得自己的狀態(tài)變遷為L(zhǎng)AST_ACK。
# 回復(fù):[Socket]尷尬的CLOSE_WAIT狀態(tài)以及應(yīng)對(duì)策略 2005-01-30 3:54 PM yun.zheng
elssann(臭屁蟲和他的開心果) ( ) 信譽(yù):51 2005-01-30 15:39:00 得分: 0
比如被動(dòng)關(guān)閉的是客戶端。。。
當(dāng)對(duì)方調(diào)用closesocket的時(shí)候,你的程序正在
int nRet = recv(s,....);
if (nRet == SOCKET_ERROR)
{
// closesocket(s);
return FALSE;
}
很多人就是忘記了那句closesocket,這種代碼太常見了。
我的理解,當(dāng)主動(dòng)關(guān)閉的一方發(fā)送FIN到被動(dòng)關(guān)閉這邊后,被動(dòng)關(guān)閉這邊的TCP馬上回應(yīng)一個(gè)ACK過去,同時(shí)向上面應(yīng)用程序提交一個(gè)ERROR,導(dǎo)致上面的SOCKET的send或者recv返回SOCKET_ERROR,正常情況下,如果上面在返回SOCKET_ERROR后調(diào)用了 closesocket,那么被動(dòng)關(guān)閉的者一方的TCP就會(huì)發(fā)送一個(gè)FIN過去,自己的狀態(tài)就變遷到LAST_ACK.
# 回復(fù):[Socket]尷尬的CLOSE_WAIT狀態(tài)以及應(yīng)對(duì)策略 2005-01-30 4:17 PM yun.zheng
int nRecvBufLength =
recv(sockConnected,
szRecvBuffer,
sizeof(szRecvBuffer),
0);
/// zhengyun 20050130:
/// elssann舉例說,當(dāng)對(duì)方調(diào)用closesocket的時(shí)候,我的程序正在
/// recv,這時(shí)候有可能對(duì)方發(fā)送的FIN包我沒有收到,而是由TCP代回了
/// 一個(gè)ACK包,所以我這邊程序進(jìn)入CLOSE_WAIT狀態(tài)。
/// 所以他建議在這里判斷是否已出錯(cuò),是就主動(dòng)closesocket。
/// 因?yàn)榍懊嫖覀円呀?jīng)設(shè)置了recv超時(shí)時(shí)間為30秒,那么如果真的是超時(shí)了,
/// 這里收到的錯(cuò)誤應(yīng)該是WSAETIMEDOUT,這種情況下也可以關(guān)閉連接的
if (nRecvBufLength == SOCKET_ERROR)
{
TRACE_INFO(_T("=用recv接收發(fā)生Socket錯(cuò)誤="));
closesocket(sockConnected);
continue;
}
這樣可以嗎?
網(wǎng)絡(luò)連接無法釋放—— CLOSE_WAIT
關(guān)鍵字:TCP ,CLOSE_WAIT, Java, SocketChannel
問題描述:最近性能測(cè)試碰到的一個(gè)問題。客戶端使用NIO,服務(wù)器還是一般的Socket連接。當(dāng)測(cè)試進(jìn)行一段時(shí)間以后,發(fā)現(xiàn)服務(wù)器端的系統(tǒng)出現(xiàn)大量未釋放的網(wǎng)絡(luò)連接。用netstat -na查看,連接狀態(tài)為CLOSE_WAIT。這就奇怪了,為什么Socket已經(jīng)關(guān)閉而連接依然未釋放。
解決:Google了半天,發(fā)現(xiàn)關(guān)于CLOSE_WAIT的問題一般是C的,Java似乎碰到這個(gè)問題的不多(這有一篇不錯(cuò)的,也是解決CLOSE_WAIT的,但是好像沒有根本解決,而是選擇了一個(gè)折中的辦法)。接著找,由于使用了NIO,所以懷疑可能是這方面的問題,結(jié)果找到了這篇。順著帖子翻下去,其中有幾個(gè)人說到了一個(gè)問題—— 一端的Socket調(diào)用close后,另一端的Socket沒有調(diào)用close.于是查了一下代碼,果然發(fā)現(xiàn)Server端在某些異常情況時(shí),沒有關(guān)閉Socket。改正后問題解決。
時(shí)間基本上花在Google上了,不過也學(xué)到不少東西。下面為一張TCP連接的狀態(tài)轉(zhuǎn)換圖:
說明:虛線和實(shí)線分別對(duì)應(yīng)服務(wù)器端(被連接端)和客戶端端(主動(dòng)連接端)。
結(jié)合上圖使用netstat -na命令即可知道到當(dāng)前的TCP連接狀態(tài)。一般LISTEN、ESTABLISHED、TIME_WAIT是比較常見。
分析:
上面我碰到的這個(gè)問題主要因?yàn)門CP的結(jié)束流程未走完,造成連接未釋放。現(xiàn)設(shè)客戶端主動(dòng)斷開連接,流程如下
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)閉時(shí)而沒有調(diào)用關(guān)閉,造成服務(wù)器端的連接處在“掛起”狀態(tài),而客戶端則處在等待應(yīng)答的狀態(tài)上。此問題的典型特征是:一端處于FIN_WAIT2 ,而另一端處于CLOSE_WAIT. 不過,根本問題還是程序?qū)懙牟缓茫写岣摺?/font>
TIME_WAIT狀態(tài)
根據(jù)TCP協(xié)議,主動(dòng)發(fā)起關(guān)閉的一方,會(huì)進(jìn)入TIME_WAIT狀態(tài),持續(xù)2*MSL(Max Segment Lifetime),缺省為240秒,在這個(gè)post中簡(jiǎn)潔的介紹了為什么需要這個(gè)狀態(tài)。
值得一說的是,對(duì)于基于TCP的HTTP協(xié)議,關(guān)閉TCP連接的是Server端,這樣,Server端會(huì)進(jìn)入TIME_WAIT狀態(tài),可想而知,對(duì)于訪問量大的Web Server,會(huì)存在大量的TIME_WAIT狀態(tài),假如server一秒鐘接收1000個(gè)請(qǐng)求,那么就會(huì)積壓240*1000=240,000個(gè) TIME_WAIT的記錄,維護(hù)這些狀態(tài)給Server帶來負(fù)擔(dān)。當(dāng)然現(xiàn)代操作系統(tǒng)都會(huì)用快速的查找算法來管理這些TIME_WAIT,所以對(duì)于新的 TCP連接請(qǐng)求,判斷是否hit中一個(gè)TIME_WAIT不會(huì)太費(fèi)時(shí)間,但是有這么多狀態(tài)要維護(hù)總是不好。
HTTP協(xié)議1.1版規(guī)定default行為是Keep-Alive,也就是會(huì)重用TCP連接傳輸多個(gè) request/response,一個(gè)主要原因就是發(fā)現(xiàn)了這個(gè)問題。還有一個(gè)方法減緩TIME_WAIT壓力就是把系統(tǒng)的2*MSL時(shí)間減少,因?yàn)?240秒的時(shí)間實(shí)在是忒長(zhǎng)了點(diǎn),對(duì)于Windows,修改注冊(cè)表,在HKEY_LOCAL_MACHINE\ SYSTEM\CurrentControlSet\Services\ Tcpip\Parameters上添加一個(gè)DWORD類型的值TcpTimedWaitDelay,一般認(rèn)為不要少于60,不然可能會(huì)有麻煩。
對(duì)于大型的服務(wù),一臺(tái)server搞不定,需要一個(gè)LB(Load Balancer)把流量分配到若干后端服務(wù)器上,如果這個(gè)LB是以NAT方式工作的話,可能會(huì)帶來問題。假如所有從LB到后端Server的IP包的 source address都是一樣的(LB的對(duì)內(nèi)地址),那么LB到后端Server的TCP連接會(huì)受限制,因?yàn)轭l繁的TCP連接建立和關(guān)閉,會(huì)在server上留下TIME_WAIT狀態(tài),而且這些狀態(tài)對(duì)應(yīng)的remote address都是LB的,LB的source port撐死也就60000多個(gè)(2^16=65536,1~1023是保留端口,還有一些其他端口缺省也不會(huì)用),每個(gè)LB上的端口一旦進(jìn)入 Server的TIME_WAIT黑名單,就有240秒不能再用來建立和Server的連接,這樣LB和Server最多也就能支持300個(gè)左右的連接。如果沒有LB,不會(huì)有這個(gè)問題,因?yàn)檫@樣server看到的remote address是internet上廣闊無垠的集合,對(duì)每個(gè)address,60000多個(gè)port實(shí)在是夠用了。
一開始我覺得用上LB會(huì)很大程度上限制TCP的連接數(shù),但是實(shí)驗(yàn)表明沒這回事,LB后面的一臺(tái)Windows Server 2003每秒處理請(qǐng)求數(shù)照樣達(dá)到了600個(gè),難道TIME_WAIT狀態(tài)沒起作用?用Net Monitor和netstat觀察后發(fā)現(xiàn),Server和LB的XXXX端口之間的連接進(jìn)入TIME_WAIT狀態(tài)后,再來一個(gè)LB的XXXX端口的 SYN包,Server照樣接收處理了,而是想像的那樣被drop掉了。翻書,從書堆里面找出覆滿塵土的大學(xué)時(shí)代買的《UNIX Network Programming, Volume 1, Second Edition: Networking APIs: Sockets and XTI》,中間提到一句,對(duì)于BSD-derived實(shí)現(xiàn),只要SYN的sequence number比上一次關(guān)閉時(shí)的最大sequence number還要大,那么TIME_WAIT狀態(tài)一樣接受這個(gè)SYN,難不成Windows也算BSD-derived?有了這點(diǎn)線索和關(guān)鍵字 (BSD),找到這個(gè)post,在NT4.0的時(shí)候,還是和BSD-derived不一樣的,不過Windows Server 2003已經(jīng)是NT5.2了,也許有點(diǎn)差別了。
做個(gè)試驗(yàn),用Socket API編一個(gè)Client端,每次都Bind到本地一個(gè)端口比如2345,重復(fù)的建立TCP連接往一個(gè)Server發(fā)送Keep-Alive=false 的HTTP請(qǐng)求,Windows的實(shí)現(xiàn)讓sequence number不斷的增長(zhǎng),所以雖然Server對(duì)于Client的2345端口連接保持TIME_WAIT狀態(tài),但是總是能夠接受新的請(qǐng)求,不會(huì)拒絕。那如果SYN的Sequence Number變小會(huì)怎么樣呢?同樣用Socket API,不過這次用Raw IP,發(fā)送一個(gè)小sequence number的SYN包過去,Net Monitor里面看到,這個(gè)SYN被Server接收后如泥牛如海,一點(diǎn)反應(yīng)沒有,被drop掉了。
按照書上的說法,BSD-derived和Windows Server 2003的做法有安全隱患,不過至少這樣至少不會(huì)出現(xiàn)TIME_WAIT阻止TCP請(qǐng)求的問題,當(dāng)然,客戶端要配合,保證不同TCP連接的sequence number要上漲不要下降。
本文來自CSDN博客,轉(zhuǎn)載請(qǐng)標(biāo)明出處:http://blog.csdn.net/lionzl/archive/2009/03/20/4007206.as
posted on 2010-07-17 22:37
chatler 閱讀(1135)
評(píng)論(0) 編輯 收藏 引用 所屬分類:
Socket