當咱們聊到 TCP 協議的時候,聊的最多的就是三次握手與四次揮手。可是大部分資料和文章,寫的都是正常的狀況下的流程。可是你有沒有想過,三次握手或者四次揮手時,若是發生異常了,是如何處理的?又是由誰來處理?linux
TCP 做爲一個靠譜的協議,在傳輸數據的先後,須要在雙端之間創建鏈接,並在雙端各自維護鏈接的狀態。TCP 並無什麼特別之處,在面對多變的網絡狀況,也只能經過不斷的重傳和各類算法來保證可靠性。算法
創建鏈接前,TCP 會經過三次握手來保證雙端狀態正確,而後就能夠正常傳輸數據了。當數據傳輸完成,須要斷開鏈接的時候,TCP 會經過四次握手來完成雙端的斷連,並回收各自的資源。安全
咱們在學習 TCP 建連和斷連時,多數都在說一個標準的流程,可是網絡環境是多變的,不少時候並不像教科書那樣標準,那麼今天就來聊聊,TCP 三次握手和四次揮手時,若是出現異常狀況,是如何處理的?由是由誰來處理?網絡
雖然是說三次握手的異常狀況,咱們仍是先來了解一下三次握手。tcp
在經過 TCP 傳輸數據時,第一步就是要先創建一個鏈接。TCP 創建鏈接的過程,就是咱們常說的三次握手。學習
咱們常常將三次握手,描述成「請求 → 應答 → 應答之應答」。測試
至於 TCP 握手爲何是三次?其實就是要讓雙端都經歷一次「請求 → 應答」的過程,來確認對方還在。網絡狀況是多變的,雙端都須要一次本身主動發起的請求和對方回覆的應答過程,來確保對方和網絡是正常的。spa
下面這張圖,是比較經典的 TCP 三次握手的消息和雙端狀態的變化。操作系統
咱們先來解釋一下這張圖:.net
1. 在初始時,雙端處於 CLOSE 狀態,服務端爲了提供服務,會主動監聽某個端口,進入 LISTEN 狀態。
2. 客戶端主動發送鏈接的「SYN」包,以後進入 SYN-SENT 狀態,服務端在收到客戶端發來的「SYN」包後,回覆「SYN,ACK」包,以後進入 SYN-RCVD 狀態。
3. 客戶端收到服務端發來的「SYN,ACK」包後,能夠確認對方存在,此時回覆「ACK」包,並進入 ESTABLISHED 狀態。
4. 服務端收到最後一個「ACK」包後,也進入 ESTABLISHED 狀態。
這是正常的 TCP 三次握手,握手完成後雙端都進入 ESTABLISHED 狀態,在此以後,就是正常的數據傳輸過程。
三次握手的正常發包和應答,以及雙端的狀態扭轉咱們已經講了,接下來就來看看在這三次握手的過程當中,出現的異常狀況。
1. 客戶端第一個「SYN」包丟了。
若是客戶端第一個「SYN」包丟了,也就是服務端根本就不知道客戶端曾經發過包,那麼處理流程主要在客戶端。
而在 TCP 協議中,某端的一組「請求-應答」中,在必定時間範圍內,只要沒有收到應答的「ACK」包,不管是請求包對方沒有收到,仍是對方的應答包本身沒有收到,均認爲是丟包了,都會觸發超時重傳機制。
因此此時會進入重傳「SYN」包。根據《TCP/IP詳解卷Ⅰ:協議》中的描述,此時會嘗試三次,間隔時間分別是 5.8s、24s、48s,三次時間大約是 76s 左右,而大多數伯克利系統將創建一個新鏈接的最長時間,限制爲 75s。
也就是說三次握手第一個「SYN」包丟了,會重傳,總的嘗試時間是 75s。
2. 服務端收到「SYN」並回復的「SYN,ACK」包丟了。
此時服務端已經收到了數據包並回復,若是這個回覆的「SYN,ACK」包丟了,站在客戶端的角度,會認爲是最開始的那個「SYN」丟了,那麼就繼續重傳,就是咱們前面說的「錯誤 1」 的流程。
而對服務端而言,若是發送的「SYN,ACK」包丟了,在超時時間內沒有收到客戶端發來的「ACK」包,也會觸發重傳,此時服務端處於 SYN_RCVD 狀態,會依次等待 3s、6s、12s 後,從新發送「SYN,ACK」包。
而這個「SYN,ACK」包的重傳次數,不一樣的操做系統下有不一樣的配置,例如在 Linux 下能夠經過 tcp_synack_retries
進行配置,默認值爲 5。若是這個重試次數內,仍未收到「ACK」應答包,那麼服務端會自動關閉這個鏈接。
同時因爲客戶端在沒有收到「SYN,ACK」時,也會進行重傳,當客戶端重傳的「SYN」被收到後,服務端會當即從新發送「SYN,ACK」包。
3. 客戶端最後一次回覆「SYN,ACK」的「ACK」包丟了。
若是最後一個「ACK」包丟了,服務端由於收不到「ACK」會走重傳機制,而客戶端此時進入 ESTABLISHED 狀態。
多數狀況下,客戶端進入 ESTABLISHED 狀態後,則認爲鏈接已創建,會當即發送數據。可是服務端由於沒有收到最後一個「ACK」包,依然處於 SYN-RCVD 狀態。
那麼這裏的關鍵,就在於服務端在處於 SYN-RCVD 狀態下,收到客戶端的數據包後如何處理?
這也是比較有爭議的地方,有些資料裏會寫到當服務端處於 SYN-RCVD 狀態下,收到客戶端的數據包後,會直接回復 RTS 包響應,表示服務端錯誤,並進入 CLOSE 狀態。
可是這樣的設定有些過於嚴格,試想一下,服務端還在經過三次握手階段肯定對方是否真實存在,此時對方的數據已經發來了,那確定是存在的。
因此當服務端處於 SYN-RCVD 狀態下時,接收到客戶端真實發送來的數據包時,會認爲鏈接已創建,並進入 ESTABLISHED 狀態。
實踐出真知,具體測試流程能夠參考這篇文章:《TCP三次握手的第三個ack丟了會怎樣》
那麼實際狀況,爲何會這樣呢?
當客戶端在 ESTABLISHED 狀態下,開始發送數據包時,會攜帶上一個「ACK」的確認序號,因此哪怕客戶端響應的「ACK」包丟了,服務端在收到這個數據包時,可以經過包內 ACK 的確認序號,正常進入 ESTABLISHED 狀態。
參考:《What if a TCP handshake segment is lost?》
4. 客戶端故意不發最後一次「SYN」包。
前面一直在說正常的異常邏輯,雙方都還算友善,按規矩作事,出現異常主要也是由於網絡等客觀問題,接下來講一個惡意的狀況。
若是客戶端是惡意的,在發送「SYN」包後,並收到「SYN,ACK」後就不回覆了,那麼服務端此時處於一種半鏈接的狀態,雖然服務端會經過 tcp_synack_retries
配置重試的次數,不會無限等待下去,可是這也是有一個時間週期的。
若是短期內存在大量的這種惡意鏈接,對服務端來講壓力就會很大,這就是所謂的 SYN FLOOD 攻擊。
這就屬於安全攻防的範疇了,今天就不討論了,有興趣能夠自行了解。
說完 TCP 三次握手,繼續來分析 TCP 四次揮手的異常狀況。
保持行文風格,在此以前,咱們仍是先來簡單瞭解一下 TCP 的四次揮手。
當數據傳輸完成,須要斷開鏈接的時候,TCP 會採起四次揮手的方式,來安全的斷開鏈接。
爲何握手須要三次,而揮手須要四次呢?
本質上來講,雙端都須要通過一次「分手」的過程,來保證本身和對端的狀態正確。本着友好協商的態度,你先提出的分手,也要把最大的善意給對方,不能打了對方一個措手不及。你說不玩了就不玩了,那之後誰還敢和你玩。
下面這張圖,是比較經典的 TCP 四次揮手的消息和雙端狀態的變化。
咱們解釋一下這張圖:
1. 初始時雙端還都處於 ESTABLISHED 狀態並傳輸數據,某端能夠主動發起「FIN」包準備斷開鏈接,在這裏的場景下,是客戶端發起「FIN」請求。在發出「FIN」後,客戶端進入 FIN-WAIT-1 狀態。
2. 服務端收到「FIN」消息後,回覆「ACK」表示知道了,並從 ESTABLISHED 狀態進入 CLOSED-WAIT 狀態,開始作一些斷開鏈接前的準備工做。
3. 客戶端收到以前「FIN」的回覆「ACK」消息後,進入 FIN-WAIT-2 狀態。而當服務端作好斷開前的準備工做後,也會發送一個「FIN,ACK」的消息給客戶端,表示我也好了,請求斷開鏈接,並在發送消息後,服務端進入 LAST-ACK 狀態。
4. 客戶端在收到「FIN,ACK」消息後,會當即回覆「ACK」,表示知道了,並進入 TIME_WAIT 狀態,爲了穩定和安全考慮,客戶端會在 TIME-WAIT 狀態等待 2MSL 的時長,最終進入 CLOSED 狀態。
5. 服務端收到客戶端回覆的「ACK」消息後,直接從 LAST-ACK 狀態進入 CLOSED 狀態。
正常的通過四次揮手以後,雙端都進入 CLOSED 狀態,在此以後,雙端正式斷開了鏈接。
四次揮手的正常發包和應答過程,咱們已經簡單瞭解了,接下來就繼續看看,四次揮手過程當中,出現的異常狀況。
1. 斷開鏈接的 FIN 包丟了。
咱們前面一直強調過,若是一個包發出去,在必定時間內,只要沒有收到對端的「ACK」回覆,均認爲這個包丟了,會觸發超時重傳機制。而不會關心究竟是本身發的包丟了,仍是對方的「ACK」丟了。
因此在這裏,若是客戶端率先發的「FIN」包丟了,或者沒有收到對端的「ACK」回覆,則會觸發超時重傳,直到觸發重傳的次數,直接關閉鏈接。
對於服務端而言,若是客戶端發來的「FIN」沒有收到,就沒有任何感知。會在一段時間後,也關閉鏈接。
2. 服務端第一次回覆的 ACK 丟了。
此時由於客戶端沒有收到「ACK」應答,會嘗試重傳以前的「FIN」請求,服務端收到後,又會當即再重傳「ACK」。
而此時服務端已經進入 CLOSED-WAIT 狀態,開始作斷開鏈接前的準備工做。當準備好以後,會回覆「FIN,ACK」,注意這個消息是攜帶了以前「ACK」的響應序號的。
只要這個消息沒丟,客戶端能夠憑藉「FIN,ACK」包中的響應序號,直接從 FIN-WAIT-1 狀態,進入 TIME-WAIT 狀態,開始長達 2MSL 的等待。
3. 服務端發送的 FIN,ACK 丟了。
服務端在超時後會重傳,此時客戶端有兩種狀況,要麼處於 FIN-WAIT-2 狀態(以前的 ACK 也丟了),會一直等待;要麼處於 TIME-WAIT 狀態,會等待 2MSL 時間。
也就是說,在一小段時間內客戶端還在,客戶端在收到服務端發來的「FIN,ACK」包後,也會回覆一個「ACK」應答,並作好本身的狀態切換。
4. 客戶端最後回覆的 ACK 丟了。
客戶端在回覆「ACK」後,會進入 TIME-WAIT 狀態,開始長達 2MSL 的等待,服務端由於沒有收到「ACK」的回覆,會重試一段時間,直到服務端重試超時後主動斷開。
或者等待新的客戶端接入後,收到服務端重試的「FIN」消息後,回覆「RST」消息,在收到「RST」消息後,復位服務端的狀態。
5. 客戶端收到 ACK 後,服務端跑路了。
客戶端在收到「ACK」後,進入了 FIN-WAIT-2 狀態,等待服務端發來的「FIN」包,而若是服務端跑路了,這個包永遠都等不到。
在 TCP 協議中,是沒有對這個狀態的處理機制的。可是協議無論,系統來湊,操做系統會接管這個狀態,例如在 Linux 下,就能夠經過 tcp_fin_timeout
參數,來對這個狀態設定一個超時時間。
須要注意的是,當超過 tcp_fin_timeout 的限制後,狀態並非切換到 TIME_WAIT,而是直接進入 CLOSED 狀態。
參考:《關於FIN_WAIT2》
6. 客戶端收到 ACK 後,客戶端本身跑路了。
客戶端收到「ACK」後直接跑路,服務端後續在發送的「FIN,ACK」就沒有接收端,也就不會獲得回覆,會不斷的走 TCP 的超時重試的機制,此時服務端處於 LAST-ACK 狀態。
那就要分 2 種狀況分析:
「RST」消息是一種重置消息,表示當前錯誤了,應該回到初始的狀態。若是客戶端跑路後有新的客戶端接入,會在此發送「SYN」以指望創建鏈接,此時這個「SYN」將被忽略,並直接回復「FIN,ACK」消息,新客戶端在收到「FIN」消息後是不會認的,而且會回覆一個「RST」消息。
參考:《Coping with the TCP TIME-WAIT state on busy Linux servers》
本文聊了 TCP 在三次握手和四次揮手的時候,出現異常的處理邏輯。
大多數狀況下,都是依賴超時重傳來保證 TCP 的可靠性,可是重傳的次數,狀態的轉換,以及有哪些狀態是被系統接管,這些細節,就是本文的主題。
有任何問題歡迎留言討論,有所幫助也別忘了轉發和點收藏支持一下,謝謝!
公衆號後臺回覆成長『 成長』,將會獲得我準備的學習資料。