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