首頁 > 軟體

Linux-深入理解Socket異常

2020-06-16 17:25:12

在各種網路異常情況的背後,TCP是怎麼處理的?又是怎樣把處理結果反饋給上層應用的?本文就來討論這個問題。分為兩個場景來討論

建立連線時的異常情況

1 正常情況下
  經過三次握手,用戶端連線成功,伺服器端有一個新連線到來。

 

2 用戶端連線了伺服器端未監聽的埠
  在這種情況下,伺服器端會對收到的SYN回應一個RST(RFC 793 3.4),用戶端收到RST之後,終止連線,並進入CLOSED狀態。
用戶端的connect返回ECONNREFUSED 111 /* Connection refused */。


3 用戶端與伺服器之間的網路不通,這又分兩種情況
  connect返回主機不可達。具體資訊在不同系統上不一樣,比如linux上的定義是EHOSTUNREACH 113 /* No route to host */。明顯給出了一個不可存取的地址(例如,存取一個不存在的本地網路地址,或者DNS解析失敗會導致這種情況。
connect返回連線超時。這種情況下,用戶端傳送的SYN丟失在網路中,沒有得到確認,用戶端的TCP會超時重發SYN。以Ubuntu 12.04為例,重發SYN的時間,系列是:0,1,3,7,15,31,63(2n-1-1)。即傳送7個SYN後等待一個超時時間(例如:127秒),如果在這段時間內仍然沒有收到ACK,則connect返回超時。
  在這兩種情況下, 伺服器端的狀態沒有變化,對伺服器端來講什麼也沒發生。

4 建立連線的過程中包丟失
  三次握手傳送的包系列是SYN > SYN-ACK > ACK
  SYN丟失。這種情況就是3種的第2種情況。
  SYN-ACK丟失。從用戶端的角度來講以前面一種情況類似。從伺服器端的角度來講,由LISTEN狀態進入SYN_REVD狀態。伺服器端的TCP會重發SYN-ACK,直到超
時。SYN攻擊正是利用這一原理,攻擊方偽造大量的SYN包傳送到伺服器,伺服器對收到的SYN包不斷回應SYN-ACK,直到超時。這會浪費服務
器大量的資源,甚至導致奔潰。對伺服器端的應用層來講,什麼也沒有發生。因為TCP只有在經過3次握手之後才回通知應用層,有新的連線到來。


  ACK丟失。這對伺服器端來講與2相同。對於用戶端來講,由SYN_SENT狀態進入了ESTABLISED狀態,即連線成功了。連線成功後用戶端就可以傳送資料了。
但實際上資料是傳送不到伺服器端的(我們假設用戶端收到SYN-ACK之後,用戶端與伺服器端之間的網路就斷開了),用戶端傳送出去的資料得不
到確認,一般重發3次左右就會處於等待ACK的狀態(win7)。而ubuntu 12.10下,呼叫send會返回成功,直到TCP的緩衝被填滿(測試環
境:區域網,感覺這個不是很合理,按照書上所說:應該是使用“指數退避”進行重傳 -- TCP/IP協定詳解, 大概是我的測試環境中有NAT所致
吧)。最終,用戶端產生一個復位信號並終止連線。返回給應用程式的結果是Connection time out(errno: 110)

 

連線建立成功後出現的異常情況
1 用戶端與伺服器的網路斷開,雙方不再傳送資料
  這樣,雙方都不知道網路已經不通,會一直保持ESTABLISHDED狀態,除非開啟了SO_KEEPALIVE選項。

2 網路斷開,一方給另一方傳送資料
  這種情況下,接收一方不知道網路出問題,會一直等待資料到來。對於傳送方,理論上的情況是,重傳一定次數後,返回連線超時。不過實際,很可能是這樣的情況,傳送方顯示傳送資料成功(send返回傳送的資料長度),但實際接收方還沒有接收到資料。
  對於已經傳送成功的資料有3種可能情況:
    1 在本機的TCP快取中
    2 在網路上的某個NAT的快取中
    3 對方已經成功接收到
  在實驗的過程中發現,即使網路斷開了,傳送方仍然收到了對資料的ACK(在有NAT的情況下),猜測是NAT把資料快取起來並行送了ACK。
  當網路恢復時,那些被快取的資料會被傳送到接收方。鑑於這樣的結果,給我們一個提示:不能依賴於TCP的可靠性,認為我傳送成功的資料,對方一
定能收到。TCP可以保證可靠、有序的傳輸,這意思是說保證收到的資料時有序正確的,並沒有說已經傳送成功的資料,對方一定就收到了。
  在ubuntu 12.10上,傳送方一直在傳送資料,直到緩衝區滿。而在win7下,重發3次就會停止,進入等待ACK狀態。
解決的辦法是:應用層對資料是否接收完成進行確認(需要的時候)。

3 網路斷開,一方等待著另一方傳送資料
  這種情況下,等待資料的一方將一直等待下去。接收方無法直接知道網路已經斷開,一般是設定一個超時時間,超時時間到就判斷為網路已斷開。傳送
資料的一方的反應如2所述。

4 一方crash,另一方繼續傳送/接收資料
  這依賴於TCP協定棧對crash的反應。與系統相關性很大,例如:
    在windows下:按ctrl+c結束程式,會傳送RST段。而在linux下,按ctrl+c結束程式,會呼叫close。
    在wind7下,如果沒有呼叫close而結束程式,TCP會傳送RST。而Ubuntu12.10上,則會傳送FIN段。

  1.crash的一端傳送FIN,相當於呼叫了close
    沒有crash的一端接收資料,具體的反應與系統有關,例如
      linux 3.8.0-29-generic呼叫recv返回-1,errno被設定為22,Invalid argument,而linux3.3.6-030306-generic呼叫recv返回0.在TCP內部,呼叫recv時,傳送FIN,終止連線(Linux)。
      windows情況以此不同,recv返回0,表示對方呼叫了shutdown。TCP內部傳送一個RST。
    但共同點是recv都會立即返回失敗。

    沒有crash的一端傳送資料
      第一次呼叫send返回成功,資料會被傳送到crash的一端,crash的一端會回應一個RST,再次呼叫send返回-1, errno被設定為32, Broken pipe。 注意:這會向應用程式傳送SIGPIPE信號,你的程式會莫名其妙退出。這是因為程式對SIGPIPE的預設處理就是結束程式。
      這是編寫伺服器程式是最需要注意的一個問題。最簡單的處理方法是忽略該信號 -- signal(SIGPIPE,SIG_IGN);
windows下行為是一樣的, 不同的是返回的錯誤是10053 - WSAECONNABORTED, 由於軟體錯誤,造成一個已經建立的連線被取消。
    共同點第一次send成功,之後就出錯。

  2.crash的一端傳送RST
    沒有crash的一端接收資料
      呼叫recv返回-1,errno被設定為104, Connection reset by peer。在TCP內部,當收到RST時,把錯誤號設為ECONNRESET。
    沒有crash的一端傳送資料
      呼叫send返回-1,errno被設定為104, Connection reset by peer。在TCP內部,當收到RST時,把錯誤號設為ECONNRESET

  3.crash的一端即沒傳送FIN也沒傳送RST
    沒有crash的一端接收資料
      呼叫recv會一直阻塞等待資料到來
    沒有crash的一端傳送資料
      重傳一定次數後,返回connection time out。

5 一端關閉連線
  這種情況與一端crash並行送FIN 的情況相同,參看4.1

總結
  上面分析的目的是:當程式出現網路異常時,能夠知道問題的原因在哪?

  作為開發者,我們主要關心應用層面的返回狀態。一般出錯的地方是呼叫connect, recv, send的時候。
  下面做一個總結
    connect函數返回狀態及其原因

    recv函數返回狀態及其原因

    send函數返回狀態及其原因

  各種不同步的狀態,都是通過傳送RST來恢復的,理解這些狀況的關鍵在於理解何時產生RST,以及在各種狀態下,對RST段如何處理。

本文永久更新連結地址http://www.linuxidc.com/Linux/2016-12/138767.htm


IT145.com E-mail:sddin#qq.com