簡單聊聊TCP的可靠性

前言

首發自 blog.cc1234.cc/html

傳輸控制協議(縮寫:TCP)是一種面向鏈接的、可靠的、基於字節流傳輸層通訊協議,由IETFRFC 793定義。算法

TCP在不可靠的IP協議之上實現了可靠性, 從而使得咱們沒必要再去關注網絡傳輸中的種種複雜性,所謂的可靠就是讓咱們去信任它便可緩存

信任歸信任,可咱們仍是的得去了解它,知道它爲什麼值得信任,信任主要體如今哪些方面,換句話說就是安全

  • TCP的可靠性是什麼
  • TCP如何實現的可靠性

上面的問題就是本文討論的核心點markdown

TCP的可靠性實則是一個很大的話題,不少細節都值得深究,因爲本人水平有限,文中不少描述都沒有深刻甚至可能有錯誤,讀者如有不一樣觀點,儘可提出網絡

什麼是可靠性

其實在RFC 7931.5 Operation專門對Reliability(可靠性)作了說明異步

總結下來以下oop

確保一個進程從其接受緩存中讀出的數據流是無損壞,無間隔,非冗餘和按序的數據流;即字節流與鏈接的另外一方端系統發送出的字節流是徹底相同的學習

須要解決的問題

前面說到的可靠性,提到了無損壞,無間隔,非冗餘和按序等幾個關鍵詞, 而在網絡中要實現這些指標,咱們都有對應的問題須要去解決spa

其中最典型的幾個問題以下

  • 干擾

    網絡的干擾多是由於硬件故障致使數據包受到破壞, 也有多是網絡波動致使數據包的某些bit位產生了變化

    題外話:這裏不的干擾並不包含惡意攻擊,惡意攻擊是屬於傳輸安全的範疇了,好比咱們熟知的SSL/TLS就是一個成熟的網絡傳輸安全問題的解決方案

    以下圖,發送的111 因爲干擾變成了101

  • 亂序

    發送方連續前後發送兩個數據包, 後發送的數據包可能先到達接收方,若是接收方按接收順序處理數據包,這就會致使接收的數據包與發送的數據包不一致。

    形成這樣的緣由是由於每個數據包都會根據當時的網絡狀況選擇不一樣的路由進行傳輸, 就像從開車從上海到北京有不少路線可選,不必定你先出發就能先到(我沒去過北京,請不要槓我......)

    以下圖,發送方順序發送了 A -> B -> C三個數據包, 然而接收方多是以A -> C -> B這樣的順序接收的報文,很明顯 B 和 C兩個個報文的順序不符合指望,產生了亂序

  • 丟包

    網絡丟包是一個很常見的現象,形成的緣由也多種多樣,比較常見的有

    1. 接收方因爲緩存溢出,致使沒法再處理到來的數據包了,直接丟棄從而形成丟包

    2. 網絡擁塞致使數據包丟包

    3. 數據包被檢測到損壞了,被接收方丟棄形成了丟包

    4. ......

    下圖展現了這種狀況,發送的數據CBA因爲A產生了丟包,致使接收方只收到了CB

  • 冗餘

    發送方可能由於某些緣由重複發送了同一個數據包,接收方要有能力處理這種冗餘數據包

    好比發送方發送的一個數據包由於網絡擁塞遲遲沒有被接收方收到, 發送方認爲產生了丟包就又重發了一次,結果最終接收方收到了兩個一樣的數據包,產生了數據冗餘

在繼續往下看以前,能夠先思考一下: 你會如何去解決這些問題?

0x01 解決干擾

爲了可以檢測到數據包在傳輸過程當中是否發生了差錯,TCP引入了checksum

checksum的具體細節能夠查閱RFC1071

下圖是TCP的報文結構,藍色部分就是checksum

checksum是一個16bit長的字段,發送方在計算checksum時會先將報文中的Checksum置零,而後基於整個報文(頭部 + 數據部分)計算出checksum

實際上還會加上96bit的僞頭部,能夠參考RFC 793 Header Format 一節

接收方在收到報文後也會計算checksum

  • 若是計算結果符合指望值,說明數據包沒有收到干擾/損壞
  • 若是不符合指望,通常會直接丟棄該數據包

TCP的校驗和也有必定的限制,並不必定100%能檢測到數據包產生的錯誤

這就和咱們日常作API開發時的簽名同樣

0x02 解決亂序和冗餘

請先回顧如下前面談到亂序時的一個示例圖

亂序有多個解決方案,好比發送一個數據後,我確認該發送的數據被接收方接收了我再發下一個,這樣確定是有序的, 可是這樣的方案對網絡利用率實在是過低了

另外一個很樸實的解決方案就是爲每一個報文標上序號, 這樣接收方在收到報文後只須要按序號對報文排序就能夠獲得有序的報文了,過程以下圖所示

實際上TCP協議採用的就是爲報文加上序號這樣的方法

TCP的報文結構上維護着一個Sequence Number(下面簡稱seq),以下圖的紅色區域所示

TCP的發送端和接收端各自獨立維護一個seq, seq的初始值是在建立鏈接時初始化的(值是隨機的)

有一個控制位SYN就是專門用來在發送方和接收方同步seq的 (這裏的同步指的是讓對方知道本身的seq初始值是多少)

詳細內容參考RFC 793 的 3.3. Sequence Numbers

創建鏈接後的每個報文都會攜帶seq

以下圖所示,假設初始seq=1,發送的第一個報文A的長度爲12, 那麼發送第二個報文Bseq=1+12=13

接收方接收到多個報文後,能夠基於seq多數據包進行升序排序,而且經過檢查seq的值,能夠判斷接收的數據是否有間隔,以及數據是不是有序的。

除此以外,seq使得TCP有能力處理重複數據包的問題,由於接收方能夠根據seq判斷出該數據包是否是已經被接收了,這樣順帶還解決了數據冗餘的問題

0x03 解決丟包

丟包的緣由可能各式各樣,咱們從一個簡單的場景開始開始分析

假設沒有丟包的狀況下,如何讓發送方知道接收方已成功接收到數據包了呢?

這就像人與人之間交談,你如何判斷對方聽見了呢?

現實生活中咱們靠的是對方的響應來作出判斷

TCP也採用相似的機制,咱們通常稱之爲ACK(Acknowledgment):接收方在收到數據包之後會對發送方響應一個特定的數據包.

仍是繼續看一下TCP的結構圖,注意綠色區域的Acknowledgment Number(後面簡稱ack

注意大寫ACK和小寫ack是有區別的

大寫ACK通常指的是報文的類型

小寫ack指的就是這個32bit長的號碼

ackseq都是32bit長,前面咱們說到TCP創建鏈接後發送的報文都會帶上seq, 接收方在收到報文後,會響應一個類型爲ACK的報文

報文的acknowledgment number的值是接收方下次指望收到的報文的seq

實際上ack的值會受不少狀況影響, 好比TCP的累積確認機制, 選擇重傳機制等等都會影響響應的ack值,細節能夠參考RFC 793

有了ACK後, 發送方就能夠知道報文有沒有被正確接收了

請看下圖,這是一個簡單的交互, 發送方發送數據,接收方確認數據後作出響應

前面咱們都是基於沒有丟包的狀況進行分析的,ACK並無解決丟包的問題,以下圖所示,發送的數據若是丟包了就沒有ACKACK若是丟包了就不知道數據是否被正確接收。

此時咱們引入超時重傳, 結合ACK機制一塊兒來應對丟包的問題

  • 發送方發送一個未被確認的數據包後就啓動一個計時器
  • 若是在指定時間內沒有收到ACK, 發送方能夠重傳該報文

超時重傳 + ACK也有一個小問題,就是最開始提到的數據冗餘問題

重傳數據包可能致使接收方接收到多個重複的數據包, 若是你還沒忘記的話,這個能夠經過前面一節說到的seq去解決

實際上TCP的擁塞控制也是處理丟包的有效機制之一,有興趣的同窗能夠去了解

0x04 基本可靠

在回顧一下咱們最開始對可靠的要求

確保一個進程從其接受緩存中讀出的數據流是無損壞,無間隔,非冗餘和按序的數據流......

前面咱們經過checksum, seq,ack,超時重傳等機制,算是達到了一個可靠性的基本要求, 爲何說是基本可靠呢?

由於到目前爲止咱們的場景還都相對簡單,因此會忽略掉不少變量和細節問題。

好比咱們都沒有提到很重要的滑動窗口擁塞控制算法等, 但實際上這些也是TCP在複雜的網絡環境中實現可靠性不可獲取的東西

0x05 番外篇

本節做爲番外篇,能夠認爲是對前面內容的一些補充,補充主要也是針對一些細節的地方,以FAQ的方式進行表述

  1. 每一個seq都須要一個ack嗎?

    固然不是

    發送方能夠直接發送多個報文, 接收方在接收到報文後先不急着響應ack,由於後續報文可能立刻到達, 這就是ACK延遲確認

    延遲確承認以讓咱們同時對多個接受的報文進行一次確認,這個又稱之爲累計確認

    這樣接收方就沒必要對每一個報文都進行確認,接收到多個報文後若是延遲時間內沒有報文到來,就發送下一個指望接收報文的ack, 以下圖所示

  2. 上圖中,若是同時發送多個seq報文,若中間某一個丟包,ack如何響應呢?

    通常接收方會有一個接收緩衝,會緩存接收到的報文,這些報文按seq值排序, 若是中間缺乏某段報文,那麼接收方就會響應這段報文的seq值

    以下圖所示,發送方發送了seq=1,seq=2,seq=3的報文, seq=2丟包, 可是接收方緩存了1和3,

    因此知道這部分報文不連續,中間缺乏2,因此響應了一個ack=2 (通常叫作最小ack)

    發送方重發seq=2的報文, 接收方發現1,2,3已經完整接收了,就響應下一個指望值, 即響應ack=4

  1. 每發送一個報文就啓動一個定時器嗎?

    爲了保證可靠性,TCP增長了超時重傳機制, 使得每一個未被確認(ACK)的報文在必定時間後能夠被從新發送

    一種實現方式就爲每一個未被確認的報文都單獨配置一個計時器,但是這樣作的話開銷太大了

    RFC 6298Managing the RTO Timer中說起了一種單一計時器的管理方式(具體細節請參考文檔)

    每一個已發送但未確認數據包都會被放進隊列裏, 這個隊列持有一個單獨的計時器

    當第一個數據包進入隊列時,計時器啓動了

    若是計時器超時,隊列頭部的數據包會被重發,而且計時器從新計時

    當收到ACK時,計時器也會重啓

    隊列的全部數據都被確認了的話,就關閉定時器

  2. 超時時間怎麼設置呢?

    重傳超時時間(Retransmission TimeOut), 通常簡稱RTO, 這個時間既不能太長也不能過短。

    • 太長可能會出現數據包已經丟了,但還要等待無謂的時間才能重傳

    • 過短可能數據包還沒有到達,此時發生重傳,浪費了資源

    往返時延 RTT (Round Trip Time)是配置RTO的一個重要指標, 可是因爲網絡間端到端的RTT並非固定的,因此TCP採用了一種自適應的方法來計算RTT, 而且根據計算的值來配置RTO

    整個過程是動態的,也就是說當RTT變化時,RTO也能相應的作出調整

    具體的細節能夠參考RFC 6298The Basic Algorithm

  3. 必定要超時了才重傳嗎?

    TCP有一個快速重傳機制, 當一個接收方收到三個以上的重複ack時,接收方就會直接根據ack的值重傳對應的報文而無需等待超時

    下圖展現了一個簡單的示例

 1. 發送方發送了seq=1, seq=2, seq=3, seq=4的報文
   
 2. seq=1的報文發送了丟包
   
 3. 接收方分別接受到了`2,3,4`的報文,接收方緩存收到的報文,而後檢查seq知道報文不連續,缺乏`seq=1`的報文, 因此每次都響應`ack=2`
   (爲了簡化描述,咱們不考慮延遲確認和緩衝區大小)
   
 4. 發送方連續收到了3個`ack=2`的報文,因此認爲`seq=1`的報文丟包了,重傳`seq=1`
   
 5. 接收方收到`seq=1`的報文後,發現seq=2,seq=3,seq=4已經接收過了,直接響應`ack=5`
複製代碼

總結

咱們看見TCP在實現可靠性上作出了不少精妙的設計,這些設計在大的方面追求至簡,而在細節又追求極致,絕對是很是值得學習和思考的

TCP的這些設計你也能夠在如今的軟件系統中看到它的影子

  • 好比消息隊列有相似的ack機制去確保消息已經被投遞
  • 好比爲了保證異步消息的有序性也會有相似seq的機制

正如前言所說,可靠性實則是一個很大的話題,不可避免的我仍是留下了不少坑,若是有錯誤的地方,還望指出。

參考

  1. James F.Kurose, Keith W.Ross 著 陳鳴譯。《計算機網絡 自頂向下方法》 (第六版)
  2. 維基百科 TCP
  3. RFC 793
  4. RFC 1071
  5. RFC 6298
  6. 一的補數
  7. 二的補數
  8. 往返時延
  9. TCP超時重傳機制
  10. 擁塞控制
相關文章
相關標籤/搜索