一、前言
這幾天寫了四篇TCP
系列的博客,這一篇是第五篇,也預計是這段時間的最后一篇了,寫完這篇我就要開始進行網絡層的研究了。若對於我其他TCP
方面的博客感興趣,可以去我個人博客的計算機網絡這一分類中查閱。這篇博客就來談一談TCP
是通過哪些手段,來保證可靠數據傳輸的。
二、正文
2.1 網絡傳輸存在的問題
研究TCP
如何保證可靠數據傳輸之前,我們先來列舉一下網絡傳輸存在什么問題,只有發現了問題,才能對症下葯,找出應對的方法。TCP
是依靠網絡層的IP
協議來發送數據,而IP
協議是一個不可靠的協議,它僅僅只是盡最大努力傳輸,但是並不保證數據能夠完好地到達,甚至不能保證數據能否到達。同時,網絡中允許傳輸的最大單元(MTU)是有限制的,一般為1500
個字節。所以,TCP
為了發送比這個大的數據,需要將數據拆分成一個個數據段進行發送。正因為這些原因,網絡傳輸將存在以下問題:
- 數據在傳輸的過程中被損壞,比特的
0
變成1
,或者1
變成0
; - 數據在傳輸過程中丟失,沒有到達目的地;
- 多個報文段沒有按順序到達接收方,因此接收方無法正確地將數據組合;
TCP
的實現基本上就是圍繞上面三個問題,以及如何提高傳輸速率實現的。下面我們就來說一說TCP
為了應對上面這些問題,做了哪些事情。
2.2 TCP解決數據損壞
我們先來討論第一個問題,數據發生損壞。檢驗數據是否損壞的方式就是數據校驗,以下是TCP報文的格式,可以看到其中有一個16
位的字段,叫做校驗和,這就是接收方用來校驗數據是否出錯的部分。
這個字段的名稱叫做校驗和,因為TCP
對數據校驗的方式就是校驗和算法,這個算法的過程如下:
- 將校驗和字段置為
0
,然后將數據部分以16
位為一個單位,進行拆分; - 將拆分后的若干個單元進行二進制相加,若相加結果進位到了第
17
位,則將17
位加到第一位(其實就是補碼和運算,也稱為回卷),最終相加的結果取反(0
變成1
,1
變成0
),然后放入校驗和字段; - 報文段被發送到目的地后,目標主機也進行上面兩步,然后將運算的結果與首部中的校驗和字段相加,再取反,若結果為
0
,則認為數據沒有出錯;
這樣是如何起到校驗的作用呢?其實很簡單,假設在數據傳輸的過程中,數據沒有出錯,則在發送端和接收端,求出的結果都是一樣的。而校驗和字段,就是這個結果的反碼,也就是說,結果中為1
的位,在反碼中就為0
,而結果中為0
的位,在反碼中就是1
。也就是說,在數據沒有出錯的情況下,這個結果與校驗和字段相加,一定是全1
,而此時再取反,就是0
了。所以只要最終求得的結果不是0
,接收方就認為數據出錯。若數據出錯,則接收方會直接將數據丟棄,發送方在一段時間后,沒有接收到ACK報文,就會在超時后,重傳這個報文。
但是,上面這個算法一定可以校驗出數據是否出錯嗎?答案是不能。因為這個校驗的根本,其實不過是將數據求和,然后判斷這個和有沒有改變而已。而我們都知道,1+2 == 2+1
,1 + 4 == 2 + 3
,僅僅依靠求和,根本無法保證數據的沒有發生改變。只要數據的多處都發生了錯誤,而且相互抵消,這種算法將無法察覺。但是TCP
依然采用了這種算法,我個人認為原因是實現簡單,而且網絡中數據出錯的概率不高,而多處出錯並相互抵消的概率就更小了,所以這種算法的可靠性還是比較高的。
2.3 TCP解決丟包問題
TCP
解決數據丟失的方法就是超時重傳。TCP
將維護一個計時器,並設置一個超時時間,當發送一個TCP
報文段后,沒有在超時時間之內得到ACK報文,則發送方將認為數據丟失,於是將重傳丟失的報文段,直至接收。由於TCP
使用的是流水線傳輸,在同一時間內,可能有多個已經發送但沒有接收到ACK
的報文段,所以按理來講,TCP
將維護多個計時器,為每一個報文段綁定一個,但是這樣做將產生較大的開銷,而且對於計時器的管理也很復雜。所以在TCP
中,實際上只會維護一個計時器,記錄的是當前最早被發送,但是還沒有接收到ACK
報文的報文段。當這個報文段超時,發送方將重傳報文段,並重啟計時器;若收到這個報文段的ACK
報文,同樣重啟計時器,不過此時最早被發出但還沒有被確認的報文段已經發生了改變,此時記錄的就是這個新的報文段的傳輸時間。除此之外,觸發快速重傳時,計時器也會重新啟動。關於TCP
的流水線傳輸,可以參考我的這一篇博客:https://www.cnblogs.com/tuyang1129/p/12450978.html。
這里就有一個復雜的問題了,這個超時時間將要如何設定?不難想到,這個超時時間應該要略大於數據的往返時間(RTT),比如數據從發送到接收到ACK
報文,共用了200ms
,那超時時間應該略大於這個值,比如400ms
。但是網絡是不穩定的,對於每一個報文,因為所通過的路徑不同,網絡擁塞程度的不同,往返時間或多或少都會發生改變。所以對於這個超時時間,應該基於平均RTT進行計算。但是直接統計求平均值就太粗糙了,對於TCP
來說,有一套復雜的算法來計算這個超時時間。
要計算RTT
的大致平均值,首先得有樣本值,假設樣本RTT定義為SampleRTT
。TCP
程序在運行期間,可能會在任意時刻測量一次SampleRTT
,即測量一個報文從發出到接收ACK
所用的時間,然后將它用於計算RTT
的加權平均值。然后過一段時間再進行一次,並將新測出的SampleRTT
用於更新加權平均值。假設這個加權平均值RTT
定義為EstimatedRTT
,在TCP
規范中,計算EstimatedRTT
的公式為:
EstimatedRTT = (1- α)*EstimatedRTT + α * SampleRTT
(公式一)
其中SampleRTT
就是最新測得的樣本RTT
,通過以上公式就能動態地確定RTT
的加權平均值。由於越晚測量的SampleRTT
,越接近網絡中當前的狀況,所以在更新EstimatedRTT
的過程中,最新的SampleRTT
應該要占據更多的比重,所以在TCP
規范中,建議將α
的值設定為1/8
,所以上面的公式就是:
EstimatedRTT = 0.875 * EstimatedRTT + 0.125 * SampleRTT
而SampleRTT
與EstimatedRTT
的波動圖如下所示:
除了求RTT
的加權平均值,網絡中RTT
的變化情況也是很有必要的,畢竟從上圖可以看出,樣本RTT
的波動十分劇烈,只有EstimatedRTT
,還不足以讓我們准確的估算超時時間。所以我們需要求出SampleRTT
與EstimatedRTT
的偏離程度,也就是類似於方差,根據方差,來動態地設置超時時間。假設這個方差定義為DevRTT
,則TCP
規范中定義DevRTT
的計算公式如下:
DevRTT = (1 - β)* DevRTT + β * | SampleRTT - EstimatedRTT |
(公式二)
由上面公式可以看出,如果SampleRTT
的波動很大,DevRTT
的值就會很大,反之就會很小。而在TCP
規范中β
的推薦值是0.25
。我們現在知道了RTT
的加權平均值,也知道了RTT
的波動情況,現在就該考慮怎么設置超時時間了。不難想到,超時時間應該需要比RTT
的加權平均值,也就是EstimatedRTT
要大一些,讓大部分報文段的RTT
都小於這個值,以免頻繁超時重傳。那應該大多少呢?這樣考慮,當網絡波動較為劇烈時,表示實際RTT
應該會離EstimatedRTT
遠一些,而波動較小時,實際RTT
應該會接近於EstimatedRTT
,而這個波動情況的數值,我們已經計算過了,就是上一個公式中的DevRTT
,所以假設超時時間定義為TimeoutInterval
,TCP
規范推薦使用以下方式來計算它的值:
TimeoutInterval = EstimatedRTT + 4 * DevRTT
(公式三)
這樣,不論是加權平均值還是網絡的波動情況就都考慮到了。而TCP
規范中推薦的初始TimeoutInterval
為1s
(當時從書上看到這部分內容,才深刻體會到了數學的強大,真真的將理論應用於實際)。當然,對於超時時間的計算,還有兩個特例:
- 當某個報文段超時,發送方將重傳這個報文段,同時重新開啟定時器,而超時時間將會設置為上一次的兩倍,而不是使用公式三計算的值;若還是超時,則繼續重傳,超時時間再次擴大兩倍,直到這個報文被成功接收,而成功接收時,才使用公式三重新計算超時時間。這么做的目的是為了防止多次超時導致連續重傳,從而導致網絡擁塞更加嚴重,畢竟超時就是網絡擁塞的結果。
- 記錄樣本
RTT
時,不會選擇重傳的報文段作為樣本,這是因為,當發生超時事件時,發送方並不知道數據是因為丟失還是因為網絡延遲而超時。若報文段是因為延遲而超時,則重傳報文后,這時延遲的ACK
報文到達,發送方將誤以為重傳的分組被正確接收,於是將測出一個錯誤的SampleRTT
。
總之,TCP
的超時重傳機制,很好地解決了網絡中發生數據丟包的問題。而且,為了提高效率,TCP
還有一種快速重傳機制,可以根據特定情況,在超時前就判斷報文段丟失,然后進行重傳,不過這里就不詳細敘述了。
2.4 TCP如何解決數據亂序到達
第三個問題就是數據的亂序到達問題。由於網絡的限制,TCP
必須將較大的數據拆分成一個個較小的報文段,封裝成TCP
報文段,逐個傳輸。由於網絡傳輸的不確定性(比如所通過的路徑不同,某個報文段丟失然后重傳等),這些報文段完全有可能不是按照順序到達。所以,為了在接收方能夠完整的接收數據,並能按序將這些報文組合起來,TCP
必須有一種機制解決這個問題。
TCP
所使用的方法就是為每一個TCP
報文段分配一個序號,每個報文段的序號依次增加,這樣接收方就可以根據序號,來確定接收到的報文段是整個數據中的哪一部分,以及是否接收到了所有的部分。從上面那張TCP
報文結構圖中我們可以看到,其中有一個32位
的序號字段。但是,TCP
對報文段的編號可不是0,1,2,3
....這么簡單,下面我們就來說說TCP
是如何實現這種序號機制的。
首先我們要明確一個點,TCP是對字節進行編號,而不是對報文段進行編號。TCP
對需要發送的數據的每一個字節都賦予了一個編號,比如第一個字節為0
號,第二個為1
號,以此類推。而每一個報文段一般都不止封裝一個字節的數據,所以在TCP
報文段中,封裝的是這個報文段的數據中,第一個字節的序號。舉個例子,比如說發送方要發送250
字節的數據,假設初始序號從0
開始,則這250
個字節的序號分別是0-249
。再假設每一個報文段最多允許封裝100
個字節的數據,所以第一個報文段將封裝第1
到100
個字節,這些字節的序號為0-99
,所以第一個報文段會將0
放入它首部中的序號部分;而第二個報文段封裝100-199
號字節,所以它的序號為100
;而第三個報文段封裝200-249
號字節,所以它的序號為200
。以上就是TCP
發送方對序號的處理方法。
下面來說一說TCP
的接收方如何在這種序號機制中工作。同樣以上面三個報文段舉例,假設發送方將上面三個報文段發出,接收方接收到第一個報文段,發現這個報文的序號是0
,同時包含100
個字節的數據,於是接收方將會向發送方確認已經接收到這個報文,而確認的方式就是使用TCP
首部中的確認序號字段。接收方接收到序號為0
,長度為100
個字節的報文段后,將會在ACK
報文的確認序號中填入100
,表示自己已經接收到序號小於100
的全部字節,希望下一個接收到的報文段的序號是100
;而第二個報文段按序到達,序號為100
,長度為100
字節,於是接收方再次回送ACK
報文,此時確認序號將為200
,表示自己接收到200
以前的全部字節,希望下一條報文的序號是200
;然后接收到序號為200
,長度為50
字節的報文段,將回送確認序號為250
的ACK報文。
以上是按順序接收到報文段的情況,假設在上面的情況中,三條報文到達的先后順序是0 -> 200 -> 100,也就是順序被打亂,則將發生以下情況:
- 接收方接收到序號
0
,長度100
字節的報文段,回送確認號為100
的ACK
報文; - 接收到序號
200
,長度為50
字節 的報文段,此時接收方希望接收到的是100
號報文,於是判斷發生了亂序到達的情況,不向上層交付這一段數據,而是將其放入接收緩存; - 接收到序號為
100
,長度為100
的報文段,100
正是接收方期待接收到的報文段序號,於是將其接收並交付給上層,同時發現在接收緩存中存在序號為200
的報文段,這正是接收方期待接收到的下一條報文,於是將其取出,交付上層,同時向發送方發送ACK
報文,ACK
的確認序號為250
,表示自己已經接收到250
之前的所有字節,下一條期望到達的報文的序號是250
;
通過上面的機制,接收方成功解決了數據亂序到達的問題。當然,對於這種序號機制的使用,其實不止這么簡單,這其中還牽涉到TCP的流水線傳輸機制,若想要了解,可以參考我的另外一篇博客——https://www.cnblogs.com/tuyang1129/p/12450978.html。
這里還有一個問題,在上面的例子中,我假設序號是從0
開始,但是實際情況並非如此。在實際的實現中,序號一般是一個通過特殊算法計算出的隨機值,這樣做的原因有兩點:
- 假設每一個
TCP
連接的序號都是從0
開始,那么假設客戶端先向服務器發送了一個報文,還沒有確認接收后,立即斷開連接;但是在斷開后,它們立刻又建立了一個連接,而此時,第一次發送出去的報文段才剛到達服務器,那會發生什么情況。服務器會以為這是新建立的連接發送的數據,而由於兩次連接的初始序號都是0
,接收方將會把這個報文段接收。為了減小類似情況發生的概率,TCP
采用隨機初始序號,這樣兩次連接的初始序號將大概率不同,再發生這種情況時,接收方也不會接收這個報文段; - 第二個原因就是出於安全性考慮,若初始序號都是固定的,那每一個報文段的序號完全可以推測得出,於是就有黑客可以利用這一點,模擬發送方發送TCP報文,做出攻擊,比如發送大量連接請求,占用服務器資源;
2.5 TCP的流量控制與擁塞控制
流量控制與擁塞控制,嚴格來講並不是TCP
的可靠傳輸機制,但是也算是有點關系,所以我還是提一下。
- 流量控制:
TCP
的接收方會維持一個接收緩存,用以接收發送方發送的數據。但是,接收緩存不是無限大的,若接收緩存被占滿,此時再接收到數據,將無法進行接收,只能將其丟棄。於是,為了減少這種情況的發生,TCP
接收方需要告知發送方,自己最多還能接收多少數據,TCP
發送方根據這個信息,有選擇的發送數據,這就是流量控制; - 擁塞控制:和流量控制類似,但是限制發送數據多少的不是接收方,而是路由器。路由器也有接收緩存,若路由器的接收緩存中存在過多的數據,也會對網絡傳輸造成影響,這就是網絡中丟包的原因,而擁塞控制就是根據網絡的擁塞狀況控制發送數據的速度;
這兩種機制中,流量控制相對簡單。我們可以看到,在TCP
的報文格式中,有一個叫做窗口大小的部分,這部分就是接收方告訴發送方,自己當前最多還能接收多少數據,而發送方將發送小於這個窗口大小的數據長度。但是有一種特殊情況,若這個窗口大小為0
,表示當前窗口已滿,正常情況下發送方將無法發送數據,但是在實際情況中,發送方還是會發送一個字節的數據到接收方,作為一種試探。因為接收方一般不會主動向發送方發送報文,這個窗口大小一般是攜帶在ACK
報文中,若此時窗口大小為0
,發送方將不再發送數據,接收方也就無法向發送方發送ACK
報文,此時就算緩存被清理,發送方也不會知曉。所以即使這個窗口大小為0
,發送方仍然需要發送數據,進行試探,若緩存已經被清理,通過試探報文的ACK
報文,發送方就能知曉。
擁塞控制是TCP
中相對復雜的一種機制,不是三言兩語說的清楚的,這部分內容我專門寫了一篇的博客進行說明,感興趣的可以閱讀一下:https://www.cnblogs.com/tuyang1129/p/12439862.html。
三、總結
對於TCP
可靠傳輸的描述就介紹到這里。上面的內容是對TCP
可靠傳輸原理的基本介紹,但是具體實現可能會在這些基礎上進行改進和優化。TCP
的各種機制相輔相成,若是對於TCP
沒有太多了解,可能有些介紹會看不太懂,所以若想真正搞懂TCP
以及其他計算機網絡的相關知識,建議買一本書系統地研究。希望我的這篇博客對看到的人有所幫助,若博客內容有誤,希望可以指正。
四、參考
《計算機網絡——自頂向下方法(原書第七版)》