TCP 學習筆記(三) 可靠傳輸

語言: CN / TW / HK

前言

讓我們來回憶一下TCP,TCP位於傳輸層(也有人稱之為運輸層),TCP提供可靠交付的服務,無差錯、不丟失,不重複,並且按序到達 ,這句話出自教科書。 其實這個不丟失我覺得可以理解為就算是有個資料包丟失的情況下,TCP提供的超時重傳也能保證你能收到完整的資料包。丟失的最樸素的場景就是資料包被確定要走哪一片光纜的時候,這片光纖被挖斷了,我大學的時候某個月,光纖就老是被挖斷,那被分配到這片光纜上的資料包就可以理解為丟失了。那丟失了怎麼辦呢,再重傳一次。但其實在網路這裡,重傳並不是一件簡單的事情,因為有的時候,資料包未必丟失,也可能是遲到,除此之外,還要考慮效率問題。

讓我們從最簡單的模型談起

全雙工的通訊的意思是,雙方既是傳送方也是接收方,但是為了討論問題方便,我們目前只考慮A傳送資料,而B接收,這裡是討論可靠傳輸的原理,所以不考慮 資料包在哪一個層次進行傳輸,因此把傳送的資料單元稱之為分組(傳輸層傳送的資料單元叫報文段,網路層傳輸的協議資料單元叫做IP資料包)。 讓我們先從最簡單的模型談起,也就是停止等待協議,停止等待的意思是,每傳送完一個分組就停止傳送,等待對方的確認。在收到確認之後再發送下一個分組。

在這種模型下有以下幾種情況:

  • 無差錯

這種情況最簡單,A傳送分組M1,傳送完就暫停傳送,等待B的確認。B收到了M1就向A傳送確認。A在收到了對M1的確認之後,就再發送下一個分組M2。

  • 出現差錯

分組在傳輸過程中出現差錯。B接收M1時檢測出了差錯,就丟棄了M1,其他什麼也不做(不通知A收到有差錯的分組,在可靠傳輸的協議中,也可以檢測出有差錯時傳送“否認報文”給對方。這樣做的好處是能夠讓傳送方及早知道出現差錯。不過由於這樣處理會使協議變得複雜,現在實用的可靠傳輸協議都不使用這種否認報文了)。也可能是M1在傳輸的過程中丟失了,這時B當然什麼都不知道。在這兩種情況下,B都不會發送任何資訊,一般的可靠傳輸協議是這麼設計的: A只要超過了一段時間仍然沒有收到確認,就認為剛才傳送的分組丟失了,因而重傳前面傳送過的分組。這叫超時重傳。要實現超時重傳,就要在每傳送完一個分組時設定一個超時計時器,如果在超時計時器到期之前收到了對方的確認,就撤銷已設定的超時計時器。

  • 確認丟失和確認遲到

B傳送的對M1的確認丟失了。A在設定的超時重傳時間內沒有收到確認,並無法知道是自己傳送的分組出錯、丟失,或者B傳送的確認丟失了。因此A的超時計時器丟失之後就會重傳M1。假定在這種情況下B就又收到了M1,對於B來說處理這個重傳的M1就要採取兩個行動:

  • 丟棄這個重複的分組M1,不向上層進行交付。
  • 向A傳送確認,不能認為已經發送過確認就不再發送。因為A之所以重傳M1,就表示沒有收到對M1的確認。

再有一種情況,B對M1的確認報文沒有丟失,而是遲到了,在這種情況下A可能就會重複收到確認。對重複確認的處理很簡單:收下後,就丟棄。B仍然會收到重複的M1,並且同樣丟棄重複的M1,並重傳確認分組。

通常A最終總是可以收到對所有發出的分組的確認。如果A不斷重傳分組但總是收不到確認,就說明通訊線路太差,不能進行通訊。想象一下,你的手機在弱網環境下,發出去的訊息會轉圈圈,如果轉了很長時間也沒發出去,微信就認為這條訊息沒發出去。

使用上述的確認和重傳機制,我們就可以在不可靠的傳輸網路上實現可靠的通訊。介紹這個模型主要是為了引出為了可靠傳輸會遇到哪些問題,這個模型的傳輸效率是非常低的,通道的利用率只有5.66%,這意味著通道在大多數時間內都是空閒的。

為了提高傳輸效率,傳送方採取了流水線傳輸,流水線傳輸就是傳送方可以連續傳送多個分組,不必每傳送一個分組就停頓下來,等待對方的確認。當使用流水線傳輸時,就引出了連續ARQ協議和滑動視窗協議。滑動視窗協議比ARQ協議複雜,我們這裡先介紹連續ARQ協議,再介紹滑動視窗協議。

連續ARQ協議

上面是一個典型TCP報文首部格式,我這裡不打算一一介紹TCP首部中各個欄位的意思,這裡只介紹一些我們本篇所需要的欄位:

  • 源埠和目的埠

各佔兩個位元組,分別寫入源埠號和目的埠號

  • 視窗

佔兩個位元組,視窗值是[0, 2^16^-1] 之間的整數,是指接收方允許對方傳送的資料量,之所以有這個限制,原因就在於接收方的資料快取空間是有限的。

視窗欄位明確指出了現在允許對方傳送的資料量。視窗值經常在動態的變化著。

  • 序號

佔4位元組。序號範圍為是[0,2^32^ -1], 共2^32^ 個序號,序號到達最大值之後,又回到0。在一個TCP連線中傳送的位元組流中每一個位元組都按順序編號,TCP報文首部的序號值則指的是本報文段所傳送的資料的第一個位元組的序號。

  • 確認號

佔4位元組,是期望收到對方下一個報文段的第一個資料位元組的序號。

TCP是雙工的協議,會話的雙方都可以同時接收、傳送資料。TCP會話的雙方都各自維護一個“傳送視窗”和一個“接收視窗”。其中各自的“接收視窗”大小取決於應用、系統、硬體的限制(TCP傳輸速率不能大於應用的資料處理速率)。各自的“傳送視窗”則要求取決於對端通告的“接收視窗”,要求相同

下圖表示的是傳送方維持的傳送視窗:

表示的意義是:位於傳送視窗內的5個分組都可以連續傳送出去,而不需要等待對方的確認。這樣,通道利用率就提高了。

連續ARQ協議規定,傳送方沒收到一個確認,就把傳送視窗向前滑動一個分組的位置。接收方一般都採用累積確認的方式。這就是說,接收方不必對收到的分組逐個傳送確認,而是在收到幾個分組之後,對按序到達的最後一個分組傳送確認,這就表示: 到這個分組為止的所有分組都已經正確收到了。

但是累積確認有優點也有缺點, 優點是:容易實現。缺點是不能向傳送方反映出接收方已經正確收到的所有分組的資訊。

舉一個例子: 傳送方傳送了五個分組,中間第三個分組丟失了,注意我們上面的確認方式是對按序到達的最後一個分組傳送確認,我們收到了1、2、4、5,就只能傳送對2的確認,於是傳送方就只好將3、4、5再重傳一次,我們將這種情況稱之為Go-back-N(回退N),表示需要再退回來重傳已經發送過的N個分組。可見當通訊線路質量不好時,連續ARQ協議會帶來負面的影響。對於這種協議TCP打上的補丁為選擇確認SACK,我們後面會講。

滑動視窗協議

下面我們討論TCP中的滑動視窗協議,為了討論問題,我們還是先假定資料傳輸只在一個方向進行,即A傳送資料,B接收資料,這個模型其實已經足夠說明滑動視窗協議了。

現假定A收到B發來的確認報文段,其中視窗是10位元組,而確認號是31(這表明B期望收到的下一個序號是32),而序號30為止的資料已經收到了),根據這兩個資料,A就構造出來自己的滑動視窗:

上面的傳送視窗表示:在沒收到B的確認的情況下,A可以連續把視窗內的資料都發送出去。凡是已經發送過的資料,在未收到確認之前都必須暫時保留,以便在超時重傳時使用。傳送視窗也可能變小,當對方通知的視窗縮小,但TCP的標準強烈不建議這樣做。因為很可能傳送方在收到這個通知以前已經發送了視窗的很多資料,現在又要收縮視窗,不讓傳送這些資料,這樣就會產生一些錯誤。

現在假定A傳送了序號為31-35的資料,這時傳送視窗位置並未發生改變,但傳送視窗內靠後面有5個位元組表示已傳送但未收到確認。而傳送視窗靠前面的五個位元組(36-40)是表示允許傳送但尚未傳送的。

從以上所述可以看出,要描述一個傳送視窗的狀態需要是三個指標: P1,P2和P3:

小於P1的是已傳送並已收到確認的部分,而大於P3的是不允許傳送的部分。P3-P1=A的傳送視窗,P2-P1 = 已經發送但尚未收到確認的位元組數。P3-P2=允許傳送但當前尚未傳送的位元組數。

再看下B的接收視窗。B的接收視窗大小是10.在接收視窗外面,到30號位置的資料是已經發送過確認。因此B可以不再保留這些資料,接收視窗內的序號(31-40)是允許接收的。在下面的圖中,B收到了序號為32-33的資料。這些資料沒有按序到達,因為序號為31的資料沒有收到(也許丟失了,也許滯留在網路中的某處),因為31沒收到,所以B傳送的確認報文段中的確認號仍然是31(即期望收到的序號),而不能是32或33.

現在假定B收到了A重傳的31-33的資料,並把序號31-33的交付給應用層的程式,然後B刪除這些資料。接著把接收視窗向前移動3個序號,同時給A傳送確認,其中視窗值仍然是10,但確認號是34。這表明B已經收到了到序號33為止的資料。

收到確認之後,A的可用滑動視窗變大,這個時候如果A傳送完序號36-43的資料,P2會和P3重合。傳送視窗內的序號已經用完,但還沒有再收到確認。A的傳送視窗已滿,就不能再發送資料,必須停止傳送資料。此時存在兩種情況:

  • 資料包還在路上,未到達B
  • B已經給A傳送了確認,但是還在路上。

為了保證可靠傳輸,A經過一段時間之後(在超時計時器到期之後),就會重傳這部分資料,重新設定超時計時器,直到收到B的確認位置。如果A收到確認號落在傳送視窗內,那麼A就可以傳送視窗繼續向前滑動,併發送新的資料。

選擇確認-SACK

上面的論述中還有一個問題,就是B遺失了中間序號的資料包,會請求A再重傳,但是在上面的討論中視窗值較小,影響還不是很大,但是如果視窗值很大,遺失了中間的一個包,這樣有的時候可能導致網路擁堵,對於這種情況,TCP提出了選擇確認。我們舉一個例子來說明選擇確認的原理.

假設A收到了以下三個位元組流:

從圖中我們可以看到缺少了1001-1500、3001-3500,一個直白的思路就是,收到這三個位元組流的時候,算出缺少的區間,再請求回傳的時候帶上這些區間。我們上面畫出了TCP首部,目前看來沒有哪個欄位能夠提供邊界資訊。RFC 2018對此做了規定,如果要使用選擇確認SACK,那麼在建立TCP連線時候,就必須在TCP的首部的選項中新增上“允許SACK”的選項,而雙方必須事先商定好。

超時時間的選擇

上面我們提到了超時重傳,這裡來進行詳盡的討論,設定固定的超時時間是完全不可取的,因為不同地區的網路質量不同,所以這個超時時間應當是自適應的,這也是TCP的思路,TCP採用了一種自適應演算法,記錄一個報文段發出的時間,以及收到相應的確認的時間。這兩個時間之差就是報文段的往返時間RTT。TCP保留了RTT的一個加權平均往返時間RTTs(這又被稱為平滑的往返時間,S表示Smoothed。因為進行的是加權平均,因此得出的結果更加平滑)。

每當第一次測量到RTT樣本時,RTTs值就取為所測量到的RTT樣本值。但以後每測量到一個新的RTT樣本,就按下面的共識重新計算一次RTTs:

新的RTTs = (1 - α) × (舊的RTTs)+ α × (新的RTT樣本)

在上式中,0 ≤ α ≤ 1. 若α很接近0,則表示新的RTTs值和舊的差距不大,若α接近於1,則表示新的RTTs受新的RTT樣本影響較大。RFC 6298推薦α值為1/8。

顯然超時計時器設定的超時重傳時間RTO(Retransmission Time-Out)應當略大於上面得出的加權平均往返時間RTTs。RFC6298推薦使用下面的算式計算RTO:

RTO = RTTs + 4 × RTT~D~。而RTT~D~是RTT的偏差加權平均值,它與RTTs和新的RTT樣本之差有關。RFC6298建議這樣計算RTT~D~。當第一次測量時,RTT~D~值取為測量到的RTT樣本值的一般。在以後的測量中,則使用下式計算加權平均的RTT~D~:

新的RTT~D~ = (1 - β) × (舊的RTT~D~) + β × |RTTs - 新的RTT樣本|。

這個β是個小於1的係數,它的推薦值是1/4, 即0.25。

上面說的往返時間測量,事實上實現起來相當的複雜,我們這裡舉一個例子:傳送一個報文段,設定的重傳時間到了,還沒有收到確認。於是重傳報文段,過了一段時間後,收到了確認報文段。現在的問題是: 如何判定此確認報文段是對先發送的報文的確認,還是對後來重傳的報文段的確認? 由於重傳的報文段和原來的報文段完全一樣,因此源主機在收到確認後,就無法作出正確的判斷,而正確的判斷對確定加權RTTs的值關係很大。

若將收到的確認當作是對重傳報文段的確認, 但卻被源主機當成是對原來的報文段的確認,則這樣計算出的RTTs和超時重傳時間RTO就會偏大。後面再發送的報文段又是經過重傳之後才收到確認報文段,則按此方法得出的超時重傳時間RTO就會越來越長。

同樣,若收到的確認是對原來報文段的確認,但被當成是對重傳報文段的確認。則由此計算出來的RTTs和RTO都會偏小。這就必然導致報文段過多地重傳,這樣就會導致報文會被過多重傳。

根據以上所述,Karn提出了一個演算法: 在計算加權平均RTTs時,只要報文重傳,就不採用其往返時間樣本。這樣得出的加權平均RTTS和RTO就比較準確。

但是這個演算法還遺漏了一點,就是如果網路時延突然增大了很多。因此在原來得出的重傳時間內,不會收到確認報文段。於是重傳報文段,但根據Karn的演算法,不考慮重傳的報文段的往返時間樣本,這樣,超時重傳時間就無法更新。

因此我們需要為Karn的演算法打上一個補丁: 報文段每重傳一次,就把超時重傳時間RTO增大一些。典型的做法是取新的重傳時間為舊的重傳時間的2倍。當不發生報文段的重傳時,才根據上面給出的:RTO = RTTs + 4 × RTT~D~ 計算超時時間。實踐證明,這種策略是比較合理的。

總結一下

TCP的可靠傳輸依賴於滑動視窗和超時重傳,我們從最簡單的停止等待協議談起,為了確認接收方確實收到了分組,接收方需要傳送確認給傳送方,停止等待協議對通道的利用率比較低,我們不大可能採用這種演算法來做,TCP採用了滑動視窗來提升通道的利用率,為了防止分組在途中丟失或者出了差錯,TCP引入了超時重傳,超時重傳時間的計算是一個自適應演算法。本篇基本上取材於《計算機網路(第7版)》的TCP一節,用自己的方式梳理了一下。

參考資料