TCP協議分析

tcp協議解析:

TCP在網絡OSI的七層模型中的第四層——Transport層,IP在第三層——Network層,ARP在第二層——Data Link層。在第二層上的數據,我們叫Frame,在第三層上的數據叫Packet,第四層的數據叫Segment。

  我們程序的數據首先會打到TCP的Segment中,然後TCP的Segment會打到IP的Packet中,然後再打到以太網Ethernet的Frame中,傳到對端後,各個層解析自己的協議,然後把數據交給更高層的協議處理

TCP頭格式

TCP數據段格式

wKiom1hQ5wTTzn_TAABH6BhkPiA339.jpg-wh_50

●源、目標端口號字段:佔16比特。TCP協議通過使用"端口"來標識源端和目標端的應用進程。端口號可以使用0到65535之間的任何數字。在收到服務請求時,操作系統動態地爲客戶端的應用程序分配端口號。在服務器端,每種服務在"衆所周知的端口"(Well-Know Port)爲用戶提供服務。
  ●順序號字段:佔32比特。用來標識從TCP源端向TCP目標端發送的數據字節流,它表示在這個報文段中的第一個數據字節。
  ●確認號字段:佔32比特。只有ACK標誌爲1時,確認號字段纔有效。它包含目標端所期望收到源端的下一個數據字節。
     ●頭部長度字段:佔4比特。給出頭部佔32比特的數目。沒有任何選項字段的TCP頭部長度爲20字節;最多可以有60字節的TCP頭部。
  ●標誌位字段(U、A、P、R、S、F):佔6比特。各比特的含義如下:
  ◆URG:緊急指針(urgent pointer)有效。
  ◆ACK:確認序號有效。
  ◆PSH:接收方應該儘快將這個報文段交給應用層。
  ◆RST:重建連接。
  ◆SYN:發起一個連接。
  ◆FIN:釋放一個連接。
  ●窗口大小字段:佔16比特。此字段用來進行流量控制。單位爲字節數,這個值是本機期望一次接收的字節數。
  ●TCP校驗和字段:佔16比特。對整個TCP報文段,即TCP頭部和TCP數據進行校驗和計算,並由目標端進行驗證。
  ●緊急指針字段:佔16比特。它是一個偏移量,和序號字段中的值相加表示緊急數據最後一個字節的序號。
  ●選項字段:佔32比特。可能包括"窗口擴大因子"、"時間戳"等選項。


TCP三次握手

1、TCP建立連接的三次握手過程  

  TCP會話通過三次握手來初始化。三次握手的目標是使數據段的發送和接收同步。同時也向其他主機表明其一次可接收的數據量(窗口大小),並建立邏輯連接。這三次握手的過程可以簡述如下:

  ●源主機發送一個同步標誌位(SYN)置1的TCP數據段。此段中同時標明初始序號(Initial Sequence Number,ISN)。ISN是一個隨時間變化的隨機值。

  ●目標主機發回確認數據段,此段中的同步標誌位(SYN)同樣被置1,且確認標誌位(ACK)也置1,同時在確認序號字段表明目標主機期待收到源主機下一個數據段的序號(即表明前一個數據段已收到並且沒有錯誤)。此外,此段中還包含目標主機的段初始序號。 

  ●源主機再回送一個數據段,同樣帶有遞增的發送序號和確認序號。 
三次握手
        所謂三次握手(Three-Way Handshake)即建立TCP連接,就是指建立一個TCP連接時,需要客戶端和服務端總共發送3個包以確認連接的建立。在socket編程中,這一過程由客戶端執行connect來觸發,整個流程如下圖所示:


圖2 TCP三次握手

        (1)第一次握手:Client將標誌位SYN置爲1,隨機產生一個值seq=J,並將該數據包發送給Server,Client進入SYN_SENT狀態,等待Server確認。
        (2)第二次握手:Server收到數據包後由標誌位SYN=1知道Client請求建立連接,Server將標誌位SYN和ACK都置爲 1,ack=J+1,隨機產生一個值seq=K,並將該數據包發送給Client以確認連接請求,Server進入SYN_RCVD狀態。
        (3)第三次握手:Client收到確認後,檢查ack是否爲J+1,ACK是否爲1,如果正確則將標誌位ACK置爲1,ack=K+1,並將該數據包發 送給Server,Server檢查ack是否爲K+1,ACK是否爲1,如果正確則連接建立成功,Client和Server進入 ESTABLISHED狀態,完成三次握手,隨後Client與Server之間可以開始傳輸數據了。
        
        SYN***:
                在三次握手過程中,Server發送SYN-ACK之後,收到Client的ACK之前的TCP連接稱爲半連接(half-open connect),此時Server處於SYN_RCVD狀態,當收到ACK後,Server轉入ESTABLISHED狀態。SYN***就是 Client在短時間內僞造大量不存在的IP地址,並向Server不斷地發送SYN包,Server回覆確認包,並等待Client的確認,由於源地址 是不存在的,因此,Server需要不斷重發直至超時,這些僞造的SYN包將產時間佔用未連接隊列,導致正常的SYN請求因爲隊列滿而被丟棄,從而引起網 絡堵塞甚至系統癱瘓。SYN***時一種典型的DDOS***,檢測SYN***的方式非常簡單,即當Server上有大量半連接狀態且源IP地址是隨機的,則 可以斷定遭到SYN***了,使用如下命令可以讓之現行:
                #netstat -nap | grep SYN_RECV

三、四次揮手
         三次握手耳熟能詳,四次揮手估計就,所謂四次揮手(Four-Way Wavehand)即終止TCP連接,就是指斷開一個TCP連接時,需要客戶端和服務端總共發送4個包以確認連接的斷開。在socket編程中,這一過程由客戶端或服務端任一方執行close來觸發,整個流程如下圖所示:


圖3 TCP四次揮手

        由於TCP連接時全雙工的,因此,每個方向都必須要單獨進行關閉,這一原則是當一方完成數據發送任務後,發送一個FIN來終止這一方向的連接,收到一個 FIN只是意味着這一方向上沒有數據流動了,即不會再收到數據了,但是在這個TCP連接上仍然能夠發送數據,直到這一方向也發送了FIN。首先進行關閉的 一方將執行主動關閉,而另一方則執行被動關閉,上圖描述的即是如此。
        (1)第一次揮手:Client發送一個FIN,用來關閉Client到Server的數據傳送,Client進入FIN_WAIT_1狀態。
        (2)第二次揮手:Server收到FIN後,發送一個ACK給Client,確認序號爲收到序號+1(與SYN相同,一個FIN佔用一個序號),Server進入CLOSE_WAIT狀態。
        (3)第三次揮手:Server發送一個FIN,用來關閉Server到Client的數據傳送,Server進入LAST_ACK狀態。
        (4)第四次揮手:Client收到FIN後,Client進入TIME_WAIT狀態,接着發送一個ACK給Server,確認序號爲收到序號+1,Server進入CLOSED狀態,完成四次揮手。
        上面是一方主動關閉,另一方被動關閉的情況,實際中還會出現同時發起主動關閉的情況,具體流程如下圖:

圖4 同時揮手
        流程和狀態在上圖中已經很明瞭了,在此不再贅述,可以參考前面的四次揮手解析步驟。

 另外,有幾個事情需要注意一下:

  • 關於建立連接時SYN超時。試想一下,如果server端接到了clien發的SYN後回了SYN-ACK後 client掉線了,server端沒有收到client回來的ACK,那麼,這個連接處於一箇中間狀態,即沒成功,也沒失敗。於是,server端如果在一定時間內沒有收到的TCP會重發SYN-ACK。在Linux下,默認重試次數爲5次,重試的間隔時間從1s開始每次都翻售,5次的重試時間間隔爲 1s, 2s, 4s, 8s, 16s,總共31s,第5次發出後還要等32s都知道第5次也超時了,所以,總共需要 1s + 2s + 4s+ 8s+ 16s     + 32s = 2^6 -1 = 63s,TCP纔會把斷開這個連接。

  • 關於SYN Flood***。一些惡意的人就爲此製造了SYN Flood***——給服務器發了一個SYN後,就下線了,於是服務器需要默認等63s纔會斷開連接,這樣,***者就可以把服務器的syn連接的隊列耗盡,讓正常的連接請求不能處理。於是,Linux下給了一個叫tcp_syncookies的參數來應對這個事——當SYN隊列滿了後,TCP會通過源地址端口、目標地址端口和時間戳打造出一個特別的Sequence Number發回去(又叫cookie),如果是***者則不會有響應,如果是正常連接,則會把這個 SYN Cookie發回來,然後服務端可以通過cookie建連接(即使你不在SYN隊列中)。請注意,請先千萬別用tcp_syncookies來處理正常的大負載的連接的情況。因爲,synccookies是妥協版的TCP協議,並不嚴謹。對於正常的請求,你應該調整三個TCP參數可供你選擇,第一個是:tcp_synack_retries 可以用他來減少重試次數;第二個是:tcp_max_syn_backlog,可以增大SYN連接數;第三個是:tcp_abort_on_overflow 處理不過來乾脆就直接拒絕連接了。

  • 關於ISN的初始化。ISN是不能hard code的,不然會出問題的——比如:如果連接建好後始終用1來做ISN,如果client發了30個segment過去,但是網絡斷了,於是client重連,又用了1做ISN,但是之前連接的那些包到了,於是就被當成了新連接的包,此時,client的Sequence Number 可能是3,而Server端認爲client端的這個號是30了。全亂了。RFC793中說,ISN會和一個假的時鐘綁在一起,這個時鐘會在每4微秒對ISN做加一操作,直到超過2^32,又從0開始。這樣,一個ISN的週期大約是4.55個小時。因爲,我們假設我們的TCP Segment在網絡上的存活時間不會超過Maximum Segment     Lifetime(縮寫爲MSL),所以,只要MSL的值小於4.55小時,那麼,我們就不會重用到ISN。

  • 關於 MSL 和 TIME_WAIT。通過上面的ISN的描述,相信你也知道MSL是怎麼來的了。我們注意到,在TCP的狀態圖中,從TIME_WAIT狀態到CLOSED狀態,有一個超時設置,這個超時設置是 2*MSL(RFC793定義了MSL爲2分鐘,Linux設置成了30s)爲什麼要這有TIME_WAIT?爲什麼不直接給轉成CLOSED狀態呢?主要有兩個原因:1)TIME_WAIT確保有足夠的時間讓對端收到了ACK,如果被動關閉的那方沒有收到Ack,就會觸發被動端重發Fin,一來一去正好2個 MSL,2)有足夠的時間讓這個連接不會跟後面的連接混在一起(要知道,有些自做主張的路由器會緩存IP數據包,如果連接被重用了,那麼這些延遲收到的包就有可能會跟新連接混在一起)。你可以看看這篇文章《TIME_WAIT and its design implications for protocols     and scalable client server systems

  • 關於TIME_WAIT數量太多。從上面的描述我們可以知道,TIME_WAIT是個很重要的狀態,但是如果在大併發的短鏈接下,TIME_WAIT 就會太多,這也會消耗很多系統資源。只要搜一下,你就會發現,十有八九的處理方式都是教你設置兩個參數,一個叫tcp_tw_reuse,另一個叫tcp_tw_recycle的參數,這兩個參數默認值都是被關閉的,後者recyle比前者resue更爲激進,resue要溫柔一些。另外,如果使用tcp_tw_reuse,必需設置tcp_timestamps=1,否則無效。這裏,你一定要注意,打開這兩個參數會有比較大的坑——可能會讓TCP連接出一些詭異的問題(因爲如上述一樣,如果不等待超時重用連接的話,新的連接可能會建不上。正如官方文檔上說的一樣“It should not be changed     without advice/request of technical experts”)。

  • 關於tcp_tw_reuse。官方文檔上說tcp_tw_reuse 加上tcp_timestamps(又叫PAWS, for Protection     Against Wrapped Sequence Numbers)可以保證協議的角度上的安全,但是你需要tcp_timestamps在兩邊都被打開(你可以讀一下tcp_twsk_unique的源碼 )。我個人估計還是有一些場景會有問題。

  • 關於tcp_tw_recycle。如果是tcp_tw_recycle被打開了話,會假設對端開啓了tcp_timestamps,然後會去比較時間戳,如果時間戳變大了,就可以重用。但是,如果對端是一個NAT網絡的話(如:一個公司只用一個IP出公網)或是對端的IP被另一臺重用了,這個事就複雜了。建鏈接的SYN可能就被直接丟掉了(你可能會看到connection time out的錯誤)(如果你想觀摩一下Linux的內核代碼,請參看源碼 tcp_timewait_state_process)。

  • 關於tcp_max_tw_buckets。這個是控制併發的TIME_WAIT的數量,默認值是180000,如果超限,那麼,系統會把多的給destory掉,然後在日誌裏打一個警告(如:time wait bucket table overflow),官網文檔說這個參數是用來對抗DDoS***的。也說的默認值180000並不小。這個還是需要根據實際情況考慮。

   Again,使用tcp_tw_reuse和tcp_tw_recycle來解決TIME_WAIT的問題是非常非常危險的,因爲這兩個參數違反了TCP協議(RFC 1122) 

   其實,TIME_WAIT表示的是你主動斷連接,所以,這就是所謂的“不作死不會死”。試想,如果讓對端斷連接,那麼這個破問題就是對方的了,呵呵。另外,如果你的服務器是於HTTP服務器,那麼設置一個HTTP的KeepAlive有多重要(瀏覽器會重用一個TCP連接來處理多個HTTP請求),然後讓客戶端去斷鏈接(你要小心,瀏覽器可能會非常貪婪,他們不到萬不得已不會主動斷連接)。

數據傳輸中的Sequence Number

   下圖是我從Wireshark中截了個我在訪問coolshell.cn時的有數據傳輸的圖給你看一下,SeqNum是怎麼變的。(使用Wireshark菜單中的Statistics ->Flow Graph… )


   wKiom1hQ7Wug5VK3AACmgwtO5h0414.png

   你可以看到,SeqNum的增加是和傳輸的字節數相關的。上圖中,三次握手後,來了兩個Len:1440的包,而第二個包的SeqNum就成了1441。然後第一個ACK回的是1441,表示第一個1440收到了。

   注意:如果你用Wireshark抓包程序看3次握手,你會發現SeqNum總是爲0,不是這樣的,Wireshark爲了顯示更友好,使用了Relative SeqNum——相對序號,你只要在右鍵菜單中的protocol preference 中取消掉就可以看到“AbsoluteSeqNum”了

TCP重傳機制

   TCP要保證所有的數據包都可以到達,所以,必需要有重傳機制。

   注意,接收端給發送端的Ack確認只會確認最後一個連續的包,比如,發送端發了1,2,3,4,5一共五份數據,接收端收到了1,2,於是回ack 3,然後收到了4(注意此時3沒收到),此時的TCP會怎麼辦?我們要知道,因爲正如前面所說的,SeqNum和Ack是以字節數爲單位,所以ack的時候,不能跳着確認,只能確認最大的連續收到的包,不然,發送端就以爲之前的都收到了。

超時重傳機制

   一種是不回ack,一直等待3,當發送方發現收不到3的ack超時後,會重傳3。一旦接收方收到3後,ack回傳4——意味着3和4都收到了。

   但是,這種方式會有比較嚴重的問題,那就是因爲要死等3,所以會導致4和5即便已經收到了,而發送方也完全不知道發生了什麼事,因爲沒有收到Ack,所以,發送方可能會悲觀地認爲也丟了,所以有可能也會導致4和5的重傳。

   對此有兩種選擇:

  • 一種是僅重傳timeout的包。也就是第3份數據。

  • 另一種是重傳timeout後所有的數據,也就是第3,4,5這三份數據。

   這兩種方式有好也有不好。第一種會節省帶寬,但是慢,第二種會快一點,但是會浪費帶寬,也可能會有無用功。但總體來說都不好。因爲都在等timeout,timeout可能會很長(在下篇會說TCP是怎麼動態地計算出timeout的)

快速重傳機制

   於是,TCP引入了一種叫Fast Retransmit 的算法,不以時間驅動,而以數據驅動重傳。也就是說,如果,包沒有連續到達,就ack最後那個可能被丟了的包,如果發送方連續收到3次相同的ack,就重傳。Fast Retransmit的好處是不用等timeout了再重傳。

   比如:如果發送方發出了1,2,3,4,5份數據,第一份先到送了,於是就ack回2,結果2因爲某些原因沒收到,3到達了,於是還是ack回2,後面的4和5都到了,但是還是ack回2,因爲2還是沒有收到,於是發送端收到了三個ack=2的確認,知道了2還沒有到,於是就馬上重轉2。然後,接收端收到了2,此時因爲3,4,5都收到了,於是ack回6。示意圖如下:

   wKiom1hQ7XyAE61uAADqNpgeOGU655.jpg

   Fast Retransmit只解決了一個問題,就是timeout的問題,它依然面臨一個艱難的選擇,就是重轉之前的一個還是重裝所有的問題。對於上面的示例來說,是重傳#2呢還是重傳#2,#3,#4,#5呢?因爲發送端並不清楚這連續的3個ack(2)是誰傳回來的?也許發送端發了20份數據,是#6,#10,#20傳來的呢。這樣,發送端很有可能要重傳從2到20的這堆數據(這就是某些TCP的實際的實現)。可見,這是一把雙刃劍。

SACK方法

   另外一種更好的方式叫:Selective Acknowledgment(SACK)(參看RFC 2018),這種方式需要在TCP頭裏加一個SACK的東西,ACK還是Fast Retransmit的ACK,SACK則是彙報收到的數據碎版。參看下圖:

   wKioL1hQ7ZKS-nc7AABPrlaAI7E343.png

   這樣,在發送端就可以根據回傳的SACK來知道哪些數據到了,哪些沒有到。於是就優化了Fast Retransmit的算法。當然,這個協議需要兩邊都支持。在 Linux下,可以通過tcp_sack參數打開這個功能(Linux2.4後默認打開)。

   這裏還需要注意一個問題——接收方Reneging,所謂Reneging的意思就是接收方有權把已經報給發送端SACK裏的數據給丟了。這樣幹是不被鼓勵的,因爲這個事會把問題複雜化了,但是,接收方這麼做可能會有些極端情況,比如要把內存給別的更重要的東西。所以,發送方也不能完全依賴SACK,還是要依賴ACK,並維護Time-Out,如果後續的ACK沒有增長,那麼還是要把SACK的東西重傳,另外,接收端這邊永遠不能把SACK的包標記爲Ack。

   注意:SACK會消費發送方的資源,試想,如果一個***者給數據發送方發一堆SACK的選項,這會導致發送方開始要重傳甚至遍歷已經發出的數據,這會消耗很多發送端的資源。詳細的東西請參看《TCP SACK的性能權衡

DuplicateSACK – 重複收到數據的問題

   Duplicate SACK又稱D-SACK,其主要使用了SACK來告訴發送方有哪些數據被重複接收了。RFC-2833 裏有詳細描述和示例。下面舉幾個例子(來源於RFC-2833

   D-SACK使用了SACK的第一個段來做標誌,

  • 如果SACK的第一個段的範圍被ACK所覆蓋,那麼就是D-SACK

  • 如果SACK的第一個段的範圍被SACK的第二個段覆蓋,那麼就是D-SACK


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章