從 TCP 三次握手說起:淺析TCP協議中的疑難雜症 ( 1 )
從 TCP 三次握手說起:淺析TCP協議中的疑難雜症 ( 1 )
說到TCP協議,相信大家都比較熟悉了,對於TCP協議總能說個一二三來,但是TCP協議又是一個非常複雜的協議,其中有不少細節點讓人頭疼點。本文就是來說說這些頭疼點的,淺談一些TCP的疑難雜症。那麼從哪說起呢?當然是從三次握手和四次揮手說起啦,可能大家都知道TCP是三次互動完成連線的建立,四次互動來斷開一個連線,那為什麼是三次握手和四次揮手呢?反過來不行嗎?
疑症 1 :TCP 的三次握手、四次揮手
下面兩圖大家再熟悉不過了,TCP的三次握手和四次揮手見下面左邊的”TCP建立連線”、”TCP資料傳送”、”TCP斷開連線”時序圖和右邊的”TCP協議狀態機”
TCP三次握手、四次揮手時序圖
TCP協議狀態機
要弄清TCP建立連線需要幾次互動才行,我們需要弄清建立連線進行初始化的目標是什麼。TCP進行握手初始化一個連線的目標是:分配資源、初始化序列號(通知peer對端我的初始序列號是多少),知道初始化連線的目標,那麼要達成這個目標的過程就簡單了,握手過程可以簡化為下面的四次互動:
1 ) clien 端首先發送一個 SYN 包告訴 Server 端我的初始序列號是 X。
2 ) Server 端收到 SYN 包後回覆給 client 一個 ACK 確認包,告訴 client 說我收到了。
3 ) 接著 Server 端也需要告訴 client 端自己的初始序列號,於是 Server 也傳送一個 SYN 包告訴 client 我的初始序列號是Y。
4 ) Client 收到後,回覆 Server 一個 ACK 確認包說我知道了。
整個過程4次互動即可完成初始化,但是,細心的同學會發現兩個問題:
1. Server傳送SYN包是作為發起連線的SYN包,還是作為響應發起者的SYN包呢?怎麼區分?比較容易引起混淆
2.Server的ACK確認包和接下來的SYN包可以合成一個SYN ACK包一起傳送的,沒必要分別單獨傳送,這樣省了一次互動同時也解決了問題1. 這樣TCP建立一個連線,三次握手在進行最少次互動的情況下完成了Peer兩端的資源分配和初始化序列號的交換。
大部分情況下建立連線需要三次握手,也不一定都是三次,有可能出現四次握手來建立連線的。如下圖,當Peer兩端同時發起SYN來建立連線的時候,就出現了四次握手來建立連線(對於有些TCP/IP的實現,可能不支援這種同時開啟的情況)。
在三次握手過程中,細心的同學可能會有以下疑問:
(2). 初始化序列號X、Y是可以是寫死固定的嗎,為什麼不能呢?
(3). 假如Client傳送一個SYN包給Server後就掛了或是不管了,這個時候這個連線處於什麼狀態呢?會超時嗎?為什麼呢?
TCP進行斷開連線的目標是:回收資源、終止資料傳輸。由於TCP是全雙工的,需要Peer兩端分別各自拆除自己通向Peer對端的方向的通訊通道。這樣需要四次揮手來分別拆除通訊通道,就比較清晰明瞭了。
1)Client 傳送一個FIN包來告訴 Server 我已經沒資料需要發給 Server了。
2)Server 收到後回覆一個 ACK 確認包說我知道了。
3)然後 server 在自己也沒資料傳送給client後,Server 也傳送一個 FIN 包給 Client 告訴 Client 我也已經沒資料發給client 了。
4)Client 收到後,就會回覆一個 ACK 確認包說我知道了。
到此,四次揮手,這個TCP連線就可以完全拆除了。在四次揮手的過程中,細心的同學可能會有以下疑問:
(4). Client和Server同時發起斷開連線的FIN包會怎麼樣呢,TCP狀態是怎麼轉移的?
(5). 左側圖中的四次揮手過程中,Server端的ACK確認包能不能和接下來的FIN包合併成一個包呢,這樣四次揮手就變成三次揮手了。
(6). 四次揮手過程中,首先斷開連線的一端,在回覆最後一個ACK後,為什麼要進行TIME_WAIT呢(超時設定是 2*MSL,RFC793定義了MSL為2分鐘,Linux設定成了30s),在TIME_WAIT的時候又不能釋放資源,白白讓資源佔用那麼長時間,能不能省了TIME_WAIT呢,為什麼?
疑症 2 : TCP 連線的初始化序列號能否固定
如果初始化序列號(縮寫為ISN:Inital Sequence Number)可以固定,我們來看看會出現什麼問題。假設ISN固定是1,Client和Server建立好一條TCP連線後,Client連續給Server發了10個包,這10個包不知怎麼被鏈路上的路由器快取了(路由器會毫無先兆地快取或者丟棄任何的資料包),這個時候碰巧Client掛掉了,然後Client用同樣的埠號重新連上Server,Client又連續給Server發了幾個包,假設這個時候Client的序列號變成了5。接著,之前被路由器快取的10個數據包全部被路由到Server端了,Server給Client回覆確認號10,這個時候,Client整個都不好了,這是什麼情況?我的序列號才到5,你怎麼給我的確認號是10了,整個都亂了。
RFC793中,建議ISN和一個假的時鐘綁在一起,這個時鐘會在每4微秒對ISN做加一操作,直到超過2^32,又從0開始,這需要4小時才會產生ISN的迴繞問題,這幾乎可以保證每個新連線的ISN不會和舊的連線的ISN產生衝突。這種遞增方式的ISN,很容易讓攻擊者猜測到TCP連線的ISN,現在的實現大多是在一個基準值的基礎上進行隨機的。
疑症 3 : 初始化連線的 SYN 超時問題
Client傳送SYN包給Server後掛了,Server回給Client的SYN-ACK一直沒收到Client的ACK確認,這個時候這個連線既沒建立起來,也不能算失敗。這就需要一個超時時間讓Server將這個連線斷開,否則這個連線就會一直佔用Server的SYN連線佇列中的一個位置,大量這樣的連線就會將Server的SYN連線佇列耗盡,讓正常的連線無法得到處理。
目前,Linux下預設會進行5次重發SYN-ACK包,重試的間隔時間從1s開始,下次的重試間隔時間是前一次的雙倍,5次的重試時間間隔為1s, 2s, 4s, 8s, 16s,總共31s,第5次發出後還要等32s都知道第5次也超時了,所以,總共需要 1s + 2s + 4s+ 8s+ 16s + 32s = 63s,TCP才會把斷開這個連線。由於,SYN超時需要63秒,那麼就給攻擊者一個攻擊伺服器的機會,攻擊者在短時間內傳送大量的SYN包給Server(俗稱 SYN flood 攻擊),用於耗盡Server的SYN佇列。對於應對SYN 過多的問題,linux提供了幾個TCP引數:tcp_syncookies、tcp_synack_retries、tcp_max_syn_backlog、tcp_abort_on_overflow 來調整應對。
疑症 4 : TCP 的 Peer 兩端同時斷開連線
由上面的”TCP協議狀態機 “圖可以看出,TCP的Peer端在收到對端的FIN包前發出了FIN包,那麼該Peer的狀態就變成了FIN_WAIT1,Peer在FIN_WAIT1狀態下收到對端Peer對自己FIN包的ACK包的話,那麼Peer狀態就變成FIN_WAIT2,Peer在FIN_WAIT2下收到對端Peer的FIN包,在確認已經收到了對端Peer全部的Data資料包後,就響應一個ACK給對端Peer,然後自己進入TIME_WAIT狀態。
但是如果Peer在FIN_WAIT1狀態下首先收到對端Peer的FIN包的話,那麼該Peer在確認已經收到了對端Peer全部的Data資料包後,就響應一個ACK給對端Peer,然後自己進入CLOSEING狀態,Peer在CLOSEING狀態下收到自己的FIN包的ACK包的話,那麼就進入TIME WAIT 狀態。於是,TCP的Peer兩端同時發起FIN包進行斷開連線,那麼兩端Peer可能出現完全一樣的狀態轉移 FIN_WAIT1——>CLOSEING——->TIME_WAIT,也就會Client和Server最後同時進入TIME_WAIT狀態。
同時關閉連線的狀態轉移如下圖所示:
疑症 5 : 四次揮手能不能變成三次揮手呢?
答案是可能的。
TCP是全雙工通訊,Cliet在自己已經不會在有新的資料要傳送給Server後,可以傳送FIN訊號告知Server,這邊已經終止Client到對端Server那邊的資料傳輸。但是,這個時候對端Server可以繼續往Client這邊傳送資料包。於是,兩端資料傳輸的終止在時序上是獨立並且可能會相隔比較長的時間,這個時候就必須最少需要2+2 = 4 次揮手來完全終止這個連線。但是,如果Server在收到Client的FIN包後,在也沒資料需要傳送給Client了,那麼對Client的ACK包和Server自己的FIN包就可以合併成為一個包傳送過去,這樣四次揮手就可以變成三次了(似乎linux協議棧就是這樣實現的)
6. 疑症 6 : TCP 的頭號疼症 TIME_WAIT 狀態
要說明TIME_WAIT的問題,需要解答以下幾個問題:
- Peer兩端,哪一端會進入TIME_WAIT呢?為什麼?相信大家都知道,TCP主動關閉連線的那一方會最後進入TIME_WAIT。那麼怎麼界定主動關閉方呢?是否主動關閉是由FIN包的先後決定的,就是在自己沒收到對端Peer的FIN包之前自己發出了FIN包,那麼自己就是主動關閉連線的那一方。對於疑症(4) 中描述的情況,那麼Peer兩邊都是主動關閉的一方,兩邊都會進入TIME_WAIT。為什麼是主動關閉的一方進行TIME_WAIT呢,被動關閉的進入TIME_WAIT可以不呢?我們來看看TCP四次揮手可以簡單分為下面三個過程過程一.主動關閉方傳送FIN; 過程二.被動關閉方收到主動關閉方的FIN後傳送該FIN的ACK,被動關閉方傳送FIN; 過程三.主動關閉方收到被動關閉方的FIN後傳送該FIN的ACK,被動關閉方等待自己FIN的ACK問題就在過程三中,據TCP協議規範,不對ACK進行ACK,如果主動關閉方不進入TIME_WAIT,那麼主動關閉方在傳送完ACK就走了的話,如果最後傳送的ACK在路由過程中丟掉了,最後沒能到被動關閉方,這個時候被動關閉方沒收到自己FIN的ACK就不能關閉連線,接著被動關閉方會超時重發FIN包,但是這個時候已經沒有對端會給該FIN回ACK,被動關閉方就無法正常關閉連線了,所以主動關閉方需要進入TIME_WAIT以便能夠重發丟掉的被動關閉方FIN的ACK。
- TIME_WAIT狀態是用來解決或避免什麼問題呢?
TIME_WAIT主要是用來解決以下幾個問題:
1)上面解釋為什麼主動關閉方需要進入TIME_WAIT狀態中提到的: 主動關閉方需要進入TIME_WAIT以便能夠重發丟掉的被動關閉方FIN包的ACK。如果主動關閉方不進入TIME_WAIT,那麼在主動關閉方對被動關閉方FIN包的ACK丟失了的時候,被動關閉方由於沒收到自己FIN的ACK,會進行重傳FIN包,這個FIN包到主動關閉方後,由於這個連線已經不存在於主動關閉方了,這個時候主動關閉方無法識別這個FIN包,協議棧會認為對方瘋了,都還沒建立連線你給我來個FIN包?,於是回覆一個RST包給被動關閉方,被動關閉方就會收到一個錯誤(我們見的比較多的:connect reset by peer,這裡順便說下 Broken pipe,在收到RST包的時候,還往這個連線寫資料,就會收到 Broken pipe錯誤了),原本應該正常關閉的連線,給我來個錯誤,很難讓人接受。2)防止已經斷開的連線1中在鏈路中殘留的FIN包終止掉新的連線2(重用了連線1的所有的5元素(源IP,目的IP,TCP,源埠,目的埠)),這個概率比較低,因為涉及到一個匹配問題,遲到的FIN分段的序列號必須落在連線2的一方的期望序列號範圍之內,雖然概率低,但是確實可能發生,因為初始序列號都是隨機產生的,並且這個序列號是32位的,會迴繞。3)防止鏈路上已經關閉的連線的殘餘資料包(a lost duplicate packet or a wandering duplicate packet) 干擾正常的資料包,造成資料流的不正常。這個問題和2)類似。
- TIME_WAIT會帶來哪些問題呢?
TIME_WAIT帶來的問題注意是源於:一個連線進入TIME_WAIT狀態後需要等待2*MSL(一般是1到4分鐘)那麼長的時間才能斷開連線釋放連線佔用的資源,會造成以下問題
1) 作為伺服器,短時間內關閉了大量的Client連線,就會造成伺服器上出現大量的TIME_WAIT連線,佔據大量的tuple,嚴重消耗著伺服器的資源。
2) 作為客戶端,短時間內大量的短連線,會大量消耗的Client機器的埠,畢竟埠只有65535個,埠被耗盡了,後續就無法在發起新的連線了。
( 由於上面兩個問題,作為客戶端需要連本機的一個服務的時候,首選UNIX域套接字而不是TCP )
TIME_WAIT很令人頭疼,很多問題是由TIME_WAIT造成的,但是TIME_WAIT又不是多餘的不能簡單將TIME_WAIT去掉,那麼怎麼來解決或緩解TIME_WAIT問題呢?可以進行TIME_WAIT的快速回收和重用來緩解TIME_WAIT的問題。有沒一些清掉TIME_WAIT的技巧呢?
文章來自公眾號:小時光茶社(Tech Teahouse)