Tcp關(guān)閉連接問(wèn)題及注意
最近一段時(shí)間一直在學(xué)習(xí)閱讀mina和nio的源碼,也發(fā)現(xiàn)了一些問(wèn)題無(wú)法解決,然后重讀了一下tcp協(xié)議,收獲頗多。(這就是帶著問(wèn)題去讀書(shū)的好處)
這次就和大家分享一下我們的netframework服務(wù)總會(huì)拋出一個(gè)“connet reset by peer”的原因吧。通過(guò)抓包工具分析,主動(dòng)關(guān)閉方直接發(fā)送了一個(gè)RST flags,而非FIN。就終止連接了。如下圖所示:

為什么調(diào)用sokcet的close時(shí)只通過(guò)一次握手就終結(jié)連接了?
要分析這個(gè)原因那就得從關(guān)閉連接程的四次握手,有時(shí)也會(huì)是三次握手,說(shuō)起。如下圖所示:

大家都知道tcp正常的關(guān)閉連接要經(jīng)過(guò)四次握手。如下所示:

在這四次握手狀態(tài)中,有一個(gè)特別要注意的狀態(tài)TIME_WAIT。這個(gè)狀態(tài)是主動(dòng)關(guān)閉方在收到被關(guān)閉方的FIN后會(huì)處于并長(zhǎng)期(2個(gè)MSL時(shí)間,根據(jù)具體的實(shí)現(xiàn)不同,這個(gè)值會(huì)不同,在RFC 1122建議MSL=2分鐘,但在Berkeley的實(shí)現(xiàn)上使用的值為30s,具體可以看www.rfc.net ,要是沒(méi)有耐心去看英文的可以看這個(gè)網(wǎng)站www.cnpaf.net 里面有協(xié)議說(shuō)明以及相應(yīng)的源碼,java源碼中我沒(méi)有發(fā)現(xiàn)這個(gè)值,我只能追蹤到PlainSocketImpl.java這個(gè)類,再往下就是本地接口調(diào)用了,因此它是依賴本地操作系統(tǒng)的實(shí)現(xiàn))處于的一個(gè)狀態(tài)。也就是大約1-4分鐘,然后由操作系統(tǒng)自動(dòng)回收并將TCP連接設(shè)為CLOSED初始狀態(tài)。如下圖所示:

然而在socket的處于TIME_WAIT狀態(tài)之后到它結(jié)束之前,該socket所占用的本地端口號(hào)將一直無(wú)法釋放,因此服務(wù)在高并發(fā)高負(fù)載下運(yùn)行一段時(shí)間后,就常常會(huì)出現(xiàn)做為客戶端的程序無(wú)法向服務(wù)端建立新的socket連接的情況,過(guò)了1~4分鐘之后,客戶又可以連接上了,沒(méi)多久又連接不上,再等1~4分鐘之后又可以連接上,(上一個(gè)星期我們?cè)谧鲆粋€(gè)服務(wù)切換時(shí)遇到了這種情況)

這是因?yàn)榉?wù)方socket資源已經(jīng)耗盡。netstat命令查看系統(tǒng)將會(huì)發(fā)現(xiàn)機(jī)器上存在大量處于TIME_WAIT狀態(tài)的socket連接,我這邊曾經(jīng)出現(xiàn)達(dá)到了2w多個(gè),并且占用大量的本地端口號(hào)。而此時(shí)機(jī)器上的可用本地端口號(hào)被占完,舊的大量處于TIME_WAIT狀態(tài)的socket尚未被系統(tǒng)回收時(shí),就會(huì)出現(xiàn)無(wú)法向服務(wù)端創(chuàng)建新的socket連接的情況。只能過(guò)2分鐘之后等系統(tǒng)回收這些socket和端口資源之后才能服務(wù),就這樣往復(fù)下去。
TCP為什么要這么要讓這種TIME_WAIT狀態(tài)存活這么久呢?其原因有兩個(gè)(參考stevens的unix網(wǎng)絡(luò)編程卷1 第38頁(yè)):
- 可靠地實(shí)現(xiàn)TCP全雙工連接的終止。(確保最后的ACK能讓被關(guān)閉方接收)
- 允許老的重復(fù)分節(jié)在網(wǎng)絡(luò)中消逝。(TCP中是可靠的服務(wù),當(dāng)數(shù)據(jù)包丟失會(huì)重傳,當(dāng)有數(shù)據(jù)包迷路的情況下,如果不等待2MSL時(shí),當(dāng)客戶端以同樣地方式重新和服務(wù)建立連接后,上一次迷路的數(shù)據(jù)包這時(shí)可能會(huì)到達(dá)服務(wù),這時(shí)會(huì)造成舊包被重新讀取)
解決方法:
1、(推薦方法,只能治標(biāo)不治本)重用本地端口設(shè)置SO_REUSEADDR和SO_REUSEPORT(stevens的unix網(wǎng)絡(luò)編程卷1 第179~182頁(yè))有詳情的講解,這樣就可以允許同一端口上啟動(dòng)同一服務(wù)器的多個(gè)實(shí)例。怎樣理解呢?說(shuō)白了就是即使socket斷了,重新調(diào)用前面的socket函數(shù)不會(huì)再去占用新的一個(gè),而是始終就是一個(gè)端口,這樣防止socket始終連接不上,會(huì)不斷地?fù)Q新端口。Java 中通過(guò)調(diào)用Socket的setReuseAddress,詳細(xì)可以查看java.net.Socket源碼。【這個(gè)地方會(huì)有風(fēng)險(xiǎn),具體可以看(stevens的unix網(wǎng)絡(luò)編程卷1 第181頁(yè))】
2、修改內(nèi)核TIME_WAIT等待的值,如果客戶端和服務(wù)器都在同個(gè)路由器下,這個(gè)是非常推薦的。(鏈路好,重傳機(jī)率低)
3、(不推崇,但目前我們是這樣做的,這個(gè)是造成(“connet reset by peer”)的元兇)設(shè)置SO_LINGER的值,java中是調(diào)用socket的 setSoLinger目前我們是設(shè)置為0的。設(shè)置為這個(gè)值的意思是當(dāng)主動(dòng)關(guān)閉方設(shè)置了setSoLinger(true,0)時(shí),并調(diào)用close后,立該發(fā)送一個(gè)RST標(biāo)志給對(duì)端,該TCP連接將立刻夭折,無(wú)論是否有排隊(duì)數(shù)據(jù)未發(fā)送或未被確認(rèn)。這種關(guān)閉方式稱為“強(qiáng)行關(guān)閉”,而后套接字的虛電路立即被復(fù)位,尚未發(fā)出的所有數(shù)據(jù)都會(huì)丟失。而被動(dòng)關(guān)閉方卻不知道對(duì)端已經(jīng)徹底斷開(kāi)。當(dāng)被動(dòng)關(guān)閉方正阻塞在recv()調(diào)用上時(shí),接受到RST時(shí),會(huì)立刻得到一個(gè)“connet reset by peer”的異常(即對(duì)端已經(jīng)關(guān)閉),c中是返回一個(gè)EPEERRST錯(cuò)。
為什么不推崇這種方法在(stevens的unix網(wǎng)絡(luò)編程卷1 第173頁(yè))有詳細(xì)的講解。因?yàn)門IME_WAIT狀態(tài)是我們的朋友,它是有助有我們的(也就是說(shuō),它會(huì)讓舊的重復(fù)分節(jié)在網(wǎng)絡(luò)中超時(shí)消失(當(dāng)我們的鏈路越長(zhǎng),ISP復(fù)雜的情況下(從網(wǎng)通到教育網(wǎng)的ping包用了9000ms),重復(fù)的分節(jié)的比例是非常高的。))。而且我們主動(dòng)關(guān)閉連接方大都是由客戶端發(fā)起的(除了HTTP服務(wù)和異常),而且客戶方一般都不會(huì)有持續(xù)的大并發(fā)請(qǐng)求。 因此對(duì)資源沒(méi)有這么苛刻要求。