深入理解Linux下的Socket異常
在各種網(wǎng)絡(luò)異常情況的背后,TCP是怎么處理的?又是怎樣把處理結(jié)果反饋給上層應(yīng)用的?本文就來討論這個(gè)問題,分為兩個(gè)場(chǎng)景來討論。
建立連接時(shí)的異常情況
1.正常情況下
經(jīng)過三次握手,客戶端連接成功,服務(wù)端有一個(gè)新連接到來。
2.客戶端連接了服務(wù)端未監(jiān)聽的端口
在這種情況下,服務(wù)端會(huì)對(duì)收到的SYN回應(yīng)一個(gè)RST(RFC 793 3.4),客戶端收到RST之后,終止連接,并進(jìn)入CLOSED狀態(tài)。
客戶端的connect返回ECONNREFUSED 111 /* Connection refused */。
3.客戶端與服務(wù)器之間的網(wǎng)絡(luò)不通,這又分兩種情況:
connect返回主機(jī)不可達(dá)。具體信息在不同系統(tǒng)上不一樣,比如linux上的定義是EHOSTUNREACH 113 /* No route to host */。明顯給出了一個(gè)不可訪問的地址(例如,訪問一個(gè)不存在的本地網(wǎng)絡(luò)地址,或者DNS解析失敗會(huì)導(dǎo)致這種情況。
connect返回連接超時(shí)。這種情況下,客戶端發(fā)送的SYN丟失在網(wǎng)絡(luò)中,沒有得到確認(rèn),客戶端的TCP會(huì)超時(shí)重發(fā)SYN。以Ubuntu 12.04為例,重發(fā)SYN的時(shí)間,系列是:0,1,3,7,15,31,63(2n-1-1)。即發(fā)送7個(gè)SYN后等待一個(gè)超時(shí)時(shí)間(例如:127秒),如果在這段時(shí)間內(nèi)仍然沒有收到ACK,則connect返回超時(shí)。
在這兩種情況下, 服務(wù)端的狀態(tài)沒有變化,對(duì)服務(wù)端來講什么也沒發(fā)生。
4.建立連接的過程中包丟失
三次握手發(fā)送的包系列是SYN > SYN-ACK > ACK
SYN丟失。這種情況就是3種的第2種情況。
SYN-ACK丟失。從客戶端的角度來講以前面一種情況類似。從服務(wù)端的角度來講,由LISTEN狀態(tài)進(jìn)入SYN_REVD狀態(tài)。服務(wù)端的TCP會(huì)重發(fā)SYN-ACK,直到超時(shí)。SYN攻擊正是利用這一原理,攻擊方偽造大量的SYN包發(fā)送到服務(wù)器,服務(wù)器對(duì)收到的SYN包不斷回應(yīng)SYN-ACK,直到超時(shí)。這會(huì)浪費(fèi)服務(wù)器大量的資源,甚至導(dǎo)致奔潰。對(duì)服務(wù)端的應(yīng)用層來講,什么也沒有發(fā)生。因?yàn)門CP只有在經(jīng)過3次握手之后才回通知應(yīng)用層,有新的連接到來。
ACK丟失。這對(duì)服務(wù)端來講與2相同。對(duì)于客戶端來講,由SYN_SENT狀態(tài)進(jìn)入了ESTABLISED狀態(tài),即連接成功了。連接成功后客戶端就可以發(fā)送數(shù)據(jù)了。
但實(shí)際上數(shù)據(jù)是發(fā)送不到服務(wù)端的(我們假設(shè)客戶端收到SYN-ACK之后,客戶端與服務(wù)端之間的網(wǎng)絡(luò)就斷開了),客戶端發(fā)送出去的數(shù)據(jù)得不到確認(rèn),一般重發(fā)3次左右就會(huì)處于等待ACK的狀態(tài)(win7)。而ubuntu 12.10下,調(diào)用send會(huì)返回成功,直到TCP的緩沖被填滿(測(cè)試環(huán)境:局域網(wǎng),感覺這個(gè)不是很合理,按照書上所說:應(yīng)該是使用“指數(shù)退避”進(jìn)行重傳 -- TCP/IP協(xié)議詳解, 大概是我的測(cè)試環(huán)境中有NAT所致吧)。最終,客戶端產(chǎn)生一個(gè)復(fù)位信號(hào)并終止連接。返回給應(yīng)用程序的結(jié)果是Connection time out(errno: 110)
連接建立成功后出現(xiàn)的異常情況
1.客戶端與服務(wù)器的網(wǎng)絡(luò)斷開,雙方不再發(fā)送數(shù)據(jù)
這樣,雙方都不知道網(wǎng)絡(luò)已經(jīng)不通,會(huì)一直保持ESTABLISHDED狀態(tài),除非打開了SO_KEEPALIVE選項(xiàng)。
2.網(wǎng)絡(luò)斷開,一方給另一方發(fā)送數(shù)據(jù)
這種情況下,接收一方不知道網(wǎng)絡(luò)出問題,會(huì)一直等待數(shù)據(jù)到來。對(duì)于發(fā)送方,理論上的情況是,重傳一定次數(shù)后,返回連接超時(shí)。不過實(shí)際,很可能是這樣的情況,發(fā)送方顯示發(fā)送數(shù)據(jù)成功(send返回發(fā)送的數(shù)據(jù)長(zhǎng)度),但實(shí)際接收方還沒有接收到數(shù)據(jù)。
對(duì)于已經(jīng)發(fā)送成功的數(shù)據(jù)有3種可能情況:
- 在本機(jī)的TCP緩存中
- 在網(wǎng)絡(luò)上的某個(gè)NAT的緩存中
- 對(duì)方已經(jīng)成功接收到
在實(shí)驗(yàn)的過程中發(fā)現(xiàn),即使網(wǎng)絡(luò)斷開了,發(fā)送方仍然收到了對(duì)數(shù)據(jù)的ACK(在有NAT的情況下),猜測(cè)是NAT把數(shù)據(jù)緩存起來并發(fā)送了ACK。
當(dāng)網(wǎng)絡(luò)恢復(fù)時(shí),那些被緩存的數(shù)據(jù)會(huì)被發(fā)送到接收方。鑒于這樣的結(jié)果,給我們一個(gè)提示:不能依賴于TCP的可靠性,認(rèn)為我發(fā)送成功的數(shù)據(jù),對(duì)方一定能收到。TCP可以保證可靠、有序的傳輸,這意思是說保證收到的數(shù)據(jù)時(shí)有序正確的,并沒有說已經(jīng)發(fā)送成功的數(shù)據(jù),對(duì)方一定就收到了。
在ubuntu 12.10上,發(fā)送方一直在發(fā)送數(shù)據(jù),直到緩沖區(qū)滿。而在win7下,重發(fā)3次就會(huì)停止,進(jìn)入等待ACK狀態(tài)。
解決的辦法是:應(yīng)用層對(duì)數(shù)據(jù)是否接收完成進(jìn)行確認(rèn)(需要的時(shí)候)。
3.網(wǎng)絡(luò)斷開,一方等待著另一方發(fā)送數(shù)據(jù)
這種情況下,等待數(shù)據(jù)的一方將一直等待下去。接收方無法直接知道網(wǎng)絡(luò)已經(jīng)斷開,一般是設(shè)置一個(gè)超時(shí)時(shí)間,超時(shí)時(shí)間到就判斷為網(wǎng)絡(luò)已斷開。發(fā)送數(shù)據(jù)的一方的反應(yīng)如2所述。
4.一方crash,另一方繼續(xù)發(fā)送/接收數(shù)據(jù)
這依賴于TCP協(xié)議棧對(duì)crash的反應(yīng)。與系統(tǒng)相關(guān)性很大,例如:
在windows下:按ctrl+c結(jié)束程序,會(huì)發(fā)送RST段。而在linux下,按ctrl+c結(jié)束程序,會(huì)調(diào)用close。
在wind7下,如果沒有調(diào)用close而結(jié)束程序,TCP會(huì)發(fā)送RST。而Ubuntu12.10上,則會(huì)發(fā)送FIN段。
1).crash的一端發(fā)送FIN,相當(dāng)于調(diào)用了close
沒有crash的一端接收數(shù)據(jù),具體的反應(yīng)與系統(tǒng)有關(guān),例如
linux 3.8.0-29-generic調(diào)用recv返回-1,errno被設(shè)置為22,Invalid argument,而linux3.3.6-030306-generic調(diào)用recv返回0.在TCP內(nèi)部,調(diào)用recv時(shí),發(fā)送FIN,終止連接(Linux)。
windows情況以此不同,recv返回0,表示對(duì)方調(diào)用了shutdown。TCP內(nèi)部發(fā)送一個(gè)RST。
但共同點(diǎn)是recv都會(huì)立即返回失敗。
沒有crash的一端發(fā)送數(shù)據(jù)
第一次調(diào)用send返回成功,數(shù)據(jù)會(huì)被發(fā)送到crash的一端,crash的一端會(huì)回應(yīng)一個(gè)RST,再次調(diào)用send返回-1, errno被設(shè)置為32, Broken pipe。 注意:這會(huì)向應(yīng)用程序發(fā)送SIGPIPE信號(hào),你的程序會(huì)莫名其妙退出。這是因?yàn)槌绦驅(qū)IGPIPE的默認(rèn)處理就是結(jié)束程序。
這是編寫服務(wù)器程序是最需要注意的一個(gè)問題。最簡(jiǎn)單的處理方法是忽略該信號(hào) -- signal(SIGPIPE,SIG_IGN);
windows下行為是一樣的, 不同的是返回的錯(cuò)誤是10053 - WSAECONNABORTED, 由于軟件錯(cuò)誤,造成一個(gè)已經(jīng)建立的連接被取消。
共同點(diǎn)第一次send成功,之后就出錯(cuò)。
2).crash的一端發(fā)送RST
沒有crash的一端接收數(shù)據(jù)
調(diào)用recv返回-1,errno被設(shè)置為104, Connection reset by peer。在TCP內(nèi)部,當(dāng)收到RST時(shí),把錯(cuò)誤號(hào)設(shè)為ECONNRESET。
沒有crash的一端發(fā)送數(shù)據(jù)
調(diào)用send返回-1,errno被設(shè)置為104, Connection reset by peer。在TCP內(nèi)部,當(dāng)收到RST時(shí),把錯(cuò)誤號(hào)設(shè)為ECONNRESET
3).crash的一端即沒發(fā)送FIN也沒發(fā)送RST
沒有crash的一端接收數(shù)據(jù)
調(diào)用recv會(huì)一直阻塞等待數(shù)據(jù)到來
沒有crash的一端發(fā)送數(shù)據(jù)
重傳一定次數(shù)后,返回connection time out。
5.一端關(guān)閉連接
這種情況與一端crash并發(fā)送FIN 的情況相同,參看4.1
總結(jié)
上面分析的目的是:當(dāng)程序出現(xiàn)網(wǎng)絡(luò)異常時(shí),能夠知道問題的原因在哪?
作為開發(fā)者,我們主要關(guān)心應(yīng)用層面的返回狀態(tài)。一般出錯(cuò)的地方是調(diào)用connect, recv, send的時(shí)候。
下面做一個(gè)總結(jié)
connect函數(shù)返回狀態(tài)及其原因
recv函數(shù)返回狀態(tài)及其原因
send函數(shù)返回狀態(tài)及其原因
各種不同步的狀態(tài),都是通過發(fā)送RST來恢復(fù)的,理解這些狀況的關(guān)鍵在于理解何時(shí)產(chǎn)生RST,以及在各種狀態(tài)下,對(duì)RST段如何處理。