1. 程式人生 > 程式設計 >簡單聊聊TCP的可靠性

簡單聊聊TCP的可靠性

前言

首發自 blog.cc1234.cc/

傳輸控制協議(縮寫:TCP)是一種面向連線的、可靠的、基於位元組流傳輸層通訊協議,由IETFRFC 793定義。

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

信任歸信任,可我們還是的得去了解它,知道它為何值得信任,信任主要體現在哪些方面,換句話說就是

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

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

TCP的可靠性實則是一個很大的話題,很多細節都值得深究,由於本人水平有限,文中很多描述都沒有深入甚至可能有錯誤,讀者若有不同觀點,儘可提出

什麼是可靠性

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

總結下來如下

確保一個程式從其接受快取中讀出的資料流是無損壞,無間隔,非冗餘和按序的資料流;即位元組流與連線的另一方端系統傳送出的位元組流是完全相同的

需要解決的問題

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

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

  • 幹擾

    網路的幹擾可能是因為硬體故障導致資料包受到破壞, 也有可能是網路波動導致資料包的某些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

  3. 每傳送一個報文就啟動一個定時器嗎?

    為了保證可靠性,TCP增加了超時重傳機制, 使得每個未被確認(ACK)的報文在一定時間後可以被重新傳送

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

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

    每個已傳送但未確認資料包都會被放進佇列裡, 這個佇列持有一個單獨的計時器

    當第一個資料包進入佇列時,計時器啟動了

    如果計時器超時,佇列頭部的資料包會被重發,並且計時器重新計時

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

    佇列的所有資料都被確認了的話,就關閉定時器

  4. 超時時間怎麼設定呢?

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

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

    • 太短可能資料包尚未到達,此時發生重傳,浪費了資源

    往返時延 RTT (Round Trip Time)是配置RTO的一個重要指標, 但是由於網路間端到端的RTT並不是固定的,所以TCP採用了一種自適應的方法來計算RTT, 並且根據計算的值來配置RTO

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

    具體的細節可以參考RFC 6298The Basic Algorithm

  5. 一定要超時了才重傳嗎?

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

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

    1. 傳送方傳送了seq=1,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. 擁塞控制