TCP是一個巨複雜的協議,由於他要解決不少問題,而這些問題又帶出了不少子問題和陰暗面。因此學習TCP自己是個比較痛苦的過程,但對於學習的過程卻能讓人有不少收穫。關於 TCP這個協議的細節,我仍是推薦你去看W.Richard Stevens的《TCP/IP 詳解 卷1:協議》(固然,你也能夠去讀一下RFC793以及後面N多的RFC)。另外,本文我會使用英文術語,這樣方便你經過這些英文關鍵詞來查找相關的技術文檔。html
之因此想寫這篇文章,目的有三個,linux
因此,本文不會面面俱到,只是對TCP協議、算法和原理的科普。程序員
我原本只想寫一個篇幅的文章的,可是TCP真TMD的複雜,比C++複雜多了,這30多年來,各類優化變種爭論和修改。因此,寫着寫着就發現只有砍成兩篇。算法
廢話少說,首先,咱們須要知道TCP在網絡OSI的七層模型中的第四層——Transport層,IP在第三層——Network層,ARP在 第二層——Data Link層,在第二層上的數據,咱們叫Frame,在第三層上的數據叫Packet,第四層的數據叫Segment。shell
首先,咱們須要知道,咱們程序的數據首先會打到TCP的Segment中,而後TCP的Segment會打到IP的Packet中,而後再打到以太網Ethernet的Frame中,傳到對端後,各個層解析本身的協議,而後把數據交給更高層的協議處理。segmentfault
TCP頭格式瀏覽器
接下來,咱們來看一下TCP頭的格式緩存
TCP頭格式(圖片來源)安全
你須要注意這麼幾點:服務器
關於其它的東西,能夠參看下面的圖示
(圖片來源)
TCP的狀態機
其實,網絡上的傳輸是沒有鏈接的,包括TCP也是同樣的。而TCP所謂的「鏈接」,其實只不過是在通信的雙方維護一個「鏈接狀態」,讓它看上去好像有鏈接同樣。因此,TCP的狀態變換是很是重要的。
下面是:「TCP協議的狀態機」(圖片來源) 和 「TCP建連接」、「TCP斷連接」、「傳數據」 的對照圖,我把兩個圖並排放在一塊兒,這樣方便在你對照着看。另外,下面這兩個圖很是很是的重要,你必定要記牢。(吐個槽:看到這樣複雜的狀態機,就知道這個協議有多複雜,複雜的東西老是有不少坑爹的事情,因此TCP協議其實也挺坑爹的)
不少人會問,爲何建連接要3次握手,斷連接須要4次揮手?
兩端同時斷鏈接(圖片來源)
另外,有幾個事情須要注意一下:
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… )
你能夠看到,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,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。示意圖以下:
Fast Retransmit只解決了一個問題,就是timeout的問題,它依然面臨一個艱難的選擇,就是重轉以前的一個仍是重裝全部的問 題。對於上面的示例來講,是重傳#2呢仍是重傳#2,#3,#4,#5呢?由於發送端並不清楚這連續的3個ack(2)是誰傳回來的?也許發送端發了20 份數據,是#6,#10,#20傳來的呢。這樣,發送端頗有可能要重傳從2到20的這堆數據(這就是某些TCP的實際的實現)。可見,這是一把雙刃劍。
另一種更好的方式叫:Selective Acknowledgment (SACK)(參看RFC 2018),這種方式須要在TCP頭裏加一個SACK的東西,ACK仍是Fast Retransmit的ACK,SACK則是彙報收到的數據碎版。參看下圖:
這樣,在發送端就能夠根據回傳的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又稱D-SACK,其主要使用了SACK來告訴發送方有哪些數據被重複接收了。RFC-2833裏有詳細描述和示例。下面舉幾個例子(來源於RFC-2833)
D-SACK使用了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的那些事(下)》
參考博文: