socket:close_wait狀態和time_wait狀態問題

轉自: http://blog.csdn.net/needle2/article/details/5822925

不久前,我的Socket Client程序遇到了一個非常尷尬的錯誤。它本來應該在一個socket長連接上持續不斷地向服務器發送數據,如果socket連接斷開,那麼程序會自動不斷地重試建立連接。
有一天發現程序在不斷嘗試建立連接,但是總是失敗。用netstat查看,這個程序竟然有上千個socket連接處於CLOSE_WAIT狀態,以至於達到了上限,所以無法建立新的socket連接了。
爲什麼會這樣呢?
它們爲什麼會都處在CLOSE_WAIT狀態呢?
CLOSE_WAIT狀態的生成原因
首先我們知道,如果我們的Client程序處於CLOSE_WAIT狀態的話,說明套接字是被動關閉的!
因爲如果是Server端主動斷掉當前連接的話,那麼雙方關閉這個TCP連接共需要四個packet:
       Server ---> FIN ---> Client
       Server <--- ACK <--- Client
    這時候Server端處於FIN_WAIT_2狀態;而我們的程序處於CLOSE_WAIT狀態。
       Server <--- FIN <--- Client
這時Client發送FIN給Server,Client就置爲LAST_ACK狀態。
        Server ---> ACK ---> Client
Server迴應了ACK,那麼Client的套接字纔會真正置爲CLOSED狀態。


 
我們的程序處於CLOSE_WAIT狀態,而不是LAST_ACK狀態,說明還沒有發FIN給Server,那麼可能是在關閉連接之前還有許多數據要發送或者其他事要做,導致沒有發這個FIN packet。
 
原因知道了,那麼爲什麼不發FIN包呢,難道會在關閉己方連接前有那麼多事情要做嗎?
還有一個問題,爲什麼有數千個連接都處於這個狀態呢?難道那段時間內,服務器端總是主動拆除我們的連接嗎?
 
不管怎麼樣,我們必須防止類似情況再度發生!
首先,我們要防止不斷開闢新的端口,這可以通過設置SO_REUSEADDR套接字選項做到:
重用本地地址和端口
以前我總是一個端口不行,就換一個新的使用,所以導致讓數千個端口進入CLOSE_WAIT狀態。如果下次還發生這種尷尬狀況,我希望加一個限定,只是當前這個端口處於CLOSE_WAIT狀態!
在調用
sockConnected = socket(AF_INET, SOCK_STREAM, 0);
之後,我們要設置該套接字的選項來重用:

/// 允許重用本地地址和端口:
/// 這樣的好處是,即使socket斷了,調用前面的socket函數也不會佔用另一個,而是始終就是一個端口
/// 這樣防止socket始終連接不上,那麼按照原來的做法,會不斷地換端口。
int nREUSEADDR = 1;
setsockopt(sockConnected,
              SOL_SOCKET,
              SO_REUSEADDR,
              (const char*)&nREUSEADDR,
              sizeof(int));

教科書上是這麼說的:這樣,假如服務器關閉或者退出,造成本地地址和端口都處於TIME_WAIT狀態,那麼SO_REUSEADDR就顯得非常有用。
也許我們無法避免被凍結在CLOSE_WAIT狀態永遠不出現,但起碼可以保證不會佔用新的端口。
其次,我們要設置SO_LINGER套接字選項:
從容關閉還是強行關閉?
LINGER是“拖延”的意思。
默認情況下(Win2k),SO_DONTLINGER套接字選項的是1;SO_LINGER選項是,linger爲{l_onoff:0,l_linger:0}。
如果在發送數據的過程中(send()沒有完成,還有數據沒發送)而調用了closesocket(),以前我們一般採取的措施是“從容關閉”:
因爲在退出服務或者每次重新建立socket之前,我都會先調用
/// 先將雙向的通訊關閉
     shutdown(sockConnected, SD_BOTH);
     /// 安全起見,每次建立Socket連接前,先把這個舊連接關閉
closesocket(sockConnected);
 
我們這次要這麼做:
設置SO_LINGER爲零(亦即linger結構中的l_onoff域設爲非零,但l_linger爲0),便不用擔心closesocket調用進入“鎖定”狀態(等待完成),不論是否有排隊數據未發送或未被確認。這種關閉方式稱爲“強行關閉”,因爲套接字的虛電路立即被複位,尚未發出的所有數據都會丟失。在遠端的recv()調用都會失敗,並返回WSAECONNRESET錯誤。
在connect成功建立連接之後設置該選項:
linger m_sLinger;
m_sLinger.l_onoff = 1;  // (在closesocket()調用,但是還有數據沒發送完畢的時候容許逗留)
m_sLinger.l_linger = 0; // (容許逗留的時間爲0秒)
setsockopt(sockConnected,
         SOL_SOCKET,
         SO_LINGER,
         (const char*)&m_sLinger,
         sizeof(linger));

 
總結
也許我們避免不了CLOSE_WAIT狀態凍結的再次出現,但我們會使影響降到最小,希望那個重用套接字選項能夠使得下一次重新建立連接時可以把CLOSE_WAIT狀態踢掉。

Feedback
# 回覆:[Socket]尷尬的CLOSE_WAIT狀態以及應對策略 2005-01-30 3:41 PM yun.zheng 
回覆人: elssann(臭屁蟲和他的開心果) ( ) 信譽:51 2005-01-30 14:00:00 得分: 0


我的意思是:當一方關閉連接後,另外一方沒有檢測到,就導致了CLOSE_WAIT的出現,上次我的一個朋友也是這樣,他寫了一個客戶端和 APACHE連接,當APACHE把連接斷掉後,他沒檢測到,出現了CLOSE_WAIT,後來我叫他檢測了這個地方,他添加了調用 closesocket的代碼後,這個問題就消除了。 
如果你在關閉連接前還是出現CLOSE_WAIT,建議你取消shutdown的調用,直接兩邊closesocket試試。


另外一個問題:

比如這樣的一個例子: 
當客戶端登錄上服務器後,發送身份驗證的請求,服務器收到了數據,對客戶端身份進行驗證,發現密碼錯誤,這時候服務器的一般做法應該是先發送一個密碼錯誤的信息給客戶端,然後把連接斷掉。

如果把 
m_sLinger.l_onoff = 1; 
m_sLinger.l_linger = 0; 
這樣設置後,很多情況下,客戶端根本就收不到密碼錯誤的消息,連接就被斷了。

# 回覆:[Socket]尷尬的CLOSE_WAIT狀態以及應對策略 2005-01-30 3:41 PM yun.zheng 
elssann(臭屁蟲和他的開心果) ( ) 信譽:51 2005-01-30 13:24:00 得分: 0


出現CLOSE_WAIT的原因很簡單,就是某一方在網絡連接斷開後,沒有檢測到這個錯誤,沒有執行closesocket,導致了這個狀態的實現,這在TCP/IP協議的狀態變遷圖上可以清楚看到。同時和這個相對應的還有一種叫TIME_WAIT的。

另外,把SOCKET的SO_LINGER設置爲0秒拖延(也就是立即關閉)在很多時候是有害處的。 
還有,把端口設置爲可複用是一種不安全的網絡編程方法。


# 回覆:[Socket]尷尬的CLOSE_WAIT狀態以及應對策略 2005-01-30 3:42 PM yun.zheng 
elssann(臭屁蟲和他的開心果) ( ) 信譽:51 2005-01-30 14:48:00 得分: 0


能不能解釋請看這裏 
http://blog.csdn.net/cqq/archive/2005/01/26/269160.aspx

再看這個圖:

http://tech.ccidnet.com/pub/attachment/2004/8/322252.png

斷開連接的時候, 
當發起主動關閉的左邊這方發送一個FIN過去後,右邊被動關閉的這方要回應一個ACK,這個ACK是TCP迴應的,而不是應用程序發送的,此時,被動關閉的一方就處於CLOSE_WAIT狀態了。如果此時被動關閉的這一方不再繼續調用closesocket,那麼他就不會發送接下來的FIN,導致自己老是處於CLOSE_WAIT。只有被動關閉的這一方調用了closesocket,纔會發送一個FIN給主動關閉的這一方,同時也使得自己的狀態變遷爲LAST_ACK。


# 回覆:[Socket]尷尬的CLOSE_WAIT狀態以及應對策略 2005-01-30 3:54 PM yun.zheng 
elssann(臭屁蟲和他的開心果) ( ) 信譽:51 2005-01-30 15:39:00 得分: 0


比如被動關閉的是客戶端。。。

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

int nRet = recv(s,....); 
if (nRet == SOCKET_ERROR) 

// closesocket(s); 
return FALSE; 
}

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

我的理解,當主動關閉的一方發送FIN到被動關閉這邊後,被動關閉這邊的TCP馬上回應一個ACK過去,同時向上面應用程序提交一個ERROR,導致上面的SOCKET的send或者recv返回SOCKET_ERROR,正常情況下,如果上面在返回SOCKET_ERROR後調用了 closesocket,那麼被動關閉的者一方的TCP就會發送一個FIN過去,自己的狀態就變遷到LAST_ACK.


# 回覆:[Socket]尷尬的CLOSE_WAIT狀態以及應對策略 2005-01-30 4:17 PM yun.zheng 
int nRecvBufLength = 
recv(sockConnected, 
szRecvBuffer, 
sizeof(szRecvBuffer), 
0); 
/// zhengyun 20050130: 
/// elssann舉例說,當對方調用closesocket的時候,我的程序正在 
/// recv,這時候有可能對方發送的FIN包我沒有收到,而是由TCP代回了 
/// 一個ACK包,所以我這邊程序進入CLOSE_WAIT狀態。 
/// 所以他建議在這裏判斷是否已出錯,是就主動closesocket。 
/// 因爲前面我們已經設置了recv超時時間爲30秒,那麼如果真的是超時了, 
/// 這裏收到的錯誤應該是WSAETIMEDOUT,這種情況下也可以關閉連接的 
if (nRecvBufLength == SOCKET_ERROR) 

TRACE_INFO(_T("=用recv接收發生Socket錯誤=")); 
closesocket(sockConnected); 
continue; 
}

這樣可以嗎?

網絡連接無法釋放—— CLOSE_WAIT
關鍵字:TCP ,CLOSE_WAIT, Java, SocketChannel

 

問題描述:最近性能測試碰到的一個問題。客戶端使用NIO,服務器還是一般的Socket連接。當測試進行一段時間以後,發現服務器端的系統出現大量未釋放的網絡連接。用netstat -na查看,連接狀態爲CLOSE_WAIT。這就奇怪了,爲什麼Socket已經關閉而連接依然未釋放。

解決:Google了半天,發現關於CLOSE_WAIT的問題一般是C的,Java似乎碰到這個問題的不多(這有一篇不錯的,也是解決CLOSE_WAIT的,但是好像沒有根本解決,而是選擇了一個折中的辦法)。接着找,由於使用了NIO,所以懷疑可能是這方面的問題,結果找到了這篇。順着帖子翻下去,其中有幾個人說到了一個問題—— 一端的Socket調用close後,另一端的Socket沒有調用close.於是查了一下代碼,果然發現Server端在某些異常情況時,沒有關閉Socket。改正後問題解決。

時間基本上花在Google上了,不過也學到不少東西。下面爲一張TCP連接的狀態轉換圖:

說明:虛線和實線分別對應服務器端(被連接端)和客戶端端(主動連接端)。

結合上圖使用netstat -na命令即可知道到當前的TCP連接狀態。一般LISTEN、ESTABLISHED、TIME_WAIT是比較常見。

分析:

上面我碰到的這個問題主要因爲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. 不過,根本問題還是程序寫的不好,有待提高。

TIME_WAIT狀態
根據TCP協議,主動發起關閉的一方,會進入TIME_WAIT狀態,持續2*MSL(Max Segment Lifetime),缺省爲240秒,在這個post中簡潔的介紹了爲什麼需要這個狀態。

值得一說的是,對於基於TCP的HTTP協議,關閉TCP連接的是Server端,這樣,Server端會進入TIME_WAIT狀態,可想而知,對於訪問量大的Web Server,會存在大量的TIME_WAIT狀態,假如server一秒鐘接收1000個請求,那麼就會積壓240*1000=240,000個 TIME_WAIT的記錄,維護這些狀態給Server帶來負擔。當然現代操作系統都會用快速的查找算法來管理這些TIME_WAIT,所以對於新的 TCP連接請求,判斷是否hit中一個TIME_WAIT不會太費時間,但是有這麼多狀態要維護總是不好。

HTTP協議1.1版規定default行爲是Keep-Alive,也就是會重用TCP連接傳輸多個 request/response,一個主要原因就是發現了這個問題。還有一個方法減緩TIME_WAIT壓力就是把系統的2*MSL時間減少,因爲 240秒的時間實在是忒長了點,對於Windows,修改註冊表,在HKEY_LOCAL_MACHINE/ SYSTEM/CurrentControlSet/Services/ Tcpip/Parameters上添加一個DWORD類型的值TcpTimedWaitDelay,一般認爲不要少於60,不然可能會有麻煩。

對於大型的服務,一臺server搞不定,需要一個LB(Load Balancer)把流量分配到若干後端服務器上,如果這個LB是以NAT方式工作的話,可能會帶來問題。假如所有從LB到後端Server的IP包的 source address都是一樣的(LB的對內地址),那麼LB到後端Server的TCP連接會受限制,因爲頻繁的TCP連接建立和關閉,會在server上留下TIME_WAIT狀態,而且這些狀態對應的remote address都是LB的,LB的source port撐死也就60000多個(2^16=65536,1~1023是保留端口,還有一些其他端口缺省也不會用),每個LB上的端口一旦進入 Server的TIME_WAIT黑名單,就有240秒不能再用來建立和Server的連接,這樣LB和Server最多也就能支持300個左右的連接。如果沒有LB,不會有這個問題,因爲這樣server看到的remote address是internet上廣闊無垠的集合,對每個address,60000多個port實在是夠用了。

一開始我覺得用上LB會很大程度上限制TCP的連接數,但是實驗表明沒這回事,LB後面的一臺Windows Server 2003每秒處理請求數照樣達到了600個,難道TIME_WAIT狀態沒起作用?用Net Monitor和netstat觀察後發現,Server和LB的XXXX端口之間的連接進入TIME_WAIT狀態後,再來一個LB的XXXX端口的 SYN包,Server照樣接收處理了,而是想像的那樣被drop掉了。翻書,從書堆裏面找出覆滿塵土的大學時代買的《UNIX Network Programming, Volume 1, Second Edition: Networking APIs: Sockets and XTI》,中間提到一句,對於BSD-derived實現,只要SYN的sequence number比上一次關閉時的最大sequence number還要大,那麼TIME_WAIT狀態一樣接受這個SYN,難不成Windows也算BSD-derived?有了這點線索和關鍵字 (BSD),找到這個post,在NT4.0的時候,還是和BSD-derived不一樣的,不過Windows Server 2003已經是NT5.2了,也許有點差別了。

做個試驗,用Socket API編一個Client端,每次都Bind到本地一個端口比如2345,重複的建立TCP連接往一個Server發送Keep-Alive=false 的HTTP請求,Windows的實現讓sequence number不斷的增長,所以雖然Server對於Client的2345端口連接保持TIME_WAIT狀態,但是總是能夠接受新的請求,不會拒絕。那如果SYN的Sequence Number變小會怎麼樣呢?同樣用Socket API,不過這次用Raw IP,發送一個小sequence number的SYN包過去,Net Monitor裏面看到,這個SYN被Server接收後如泥牛如海,一點反應沒有,被drop掉了。

按照書上的說法,BSD-derived和Windows Server 2003的做法有安全隱患,不過至少這樣至少不會出現TIME_WAIT阻止TCP請求的問題,當然,客戶端要配合,保證不同TCP連接的sequence number要上漲不要下降。


本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/jamex/archive/2009/11/17/4823405.aspx


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章