計算機網絡——TCP的流水線傳輸(超詳細)


一、前言

  前兩天看完了《計算機網絡——自頂向下方法》這本書的運輸層部分,看完后發現TCP協議太過復雜,所以想寫一下TCP的系列博客來加深印象,而這是其中的第三篇。這一篇博客就來談一談流水線傳輸的實現原理,以及TCP是如何實現流水線傳輸的。


二、正文

 2.1 什么是流水線傳輸以及為什么需要它

  在談流水線傳輸之前,我們先來說一說不用流水線傳輸是什么情況。假設客戶端和服務器建立了一條TCP連接,同時客戶端需要向服務器發送一個大文件,此時若TCP沒有實現流水線傳輸,則客戶端將如何向服務器發送數據呢?由於網絡對數據報長度的限制,TCP會將文件分割成很多的數據段,然后從前往后逐一發送,先發送第一個TCP數據段,當接收到服務器的確認報文后,再發送第二段,以此類推。這樣有什么問題?我們發現,客戶端在等待數據段在網絡中傳輸,並接收到ACK報文的這段時間,完全是沒有工作的,這是一種極大的資源浪費。假設一個數據段的大小是10KB,而客戶端將數據送入網絡的速率是10M/s,而數據的往返時間(RTT)是1s,由於每次只能傳輸一個數據段,所以在1s內,只傳輸了10KB的數據,盡管理論傳輸速率達到了10M/s,卻由於這種逐個傳輸的機制,導致大部分都沒有利用上。而且正是因為這種機制,導致要完整的傳輸完一份數據的時間大大增加。

  正是為了解決上面所述的問題,才有了流水線傳輸。流水線傳輸的原理很簡單:連續發送多個數據段,而不需要先等待之前發送的數據段被確認。第一段說的情況中,最大的浪費就是等待ACK到達的這段時間,客戶端什么事情都干不了,而流水線就是為了解決這個問題。同樣是一份文件,被分成了多個數據段,假設客戶端連續發送3個數據段,被服務器確認接收后,再發送3個,以此類推。因為合理利用了等待的時間,這樣相比於之前,速度將提升3倍。這就是流水線傳輸,而每次發送多少個數據段,將由具體的實現決定以及網絡擁塞情況動態決定。


 2.2 流水線傳輸將要面臨的問題

  流水線傳輸和逐個傳輸有很大的區別,而其中一個非常關鍵的區別就是:數據段可能不會按順序到達。在使用逐個傳輸的機制時,我們完全不需要考慮這個問題,因為每次都只發送一個數據段,等這個發送成功時,再發送下一個,所以數據段一定是按順序到達的。但是流水線不同,流水線機制中,發送發一次將會發送多個數據,而由於網絡的不確定性,這多個數據可能不是通過一條路徑到達目的地,所以完全有可能出現靠后的數據段先到達目的地的可能。

  為了接收方在接收到多個數據段后,能夠將它們重新按照順序組合起來,TCP為每一個數據段都進行了編號(TCP報文結構中有一個32位的序號字段),通過這個序號,接收方就可以知道當前收到的數據段是屬於完整數據的哪一部分了。假設報文段是按照0,1,2,3,4,5......逐一編號的,雖然具體情況並非如此,但是為了方便我們討論流水線傳輸,我們暫且這么認為。接下來我們就來討論流水線傳輸的兩種理論實現方式——回退N步選擇重傳


 2.2 回退N步(GBN)

  回退N步協議簡稱為GBN,是流水線傳輸的一種理論實現方式。在GBN中,將維護一個大小為N的窗口來限制數據的發送,這個窗口其實就是一個區間,而序號落在這個區間內的報文段,都可以不需要等待之前的報文段被確認而直接發送。

  在GBN中,我們可以將所有的報文段序號划分為四個區間:

  1. 已經發送並接收到ACK報文的報文段;
  2. 已經發送但是還沒有接收到ACK報文的報文段;
  3. 允許被發送,但是還沒有發送的報文段;
  4. 還不能發送的報文段;

  這里我們定義幾個變量,方便我們解釋后面的內容。假設我們將當前第一個被發送,但是還沒有接收到ACK的報文段的序號定義為base,而下一個需要被發送的報文段的序號被定義為nextseqnum,則對於上面的四部分,每一部分都對應一個區間。第一部分的序號將落在[0, base - 1]中,第二部分的序號將落在[base, nextseqnum - 1],第三部分將落在[nextseqnum, base + N - 1](N是窗口的長度),最后大於base+N的就是第四部分。而第二和第三部分共同組成了長度為N的窗口。如下圖所示:

  在上圖中的情況下,發送方還允許發送6個報文段,即屬於第三部分的報文段。而為什么需要設置窗口,而不是無限制發送呢?當然是為了不讓網絡太過擁擠,造成數據發送緩慢甚至丟失的情況。下面我們根據發送方處理各種事件的方式,來進一步了解GBN

  • 事件一:上層調用接口,向網絡中傳輸數據。此時,發送方將檢查窗口中是否有剩余的空間,即窗口中是否包含序號屬於第三個區間的報文段。若窗口有空閑,則將數據發出,同時讓nextseqnum + 1。若此時窗口中的所有數據段都是已經發送但還沒有接收到ACK的數據段,則發送方可以將數據返回給上層,隱式地告訴上層窗口已滿,而上層可能會過一段時間再試;或者也可以將上層傳遞的數據放入緩存,等窗口有空閑區間之后再將數據發送;或者使用同步機制,僅當窗口有空閑時,才允許上層調用發送數據的接口。在實際實現中一般使用第二種。
  • 事件二:接收到一個ACK報文。在GBN中,使用的是累計確認機制(累計確認指的是當接收到n號報文段的ACK報文,表示0 - n號報文段都已經被成功接收),所以當收到一個ACK報文時,窗口將向前移動,直到base的值等於這個ACK報文所確認的下一個報文的序號為止。而此時,又將有更大的序號進入到窗口中,窗口中將產生空閑區間。
  • 事件三:發生超時事件。在GBN中,只有一個計時器,而它記錄的是序號為base的報文是否超時,當這個報文段被發出后,超過了指定時間還沒有收到ACK報文(表明大概率已經丟失),則發送方將重傳所有已經發送但還沒有接收到ACK報文的報文段,也就是上面圖片中的第二部分。這就是回退N步這個名字的由來。當此事件發生,計時器將被重新啟動。

  這里在補充一下發送方的計時器,當接收到一個ACK后,若還存在已經發送但是沒有收到ACK的報文段,計時器將重新啟動;若不存在,則停止,並等到有數據發出后再啟動。說完了發送方,我們再來說說接收方對於各種事件是如何處理的:假設接收方已經成功接收到了報文段0到n-1,此時收到報文n,這是一條按順序到達的報文,所以接收方直接將數據段交付給上層;但是若收到的不是n,而是n+1,甚至更大,則接收方會將它丟棄,並向發送方回送n-1ACK報文,表示它目前成功接收到的最大的報文段是n-1(被丟棄的不算成功接收),而發送方在超時后,將重傳n,n+1......這就是GBN累計確認的實現原理,接收方每次確認的都是當前已經接收,並且按序到達的最大值。所以對於接收方來說,只需要維護一個變量,即當前期望獲得的報文段的序號expectedSeqNum,在上面的例子中expectedSeqNum == n

  這里就有一個問題,為什么接收方接收到亂序的報文后,不可以先將它緩存下來,等序號更小的報文段到達后,再一同傳遞給上層?那是因為沒有太大的必要性。就拿上面的例子來說,假設報文n丟失,發送方將重傳n,n+1,n+2等所有已經發送但還沒有接收到ACK的報文,所以接收方就沒有必要再緩存之前亂序到達的報文段了。而對於亂序到達的報文,有不小的幾率是前面的報文已經丟失。尤其是亂序到達的越多,之前的那些報文丟失的幾率越大。


 2.3 選擇重傳(SR)

  GBN最大的一個缺陷就是,在發生丟包事件時,可能出現很大程度的無用功。比如說發送方同時發送出了10個報文段,但是第一個報文段丟失,結果將導致這10個報文段都要重傳,就算后面9個報文段成功到達,也會被接收方丟棄。而選擇重傳機制解決了這個問題,因為它將有選擇地重傳報文段。

  SRGBN類似,它也維護一個大小為N的窗口,只有序號落在窗口中的報文段才允許被發送。但是和GBN不同,SR的窗口中報文段不會被按順序確認,后發出的報文也可以被先確認。和上面類似,我們假設窗口的第一個報文段的序號,也就是最先被發出但還沒收到ACK的報文段的序號為sned_base,而下一個允許發送的報文段的序號為nextseqnum。和GBN不同,SR的接收方不管報文段是否按順序到達,都會接收並發送相應的ACK報文,所以對於發送方的窗口,將充斥着三類報文段:

  1. 已經發出,但是還沒接收到ACK的報文段;
  2. 已經發出,並成功接收到ACK的報文段;
  3. 可以發送,但是還沒有發送的報文段;

  所以SR發送端的報文段的分布如下所示:

  和GBN一樣,我們根據SR發送方要處理的事件,來更進一步的了解它:

  • 事件一:上層調用數據傳輸接口,請求發送數據。此時,發送端將檢查窗口中是否還有空閑空間,即窗口中是否還存在可用,但是還沒有使用的序號。若存在,則使用nextseqnum指向的序號封裝報文段並發送。同時nextseqnum指針向前移動,直到沒有空間或者數據發送完畢為止;若不存在空閑序號,則發送方可以將數據返回給上層,隱式地告知上層當前發送接口不可用,可以稍后再試,或者也可以將數據緩存,當窗口有空閑時再發送,或者使用同步機制。
  • 事件二:接收到某個報文段的ACK報文。由於在SR中,接收方發送的ACK報文就是對接收到的報文進行確認,不論是否有序,也就是非累計確認,所以當發送方接收到ACK報文時,將有三種情況。(1)接收到一條已經被確認的報文段的重復ACK(一般是由超時引起),則忽略它;(2)收到一條序號不是send_base的報文段的ACK,則標記這個序號的報文已經被確認;(3)收到序號為send_base的報文的ACK報文,此時send_base指針向前移動,直到移動到第一個已經發送但是還沒有被確認的報文序號為止。比如上圖,接收到了send_basesend_base+1ACK報文,則此時send_base將更新為send_base+4
  • 事件三:報文超時。由於在SR中,報文可以無序到達,所以在理論實現中,需要為每一個已經發送的報文段都綁定一個計時器,來記錄此報文是否超時。若某個已經發送的報文段的計時器超時,發送方將重傳對應的報文,注意,和GBN不同,SR只重傳超時的那條報文。

  說完了發送方,再來說一說接收方。在SR機制中,發送方若接收到一個亂序報文段,不會將其丟失,而是放入到接收緩存中,然后等待序號更小的報文段都到達后,再取出交付給上層。所以對於接收方來說,報文序號分為四類:

  1. 期待接收到的報文段,即下一條想要的報文段;
  2. 已經接收,但不是按順序到達報文段;
  3. 可以接收的報文段;
  4. 暫時還不能接收的報文段;

  而接收方也是通過維護一個大小為N的窗口來管理這些報文段。同時,在接收方維護了一個變量rcv_base,表示窗口的第一個報文段的序號,其實也就是下一條期待接收的報文。至於為什么接收方也需要維護一個窗口限制接收,是因為接收緩存的大小有限,假設期待接收到的報文一直沒有到達,到達的一直都是亂序的報文,此時接收方要將它們放入接收緩存,但是由於接收緩存大小有限,所以不能無限制地接收,只能通過窗口來限制可以接收的范圍。下面就是接收窗口的模型圖:

  對於接收方來說,若接收到一條報文,將會分為四種情況進行處理:

  1. 接收到的報文段的序號為rcv_base。此時rcv_base將向前移動,直到到達第一個沒有被接收的序號為止,這樣意味着窗口在向前移動,並騰出了新的空閑區間;
  2. 接收到的報文序號在窗口中,但不是rcv_base。此時,若這個報文之前已經接收過,則直接發送此報文對應的ACK報文,但是不接收;若沒有接收過,則將此報文放入接收緩存中,再發送ACK報文;
  3. 接收到的報文序號小於rcv_base,則表示這條報文已經被接收過,直接發送一個ACK報文,但是不接收此報文段;
  4. 除上述外,其他報文段都忽略;

  這里再討論一下為什么接收方會接收到已經接收過的報文段。原因就是ACK報文超時到達接收方,或者ACK報文丟失,所以導致了發送方對報文段進行重傳。而此時,為了讓發送方知道自己已經成功接收,接收方需要再次發送此報文段的ACK報文,而不是忽略它。


 2.4 TCP的流水線傳輸

  上面兩種流水線傳輸機制是兩種理論模型,而實際的TCP流水線傳輸,是對這兩種模型的結合和改進,抽取了各自好的部分,彌補對方不足的部分。在TCP的流水線傳輸機制中,和GBN一樣使用的是累計確認,同時只使用一個計時器,用來記錄窗口中的第一個報文段是否超時,同時只重傳丟失的分組;對於接收方,接收到不按照順序到達的報文段,不會丟棄,而是放入接收緩存。下面我就來說一說TCP的流水線傳輸是如何實現的。

  對於發送方,TCP的流水線傳輸與GBN有着高度的相似性。同樣一個大小為N的窗口,同樣一個base和一個nextseqnum變量,同樣的序號划分,也就是如下圖所示的模型:

  同樣的累計確認,同樣的序號划分,同樣的單一計時器,所以發送端的實現幾乎一致,除了最重大的一個地方。GBN最大的缺陷就是當超時事件發生時,會進行大量不必要的重傳,而TCP的流水線傳輸避免了這個問題。這里的計時器記錄的是序號為base的數據段是否超時,若發生超時事件時,發送方只會重傳序號為base的報文段,而不會重傳后續的這些已經發送但還沒有接收到ACK的報文段。而且除此之外,這里還使用到了一個叫快速重傳的機制,提高效率。除了重傳,TCP流水線傳輸的發送方基本上與GBN相同。

  下面再來說一說接收方。在TCP傳輸的接收方,也會維護一個和SR類似的接收窗口,來限制數據的接收。也就是和SR一樣的模型圖:

  但是實際的接收方式,相對於SR來說,有了很大的改變。在接收方,若接收到一條報文,將產生以下幾種情況:

  1. 接收到按序到達的報文,也就是序號為rcv_base的報文,則rcv_base向前移動,也就是窗口向前移動,直到移動到第一個沒有接收到的序號為止,同時對新的rcv_base的前一條報文進行確認。比如上面這張圖,接收到rcv_base后,窗口將移動到rcv_base+4,而確認報文將是對rcv_base+3的確認,表示已經接收到rcv_base+3及其之前全部報文;
  2. 接收到的報文序號不是rcv_base,但是依舊落在窗口內,則發送確認報文,但是是對rcv_base - 1這個報文的確認,表示現在接收方已經接收到的按序的報文中,最大值是rcv_base - 1,下一條需要的是rcv_base 。同時,再判斷這條報文是否已經有緩存,若沒有,則加入到接收緩存中;

  由於TCP的流水線傳輸使用的是累計確認,所以沒有必要每接收到一條報文,都進行一次確認,而是有選擇地進行確認,這樣可以減少傳輸ACK報文的數量,節省網絡資源。而在TCP規范中,對於何時傳輸ACK,給出了幾條建議:

  1. 接收到序號為rcv_base的報文段時,若這之前的報文都已經確認過了,則延遲ACK報文的發送,等待另一個報文段的到達(最多等待500毫秒),若在等待的過程中,另一條按順序的報文段到達,則直接發送那條報文的確認,同時確認了兩條報文;若這段時間內無有序報文到達,則發送當前報文的確認;
  2. 接收到序號為rcv_base的報文段時,若在這之前還有沒被確認的報文,比如說前一條報文在等待當前報文的到達(情況一),則立即發送一個ACK,確認這兩條報文;
  3. 接收到一個序號比rcv_base大的報文段,表示出現了亂序,此時立即發送一個ACK報文,告知發送方下一條需要的是rcv_base
  4. 若接收到的報文段序號是rcv_base,同時之前已經接收了一些亂序的報文段,則立即發送ACK報文;

  上面四種建議中,1-2條是為了更少地發送ACK報文,而3-4條則是快速重傳機制的依據(尤其是第三條)。這種實現,也有另外一種叫法,叫做選擇確認


 2.5 窗口大小的限制

  在流水線傳輸中,需要注意一個問題,那就是窗口不能太大,否則將會出現問題。序號不是無限大的,比如在實際實現中,序號是一個32位的int整數,所以當序號超過最大值時,將對最大值取模,重新獲取一個較小的值作為序號。也就是說,序號范圍實際可以看成一個環,而在這個環中,有兩個窗口,一個是接收窗口,而一個是發送窗口。看下面一種情況,假設序號只有0,1,2,3這四個,窗口大小N == 3

  • 初始時刻,發送方的窗口是0,1,2,而接收方的窗口也是0,1,2

  • 發送方發送報文0,1,2,到達接收方后,接收方的發送確認報文,同時窗口移動,變為3,0,1

  • 確認報文沒有按時到達發送方,於是發送方重傳報文段0

  • 接收方接收到后,檢查自己的窗口,發現序號0在窗口中,而且並未接收,於是將此報文當作一個新的報文段接收;

  上面發生了什么?由於窗口太大,出現了序號重疊的現象,也就是在這個序號環中,接收窗口的頭,觸碰到了發送窗口的尾。為了避免這個問題,窗口長度必須小於等於總區間長度的一半。因為發送窗口的和接收窗口的一定是重合的,而為了讓發送窗口的不碰到接收窗口的,必須使它們兩個的長度之和小於等於總區間長度


三、總結

  長篇大論快給我寫吐了,就不多說了。總之,希望這篇博客對看到的人有所幫助,若是發現錯誤的部分,也希望能夠提出來。


四、參考

  • 《計算機網絡——自頂向下方法(原書第七版)》


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM