TCP是一個巨復雜的協(xié)議,因為他要解決很多問題,而這些問題又帶出了很多子問題和陰暗面。所以學習TCP本身是個比較痛苦的過程,但對于學習的過程卻能讓人有很多收獲。關(guān)于TCP這個協(xié)議的細節(jié),我還是推薦你去看W.Richard Stevens的《TCP/IP 詳解 卷1:協(xié)議》(當然,你也可以去讀一下RFC793以及后面N多的RFC)。另外,本文我會使用英文術(shù)語,這樣方便你通過這些英文關(guān)鍵詞來查找相關(guān)的技術(shù)文檔。
之所以想寫這篇文章,目的有三個,
所以,本文不會面面俱到,只是對TCP協(xié)議、算法和原理的科普。
我本來只想寫一個篇幅的文章的,但是TCP真TMD的復雜,比C++復雜多了,這30多年來,各種優(yōu)化變種爭論和修改。所以,寫著寫著就發(fā)現(xiàn)只有砍成兩篇。
廢話少說,首先,我們需要知道TCP在網(wǎng)絡OSI的七層模型中的第四層——Transport層,IP在第三層——Network層,ARP在第二層——Data Link層,在第二層上的數(shù)據(jù),我們叫Frame,在第三層上的數(shù)據(jù)叫Packet,第四層的數(shù)據(jù)叫Segment。
首先,我們需要知道,我們程序的數(shù)據(jù)首先會打到TCP的Segment中,然后TCP的Segment會打到IP的Packet中,然后再打到以太網(wǎng)Ethernet的Frame中,傳到對端后,各個層解析自己的協(xié)議,然后把數(shù)據(jù)交給更高層的協(xié)議處理。
TCP頭格式
接下來,我們來看一下TCP頭的格式
TCP頭格式(圖片來源)
你需要注意這么幾點:
關(guān)于其它的東西,可以參看下面的圖示
(圖片來源)
TCP的狀態(tài)機
其實,網(wǎng)絡上的傳輸是沒有連接的,包括TCP也是一樣的。而TCP所謂的“連接”,其實只不過是在通訊的雙方維護一個“連接狀態(tài)”,讓它看上去好像有連接一樣。所以,TCP的狀態(tài)變換是非常重要的。
下面是:“TCP協(xié)議的狀態(tài)機”(圖片來源) 和 “TCP建鏈接”、“TCP斷鏈接”、“傳數(shù)據(jù)” 的對照圖,我把兩個圖并排放在一起,這樣方便在你對照著看。另外,下面這兩個圖非常非常的重要,你一定要記牢。(吐個槽:看到這樣復雜的狀態(tài)機,就知道這個協(xié)議有多復雜,復雜的東西總是有很多坑爹的事情,所以TCP協(xié)議其實也挺坑爹的)
很多人會問,為什么建鏈接要3次握手,斷鏈接需要4次揮手?
兩端同時斷連接(圖片來源)
另外,有幾個事情需要注意一下:
Again,使用tcp_tw_reuse和tcp_tw_recycle來解決TIME_WAIT的問題是非常非常危險的,因為這兩個參數(shù)違反了TCP協(xié)議(RFC 1122)
其實,TIME_WAIT表示的是你主動斷連接,所以,這就是所謂的“不作死不會死”。試想,如果讓對端斷連接,那么這個破問題就是對方的了,呵呵。另外,如果你的服務器是于HTTP服務器,那么設置一個HTTP的KeepAlive有多重要(瀏覽器會重用一個TCP連接來處理多個HTTP請求),然后讓客戶端去斷鏈接(你要小心,瀏覽器可能會非常貪婪,他們不到萬不得已不會主動斷連接)。
數(shù)據(jù)傳輸中的Sequence Number
下圖是我從Wireshark中截了個我在訪問coolshell.cn時的有數(shù)據(jù)傳輸?shù)膱D給你看一下,SeqNum是怎么變的。(使用Wireshark菜單中的Statistics ->Flow Graph… )
你可以看到,SeqNum的增加是和傳輸?shù)淖止?jié)數(shù)相關(guān)的。上圖中,三次握手后,來了兩個Len:1440的包,而第二個包的SeqNum就成了1441。然后第一個ACK回的是1441,表示第一個1440收到了。
注意:如果你用Wireshark抓包程序看3次握手,你會發(fā)現(xiàn)SeqNum總是為0,不是這樣的,Wireshark為了顯示更友好,使用了Relative SeqNum——相對序號,你只要在右鍵菜單中的protocol preference 中取消掉就可以看到“Absolute SeqNum”了
TCP重傳機制
TCP要保證所有的數(shù)據(jù)包都可以到達,所以,必需要有重傳機制。
注意,接收端給發(fā)送端的Ack確認只會確認最后一個連續(xù)的包,比如,發(fā)送端發(fā)了1,2,3,4,5一共五份數(shù)據(jù),接收端收到了1,2,于是回ack 3,然后收到了4(注意此時3沒收到),此時的TCP會怎么辦?我們要知道,因為正如前面所說的,SeqNum和Ack是以字節(jié)數(shù)為單位,所以ack的時候,不能跳著確認,只能確認最大的連續(xù)收到的包,不然,發(fā)送端就以為之前的都收到了。
一種是不回ack,死等3,當發(fā)送方發(fā)現(xiàn)收不到3的ack超時后,會重傳3。一旦接收方收到3后,會ack 回 4——意味著3和4都收到了。
但是,這種方式會有比較嚴重的問題,那就是因為要死等3,所以會導致4和5即便已經(jīng)收到了,而發(fā)送方也完全不知道發(fā)生了什么事,因為沒有收到Ack,所以,發(fā)送方可能會悲觀地認為也丟了,所以有可能也會導致4和5的重傳。
對此有兩種選擇:
這兩種方式有好也有不好。第一種會節(jié)省帶寬,但是慢,第二種會快一點,但是會浪費帶寬,也可能會有無用功。但總體來說都不好。因為都在等timeout,timeout可能會很長(在下篇會說TCP是怎么動態(tài)地計算出timeout的)
于是,TCP引入了一種叫Fast Retransmit 的算法,不以時間驅(qū)動,而以數(shù)據(jù)驅(qū)動重傳。也就是說,如果,包沒有連續(xù)到達,就ack最后那個可能被丟了的包,如果發(fā)送方連續(xù)收到3次相同的ack,就重傳。Fast Retransmit的好處是不用等timeout了再重傳。
比如:如果發(fā)送方發(fā)出了1,2,3,4,5份數(shù)據(jù),第一份先到送了,于是就ack回2,結(jié)果2因為某些原因沒收到,3到達了,于是還是ack回2,后面的4和5都到了,但是還是ack回2,因為2還是沒有收到,于是發(fā)送端收到了三個ack=2的確認,知道了2還沒有到,于是就馬上重轉(zhuǎn)2。然后,接收端收到了2,此時因為3,4,5都收到了,于是ack回6。示意圖如下:
Fast Retransmit只解決了一個問題,就是timeout的問題,它依然面臨一個艱難的選擇,就是重轉(zhuǎn)之前的一個還是重裝所有的問題。對于上面的示例來說,是重傳#2呢還是重傳#2,#3,#4,#5呢?因為發(fā)送端并不清楚這連續(xù)的3個ack(2)是誰傳回來的?也許發(fā)送端發(fā)了20份數(shù)據(jù),是#6,#10,#20傳來的呢。這樣,發(fā)送端很有可能要重傳從2到20的這堆數(shù)據(jù)(這就是某些TCP的實際的實現(xiàn))。可見,這是一把雙刃劍。
另外一種更好的方式叫:Selective Acknowledgment (SACK)(參看RFC 2018),這種方式需要在TCP頭里加一個SACK的東西,ACK還是Fast Retransmit的ACK,SACK則是匯報收到的數(shù)據(jù)碎版。參看下圖:
這樣,在發(fā)送端就可以根據(jù)回傳的SACK來知道哪些數(shù)據(jù)到了,哪些沒有到。于是就優(yōu)化了Fast Retransmit的算法。當然,這個協(xié)議需要兩邊都支持。在 Linux下,可以通過tcp_sack參數(shù)打開這個功能(Linux 2.4后默認打開)。
這里還需要注意一個問題——接收方Reneging,所謂Reneging的意思就是接收方有權(quán)把已經(jīng)報給發(fā)送端SACK里的數(shù)據(jù)給丟了。這樣干是不被鼓勵的,因為這個事會把問題復雜化了,但是,接收方這么做可能會有些極端情況,比如要把內(nèi)存給別的更重要的東西。所以,發(fā)送方也不能完全依賴SACK,還是要依賴ACK,并維護Time-Out,如果后續(xù)的ACK沒有增長,那么還是要把SACK的東西重傳,另外,接收端這邊永遠不能把SACK的包標記為Ack。
注意:SACK會消費發(fā)送方的資源,試想,如果一個攻擊者給數(shù)據(jù)發(fā)送方發(fā)一堆SACK的選項,這會導致發(fā)送方開始要重傳甚至遍歷已經(jīng)發(fā)出的數(shù)據(jù),這會消耗很多發(fā)送端的資源。詳細的東西請參看《TCP SACK的性能權(quán)衡》
Duplicate SACK又稱D-SACK,其主要使用了SACK來告訴發(fā)送方有哪些數(shù)據(jù)被重復接收了。RFC-2833里有詳細描述和示例。下面舉幾個例子(來源于RFC-2833)
D-SACK使用了SACK的第一個段來做標志,
示例一:ACK丟包
下面的示例中,丟了兩個ACK,所以,發(fā)送端重傳了第一個數(shù)據(jù)包(3000-3499),于是接收端發(fā)現(xiàn)重復收到,于是回了一個SACK=3000-3500,因為ACK都到了4000意味著收到了4000之前的所有數(shù)據(jù),所以這個SACK就是D-SACK——旨在告訴發(fā)送端我收到了重復的數(shù)據(jù),而且我們的發(fā)送端還知道,數(shù)據(jù)包沒有丟,丟的是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 ---------
示例二,網(wǎng)絡延誤
下面的示例中,網(wǎng)絡包(1000-1499)被網(wǎng)絡給延誤了,導致發(fā)送方?jīng)]有收到ACK,而后面到達的三個包觸發(fā)了“Fast Retransmit算法”,所以重傳,但重傳時,被延誤的包又到了,所以,回了一個SACK=1000-1500,因為ACK已到了3000,所以,這個SACK是D-SACK——標識收到了重復的包。
這個案例下,發(fā)送端知道之前因為“Fast Retransmit算法”觸發(fā)的重傳不是因為發(fā)出去的包丟了,也不是因為回應的ACK包丟了,而是因為網(wǎng)絡延時了。
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)可以讓發(fā)送方知道,是發(fā)出去的包丟了,還是回來的ACK包丟了。
2)是不是自己的timeout太小了,導致重傳。
3)網(wǎng)絡上出現(xiàn)了先發(fā)的包后到的情況(又稱reordering)
4)網(wǎng)絡上是不是把我的數(shù)據(jù)包給復制了。
知道這些東西可以很好得幫助TCP了解網(wǎng)絡情況,從而可以更好的做網(wǎng)絡上的流控。
Linux下的tcp_dsack參數(shù)用于開啟這個功能(Linux 2.4后默認打開)
好了,上篇就到這里結(jié)束了。如果你覺得我寫得還比較淺顯易懂,那么,歡迎移步看下篇《TCP的那些事(下)》