socket連接關(guān)閉問題分析
socket編程過(guò)程中往往會(huì)遇到這樣那樣的問題,出現(xiàn)了這些問題,有的是由于并發(fā)訪問量太大造成的,有些卻是由于代碼中編程不慎造成的。比如說(shuō),最常見的錯(cuò)誤就是程序中報(bào)打開的文件數(shù)過(guò)多這個(gè)錯(cuò)誤。socket建立連接的時(shí)候是三次握手,這個(gè)大家都很清楚,但是socket關(guān)閉連接的時(shí)候,需要進(jìn)行四次揮手,但很多人對(duì)于這四次揮手的具體流程不清楚,吃了很多虧。
CLOSE_WAIT分析
socket是一種全雙工的通信方式,建立完socket連接后,連接的任何一方都可以發(fā)起關(guān)閉操作。這里不妨假設(shè)連接的關(guān)閉是客戶端發(fā)起??蛻舳说拇a如下:
代碼片段1.1
ret = CS_GetConnect(&client,ipAddr,9010); if (ret == 0) { printf("connected success."); } CloseSocket(client);
基本邏輯就是,連接建立后立即關(guān)閉。其中CloseSocket函數(shù)是自定義函數(shù),僅僅封裝了在windows和linux下關(guān)閉socket的不同實(shí)現(xiàn)而已
代碼片段1.2
#if defined(WIN32) || defined(WIN64) #define CloseSocket(fd) do{ closesocket(fd);/* shutdown(fd, 2);*/ }while(0) #else #define CloseSocket(fd) do{ close(fd); /*shutdown(fd,2);*/ }while(0) #endif
客戶端調(diào)用了CloseSocket之后,發(fā)送FIN信號(hào)到服務(wù)器端,告訴socket程序,連接已經(jīng)斷開。服務(wù)器端接收到FIN信號(hào)后,會(huì)將自身的TCP狀態(tài)置為`CLOSE_WAIT`,同時(shí)回復(fù) 一個(gè)ACK信號(hào)給客戶端,客戶端接收到這個(gè)ACK信號(hào)后,自身將處于`FIN_WAIT_2`狀態(tài)。
但是tcp是全雙工的通信協(xié)議,雖然客戶端關(guān)閉了連接,但是服務(wù)器端對(duì)于這個(gè)關(guān)閉動(dòng)作不予理睬怎么辦。對(duì)于服務(wù)器端來(lái)說(shuō),這是個(gè)不幸的消息,因?yàn)樗鼘⒁恢碧幱赻CLOSE_WAIT`狀態(tài),雖然客戶端已經(jīng)不需要和服務(wù)器間進(jìn)行通信了,但是服務(wù)器端的socket連接句柄一直得不到釋放;如果老是有這種情況出現(xiàn),久而久之服務(wù)器端的連接句柄就會(huì)被耗盡。對(duì)于發(fā)起關(guān)閉的客戶端來(lái)說(shuō),他處于`FIN_WAIT_2`狀態(tài),如果出現(xiàn)服務(wù)器端一直處于`CLOSE_WATI`狀態(tài)的情況,客戶端并不會(huì)一直處在`FIN_WAIT_2`狀態(tài),因?yàn)檫@個(gè)狀態(tài)有一個(gè)超時(shí)時(shí)間,這個(gè)值可以在/etc/sysctl.conf中進(jìn)行配置。在這個(gè)文件中配置`net.ipv4.tcp_fin_timeout=30`即可保證`FIN_WAIT_2`狀態(tài)最多保持30秒,超過(guò)這個(gè)時(shí)間后就進(jìn)入TIME_WAIT狀態(tài)(下面要講到這個(gè)狀態(tài))。
注意:這里socket的關(guān)閉從客戶端發(fā)起,僅僅是為了舉例說(shuō)明,socket的關(guān)閉完全也可以從服務(wù)器端發(fā)起。比如說(shuō)你寫了一個(gè)爬蟲程序去下載互聯(lián)網(wǎng)上的某些web服務(wù)器上的資源的時(shí)候,某些要下載的web資源不存在,web服務(wù)器會(huì)立即關(guān)閉當(dāng)前的socket連接,但是你的爬蟲程序不夠健壯,對(duì)于這種情況沒有做處理,同樣會(huì)使你的爬蟲客戶端處于CLOSE_WAIT狀態(tài)。
那么怎樣預(yù)防SOCKET處于CLOSE_WATI狀態(tài)呢,答案在這里:
代碼片段1.3
while(true) { memset(getBuffer,0,MY_SOCKET_BUFFER_SIZE); Ret = recv(client, getBuffer, MY_SOCKET_BUFFER_SIZE, 0); if ( Ret == 0 || Ret == SOCKET_ERROR ) { printf("對(duì)方socket已經(jīng)退出,Ret【%d】!\n",Ret); Ret = SOCKET_READE_ERROR;//接收服務(wù)器端信息失敗 break; } } clear: if (getBuffer != NULL) { free(getBuffer); getBuffer = NULL; } closesocket(client);
這里摘錄了服務(wù)器端部分代碼,注意這個(gè)recv函數(shù),這個(gè)函數(shù)在連接建立時(shí),會(huì)堵塞住當(dāng)前代碼,等有數(shù)據(jù)接收成功后才返回,返回值為接收到的字節(jié)數(shù);但是對(duì)于連接對(duì)方socket關(guān)閉情況,它能立即感應(yīng)到,并且返回0.所以對(duì)于返回0的時(shí)候,可以跳出循環(huán),結(jié)束當(dāng)前socket處理,進(jìn)行一些垃圾回收工作,注意最后一句closesocket操作是很重要的,假設(shè)沒有寫這句話,服務(wù)器端會(huì)一直處于CLOSE_WAIT狀態(tài)。如果寫了這句話,那么socket的流程就會(huì)是這樣的:
TIME_WAIT分析
服務(wù)器端調(diào)用了CloseSocket操作后,會(huì)發(fā)送一個(gè)FIN信號(hào)給客戶端,客戶端進(jìn)入`TIME_WAIT`狀態(tài),而且將維持在這個(gè)狀態(tài)一段時(shí)間,這個(gè)時(shí)間也被成為2MSL(MSL是maximum segment lifetime的縮寫,意指最大分節(jié)生命周期,這是IP數(shù)據(jù)包能在互聯(lián)網(wǎng)上生存的最長(zhǎng)時(shí)間,超過(guò)這個(gè)時(shí)間將在互聯(lián)網(wǎng)上消失),在這個(gè)時(shí)間段內(nèi)如果客戶端的發(fā)出的數(shù)據(jù)還沒有被服務(wù)器端確認(rèn)接收的話,可以趁這個(gè)時(shí)間等待服務(wù)端的確認(rèn)消息。注意,客戶端最后發(fā)出的ACK N+1消息,是一進(jìn)入`TIME_WAIT`狀態(tài)后就發(fā)出的,并不是在`TIME_WAIT`狀態(tài)結(jié)束后發(fā)出的。如果在發(fā)送ACK N+1的時(shí)候,由于某種原因服務(wù)器端沒有收到,那么服務(wù)器端會(huì)重新發(fā)送FIN N消息,這個(gè)時(shí)候如果客戶端還處于`TIME_WAIT`狀態(tài)的,會(huì)重新發(fā)送ACK N+1消息,否則客戶端會(huì)直接發(fā)送一個(gè)RST消息,告訴服務(wù)器端socket連接已經(jīng)不存在了。
有時(shí),我們?cè)谑褂胣etstat命令查看web服務(wù)器端的tcp狀態(tài)的時(shí)候,會(huì)發(fā)現(xiàn)有成千上萬(wàn)的連接句柄處在`TIME_WAIT`狀態(tài)。web服務(wù)器的socket連接一般都是服務(wù)器端主動(dòng)關(guān)閉的,當(dāng)web服務(wù)器的并發(fā)訪問量過(guò)大的時(shí)候,由于web服務(wù)器大多情況下是短連接,socket句柄的生命周期比較短,于是乎就出現(xiàn)了大量的句柄堵在`TIME_WAIT`狀態(tài),等待系統(tǒng)回收的情況。如果這種情況太過(guò)頻繁,又由于操作系統(tǒng)本身的連接數(shù)就有限,勢(shì)必會(huì)影響正常的socket連接的建立。在linux下對(duì)于這種情況倒是有解救措施,方法就是修改/etc/sysctl.conf文件,保證里面含有以下三行配置:
配置型 2.1
#表示開啟重用。允許將TIME-WAIT sockets重新用于新的TCP連接,默認(rèn)為0,表示關(guān)閉
net.ipv4.tcp_tw_reuse = 1
#表示開啟TCP連接中TIME-WAIT sockets的快速回收,默認(rèn)為0,表示關(guān)閉
net.ipv4.tcp_tw_recycle = 1
#表示系統(tǒng)同時(shí)保持TIME_WAIT的最大數(shù)量,如果超過(guò)這個(gè)數(shù)字,
#TIME_WAIT將立刻被清除并打印警告信息。默認(rèn)為180000,改為5000。
net.ipv4.tcp_max_tw_buckets = 5000
關(guān)于重用`TIME_WAIT`狀態(tài)的句柄的操作,也可以在代碼中設(shè)置:
代碼片段2.1
int on = 1; if (setsockopt(socketfd/*socket句柄*/,SOL_SOCKET,SO_REUSEADDR,(char *)&on,sizeof(on))) { return ERROR_SET_REUSE_ADDR; }
如果在代碼中設(shè)置了關(guān)于重用的操作,程序中將使用代碼中設(shè)置的選項(xiàng)決定重用或者不重用,/etc/sysctl.conf中`net.ipv4.tcp_tw_reuse`中的設(shè)置將不再其作用。
當(dāng)然這樣設(shè)置是有悖TCP的設(shè)計(jì)標(biāo)準(zhǔn)的,因?yàn)樘幱赻TIME_WAIT`狀態(tài)的TCP連接,是有其存在的積極作用的,前面已經(jīng)介紹過(guò)。假設(shè)客戶端的ACK N+1信號(hào)發(fā)送失敗,服務(wù)器端在1MSL時(shí)間過(guò)后會(huì)重發(fā)FIN N信號(hào),而此時(shí)客戶端重用了之前關(guān)閉的連接句柄建立了新的連接,但是此時(shí)就會(huì)收到一個(gè)FIN信號(hào),導(dǎo)致自己被莫名其妙關(guān)閉。
一般`TIME_WAIT`會(huì)維持在2MSL(linux下1MSL默認(rèn)為30秒)時(shí)間,但是這個(gè)時(shí)間可以通過(guò)代碼修改:
代碼片段2.2
struct linger so_linger; so_linger.l_onoff = 1; so_linger.l_linger = 10; if (setsockopt(socketfd,SOL_SOCKET,SO_LINGER,(char *)&so_linger,sizeof(struct linger))) { return ERROR_SET_LINGER; }
這里代碼將`TIME_WAIT`的時(shí)間設(shè)置為10秒(在BSD系統(tǒng)中,將會(huì)是0.01*10s)。TCP中的`TIME_WAIT`機(jī)制使得socket程序可以“優(yōu)雅”的關(guān)閉,如果你想你的程序更優(yōu)雅,最好不要設(shè)置`TIME_WAIT`的停留時(shí)間,讓老的tcp數(shù)據(jù)包在合理的時(shí)間內(nèi)自生自滅。當(dāng)然對(duì)于`SO_LINGER`參數(shù),它不僅僅能夠自定義`TIME_WAIT`狀態(tài)的時(shí)間,還能夠?qū)CP的四次揮手直接禁用掉,假設(shè)對(duì)于so_linger結(jié)構(gòu)體變量的設(shè)置是這個(gè)樣子的:
so_linger.l_onoff = 1;
so_linger.l_linger = 0;
如果客戶端的socket是這么設(shè)置的那么socket的關(guān)閉流程就直接是這個(gè)樣子了:
這相當(dāng)于客戶端直接告訴服務(wù)器端,我這邊異常終止了,對(duì)于我稍后給出的所有數(shù)據(jù)包你都可以丟棄掉。服務(wù)器端如果接受到這種RST消息,會(huì)直接把對(duì)應(yīng)的socket句柄回收掉。有一些socket程序不想讓TCP出現(xiàn)`TIME_WAIT`狀態(tài),會(huì)選擇直接使用RST方式關(guān)閉socket,以保證socket句柄在最短的時(shí)間內(nèi)得到回收,當(dāng)然前提是接受有可能被丟棄老的數(shù)據(jù)包這種情況的出現(xiàn)。如果socket通信的前后數(shù)據(jù)包的關(guān)聯(lián)性不是很強(qiáng)的話,換句話說(shuō)每次通信都是一個(gè)單獨(dú)的事務(wù),那么可以考慮直接發(fā)送RST信號(hào)來(lái)快速關(guān)閉連接。
補(bǔ)充
1.文中提到的修改/etc/sysctl.conf文件的情況,修改完成之后需要運(yùn)行`/sbin/sysctl -p`后才能生效。
2.圖1中發(fā)送完FIN M信號(hào)后,被動(dòng)關(guān)閉端的socket程序中輸入流會(huì)接收到一個(gè)EOF標(biāo)示,是在C代碼中處理時(shí)recv函數(shù)返回0代表對(duì)方關(guān)閉,在java代碼中會(huì)在InputStream的read函數(shù)中接收到-1:
代碼片段3.1
Socket client = new Socket();//,9090 try { client.connect( new InetSocketAddress("192.168.56.101",9090)); while(true){ int c = client.getInputStream().read(); if (c > 0) { System.out.print((char) c); } else {//如果對(duì)方socket關(guān)閉,read函數(shù)返回-1 break; } try { Thread.currentThread().sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } } catch (IOException e2) { e2.printStackTrace(); } finally { try { client.close(); } catch (IOException e) { e.printStackTrace(); } } }
3.如果主動(dòng)關(guān)閉方已經(jīng)發(fā)起了關(guān)閉的FIN信號(hào),被動(dòng)關(guān)閉方不予理睬,依然往主動(dòng)關(guān)閉方發(fā)送數(shù)據(jù),那么主動(dòng)關(guān)閉方會(huì)直接返回RST新號(hào),連接雙方的句柄就被雙方的操作系統(tǒng)回收,如果此時(shí)雙方的路由節(jié)點(diǎn)之前還存在未到達(dá)的數(shù)據(jù),將會(huì)被丟棄掉。
4.通信的過(guò)程中,socket雙發(fā)中有一方的進(jìn)程意外退出,則這一方將向其對(duì)應(yīng)的另一方發(fā)送RST消息,所有雙發(fā)建立的連接將會(huì)被回收,未接收完的消息就會(huì)被丟棄。
5.項(xiàng)目的配套代碼可以從這里得到http://git.oschina.net/yunnysunny/socket_close
以上就是socket連接關(guān)閉問題分析的詳細(xì)內(nèi)容,更多關(guān)于socket連接關(guān)閉的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
python 經(jīng)緯度求兩點(diǎn)距離、三點(diǎn)面積操作
這篇文章主要介紹了python 經(jīng)緯度求兩點(diǎn)距離、三點(diǎn)面積操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-06-06Python?threading中l(wèi)ock的使用詳解
Lock類是threading中用于鎖定當(dāng)前線程的鎖定類,本文給大家介紹了Python?threading中l(wèi)ock的使用,需要的朋友可以參考下2022-11-11Python面向?qū)ο蟪绦蛟O(shè)計(jì)之私有屬性及私有方法示例
這篇文章主要介紹了Python面向?qū)ο蟪绦蛟O(shè)計(jì)之私有屬性及私有方法,結(jié)合實(shí)例形式分析了Python私有屬性及私有方法的相關(guān)使用方法及操作注意事項(xiàng),需要的朋友可以參考下2019-04-04