TCP協議的定義和丟包時的重傳機制

TCP是一個巨複雜的協議,由於他要解決不少問題,而這些問題又帶出了不少子問題和陰暗面。因此學習TCP自己是個比較痛苦的過程,但對於學習的過程卻能讓人有不少收穫。關於TCP這個協議的細節,我仍是推薦你去看W.Richard Stevens的《TCP/IP 詳解 卷1:協議》(固然,你也能夠去讀一下RFC793以及後面N多的RFC)。另外,本文我會使用英文術語,這樣方便你經過這些英文關鍵詞來查找相關的技術文檔。html

複雜的TCPlinux

之因此想寫這篇文章,目的有三個,程序員

  • 一個是想鍛鍊一下本身是否能夠用簡單的篇幅把這麼複雜的TCP協議描清楚的能力。
  • 另外一個是以爲如今的好多程序員基本上不會認認真真地讀本書,喜歡快餐文化,因此,但願這篇快餐文章可讓你對TCP這個古典技術有所瞭解,並能體會到軟件設計中的種種難處。而且你能夠從中有一些軟件設計上的收穫。
  • 最重要的但願這些基礎知識可讓你搞清不少之前一些似是而非的東西,而且你能意識到基礎的重要。

因此,本文不會面面俱到,只是對TCP協議、算法和原理的科普。算法

我原本只想寫一個篇幅的文章的,可是TCP真TMD的複雜,比C++複雜多了,這30多年來,各類優化變種爭論和修改。因此,寫着寫着就發現只有砍成兩篇。shell

廢話少說,首先,咱們須要知道TCP在網絡OSI的七層模型中的第四層——Transport層,IP在第三層——Network層,ARP在第二層——Data Link層,在第二層上的數據,咱們叫Frame,在第三層上的數據叫Packet,第四層的數據叫Segment瀏覽器

首先,咱們須要知道,咱們程序的數據首先會打到TCP的Segment中,而後TCP的Segment會打到IP的Packet中,而後再打到以太網EthernetFrame中,傳到對端後,各個層解析本身的協議,而後把數據交給更高層的協議處理。緩存

TCP頭格式

接下來,咱們來看一下TCP頭的格式安全

TCP頭格式服務器

你須要注意這麼幾點:cookie

  • TCP的包是沒有IP地址的,那是IP層上的事。可是有源端口和目標端口。
  • 一個TCP鏈接須要四個元組來表示是同一個鏈接(src_ip, src_port, dst_ip, dst_port)準確說是五元組,還有一個是協議。但由於這裏只是說TCP協議,因此,這裏我只說四元組。
  • 注意上圖中的四個很是重要的東西:
    • Sequence Number是包的序號,用來解決網絡包亂序(reordering)問題。
    • Acknowledgement Number就是ACK——用於確認收到,用來解決不丟包的問題
    • Window又叫Advertised-Window,也就是著名的滑動窗口(Sliding Window),用於解決流控的
    • TCP Flag ,也就是包的類型,主要是用於操控TCP的狀態機的

關於其它的東西,能夠參看下面的圖示

image

TCP的狀態機

其實,網絡上的傳輸是沒有鏈接的,包括TCP也是同樣的。而TCP所謂的「鏈接」,其實只不過是在通信的雙方維護一個「鏈接狀態」,讓它看上去好像有鏈接同樣。因此,TCP的狀態變換是很是重要的。

下面是:「TCP協議的狀態機」(圖片來源) 和 「TCP建連接」、「TCP斷連接」、「傳數據」 的對照圖,我把兩個圖並排放在一塊兒,這樣方便在你對照着看。另外,下面這兩個圖很是很是的重要,你必定要記牢。(吐個槽:看到這樣複雜的狀態機,就知道這個協議有多複雜,複雜的東西老是有不少坑爹的事情,因此TCP協議其實也挺坑爹的)

image

image

不少人會問,爲何建連接要3次握手,斷連接須要4次揮手?

  • 對於建連接的3次握手,主要是要初始化Sequence Number 的初始值。通訊的雙方要互相通知對方本身的初始化的Sequence Number(縮寫爲ISN:Inital Sequence Number)——因此叫SYN,全稱Synchronize Sequence Numbers。也就上圖中的 x 和 y。這個號要做爲之後的數據通訊的序號,以保證應用層接收到的數據不會由於網絡上的傳輸的問題而亂序(TCP會用這個序號來拼接數據)。

  • 對於4次揮手,其實你仔細看是2次,由於TCP是全雙工的,因此,發送方和接收方都須要Fin和Ack。只不過,有一方是被動的,因此看上去就成了所謂的4次揮手。若是兩邊同時斷鏈接,那就會就進入到CLOSING狀態,而後到達TIME_WAIT狀態。下圖是雙方同時斷鏈接的示意圖(你一樣能夠對照着TCP狀態機看):

    兩端同時斷鏈接

     

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

  • 關於建鏈接時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 – Wikipedia語條),因此,只要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… )

image

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

注意:若是你用Wireshark抓包程序看3次握手,你會發現SeqNum老是爲0,不是這樣的,Wireshark爲了顯示更友好,使用了Relative SeqNum——相對序號,你只要在右鍵菜單中的protocol preference 中取消掉就能夠看到「Absolute SeqNum」了

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。示意圖以下:

image

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則是彙報收到的數據碎版。參看下圖:

image

這樣,在發送端就能夠根據回傳的SACK來知道哪些數據到了,哪些沒有到。因而就優化了Fast Retransmit的算法。固然,這個協議須要兩邊都支持。在 Linux下,能夠經過tcp_sack參數打開這個功能(Linux 2.4後默認打開)。

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

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

Duplicate SACK – 重複收到數據的問題

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

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

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

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

示例一:ACK丟包

下面的示例中,丟了兩個ACK,因此,發送端重傳了第一個數據包(3000-3499),因而接收端發現重複收到,因而回了一個SACK=3000-3500,由於ACK都到了4000意味着收到了4000以前的全部數據,因此這個SACK就是D-SACK——旨在告訴發送端我收到了重複的數據,並且咱們的發送端還知道,數據包沒有丟,丟的是ACK包。

Transmitted  Received    ACK Sent
Segment      Segment     (Including SACK Blocks)

3000-3499    3000-3499   3500 (ACK dropped)
3500-3999    3500-3999   4000 (ACK dropped)
3000-3499    3000-3499   4000, SACK=3000-3500

** 示例二,網絡延誤**

下面的示例中,網絡包(1000-1499)被網絡給延誤了,致使發送方沒有收到ACK,然後面到達的三個包觸發了「Fast Retransmit算法」,因此重傳,但重傳時,被延誤的包又到了,因此,回了一個SACK=1000-1500,由於ACK已到了3000,因此,這個SACK是D-SACK——標識收到了重複的包。

這個案例下,發送端知道以前由於「Fast Retransmit算法」觸發的重傳不是由於發出去的包丟了,也不是由於迴應的ACK包丟了,而是由於網絡延時了。

Transmitted    Received    ACK Sent
Segment        Segment     (Including SACK Blocks)
500-999        500-999     1000
1000-1499      (delayed)
1500-1999      1500-1999   1000, SACK=1500-2000
2000-2499      2000-2499   1000, SACK=1500-2500
2500-2999      2500-2999   1000, SACK=1500-3000
1000-1499      1000-1499   3000
1000-1499   3000, SACK=1000-1500

可見,引入了D-SACK,有這麼幾個好處:

1)可讓發送方知道,是發出去的包丟了,仍是回來的ACK包丟了。
2)是否是本身的timeout過小了,致使重傳。
3)網絡上出現了先發的包後到的狀況(又稱reordering)
4)網絡上是否是把個人數據包給複製了。

知道這些東西能夠很好得幫助TCP瞭解網絡狀況,從而能夠更好的作網絡上的流控

Linux下的tcp_dsack參數用於開啓這個功能(Linux 2.4後默認打開)

好了,上篇就到這裏結束了。若是你以爲我寫得還比較淺顯易懂,那麼,歡迎移步看下篇《TCP的流迭、擁塞處理

感謝大神塗耀輝

做者:Cooci_和諧學習_不急不躁 連接:https://www.jianshu.com/p/69695f332a71 來源:簡書 簡書著做權歸做者全部,任何形式的轉載都請聯繫做者得到受權並註明出處。

相關文章
相關標籤/搜索