《TCP/IP協議 詳解》思考總結 · TCP上篇

前言

開始這篇文章之前,我非常的緊張,因爲要寫好這個TCP協議說實話並不簡單。作爲TCP/IP協議簇最爲核心的部分,《TCP/IP協議 卷一》花了整整八章的篇幅去介紹它。如何在保證正確的前提之下,合理有序的寫出一些有意義的內容,這是一個很大的挑戰。

整個看書學習的過程,實際也是一種享受。在瞭解TCP的各項策略時,你可以通過書本瞭解到前輩設計時的所思所想。如何在無連接不可靠的IP網絡上實現一個可靠有連接的傳輸;如何根據已有的信息去推測診斷當前的網絡環境;如何充分利用當前的資源以最高效的方式傳輸;如何動態的感知網絡的波動。相信你在瞭解之後也一定會和我一樣忍不住拍手稱爲。

本文介紹TCP,依然是從三次握手和四次揮手開始;之後介紹了兩種不同情況下TCP的傳輸策略;在文章的結尾我們簡單說了帶寬時延乘積,這是一個非常重要的概念,理解它之後纔會明白擁塞發生的情形,以及我們是如何把數據報的傳遞抽象成流。雖然是熟悉的概念,但文章盡力從一些網上其他文章沒有提到的角度來分析這些問題,希望能夠給你一些新的啓示。

在計算機網絡的學習過程中,概念和參數並不是最重要的。如何去理解一個協議,如何從現有的工具裏去觀察一個協議從而分析問題,如何去借助文檔回答問題,這些能力纔是我們真正應該重視的。

再談三次握手和四次揮手

在其他介紹三次握手的文章裏,經常會從類似如下的示意圖開始

三次握手流程

圖上的內容非常的簡單,但是實際的握手過程遠比這個複雜。首先,我們要考慮的就是爲什麼TCP的建立需要三次握手。這個問題我們在這個系列的文章第一篇就進行了討論,當時給出的結論是:

TCP需要在不可靠的信道上進行可靠的傳輸,那麼必須要在通訊之前就某些問題達成一致。一條消息如果需要被單方面確認,需要一次單向握手,那麼雙方同時就某個問題達成一致就需要兩次單向握手。

序號 方向 具體操作
1) A --> B Send A’s SYN
2) A <-- B ACK A’s SYN
3) A <-- B Send B’s SYN
4) A --> B ACK B’s SYN

其中2 , 3兩步可以合併成一條信息,這就是三次握手的由來。

但這裏仍然留有幾個模糊不清的點。

1.參考上面的流程圖,整個握手結束之後實際只有B可以確認消息雙方都已經得到,而A無法確定最後一條ACK B’s SYN是否送達。

這是計算機網絡中一個非常有名的思想實驗:兩軍問題。實際上百度可以得到非常多的資料,但是各類博客抄來抄去,一些好的文章原作者已經不可考,所以這裏我再簡單做一下闡述。

兩支軍隊(我們暫時稱爲A1和A2)預備從兩邊去攻打低窪的一座城池(暫稱爲B)。他們的力量對比是

  1. B < A1 + A1
  2. B > A1
  3. B > A2

A1和A2必須要約定好在同一個時間發起攻擊,纔可以獲勝,單獨行動都會被B消滅。但是A1和A2通信必須要經過B的城池,這也就意味着通信兵可能會被截獲。如何設計一個通信的方案可以使得A1和A2必勝呢?

兩軍問題本質上和我們TCP遇到的情況一樣:在不可靠的信道上試圖就某些信息達成一致。目前的方案是每當發送一份消息,必須要返回一份回執來告知發送方消息已送達。

引出的一個問題就是發送回執的一方如何確認自己的回執被送達了呢?再返回一條回執來確認自己收到了對方的這條回執?所謂子子孫孫無窮盡也,大抵就是如此。這意味着不可靠的信道上最後一條被髮送的消息永遠都是無法被雙方同時確認的。兩軍問題本質上是無解的。

還需要強調的是,可靠性並不會因爲握手次數的增加而提高。三次握手是可靠性和效率兩者平衡妥協的結果。最後一次通信的發送方必須要承擔行動的風險。

除了連接建立時,在一端(我們假設爲客戶端)發起主動關閉時,也會遇到同樣的問題。四次揮手的過程如下。

四次揮手流程

發起主動關閉的一方在發送最後一個FIN ACK之後會在TIME_WAIT狀態停留2MSL的時間。這樣做的目的其一就是爲了實現全雙工的TCP連接的可靠終止。

MSL是Maximum Segment Lifetime,指代任何報文段被丟棄前在網絡內的最長時間

因爲最後一條FIN包的ACK發出以後客戶端是無法確認對端服務器一定收到的,如果客戶端發送完FIN ACK之後認爲已經結束關閉了這個連接,但實際FIN ACK又未送達,這時服務端重新發送了一個FIN包給客戶端,會得到一條RST的響應,這會被服務器解析成一種錯誤,而實際客戶端是正常關閉的。

爲此客戶端必須要維護狀態信息2MSL的時間,並在結束時按照最後一條FIN ACK丟失的情況處理,重新發送一次FIN ACK。

TIME_WAIT另一個目的是允許之前連接的報文在網絡中消逝。熟悉 socket編程的朋友應該知道可以通過四元組(目的端IP地址和端口,源端IP地址和端口)來確定唯一的一條TCP連接。但是如果關閉了一個TCP連接之後,在相同四元組之上重新打開一個TCP連接,後一個連接會被認爲是之前連接的化身。

這裏的翻譯比較讓人困惑,在RFC 793中是這樣描述的 : New instances of a connection will be referred to as incarnations of the connection.
後面我們會多次提到化身,指的是同一個四元組上新創建的連接。

存在一種可能是某個連接之前的重複分組在該連接終止後再現,如果此時創建了新的化身,很可能會帶來誤解。

爲此TCP拒絕爲處於TIME_WAIT的端口創建新的化身。TIME_WAIT的時間是2MSL,這保證了無論是哪個方向上的報文(存活時間MSL),還是另一端的應答(發送的報文最長存活MSL,返回的應答最多存活2MSL)都會在TIME_WAIT期間消逝。

實際這個規則存在例外,我們將在後面遇到這種情況

2. 在三次握手的過程中,雙方嘗試在哪些方法達成約定?

爲了研究這個問題,我們可以打開Wireshark,找一個處於三次握手的TCP連接。下圖是我任意找的一個報文。

wireshark觀察的三次握手
有關SYN FIN的介紹非常多,本文不再介紹。如果你不太熟悉,沒有必要一一去硬背下來,瞭解這幾個名稱實際指代的單詞會有助於理解和記憶。

1. SYN = synchronization 同步。正如我們上文介紹所說,TCP連接的建立必須要就某些問題達成約定,也就是同步信息
2. PSH = push 發送端通知接收端不要因爲等待額外數據而讓已送達的數據在緩衝區滯留。類似flush()
3. FIN = finish 也就是我們所說的四次揮手。結束的含義
4. ACK = acknowledge 確認報文。你可以簡單認爲是回執,具體是確認哪一部分的數據,需要結合sequence number

這是一個非常典型的三次握手,上文所說的握手報文合併也可以在報文 489看出。注意圖中紅框標示的信息。

  • 49817 - > 443源端口 -> 目的端口

細心的朋友應該看出443端口是爲https服務指定的,實際也確實如此,下一個未展示的報文就是Client Hello

  • SYN標示指明瞭這是一個發起連接請求的報文。
  • Seq就是我們馬上將要介紹的重點Sequence Number
  • Win是Window的意思,這一個字段我們會在後續仔細介紹
  • Len是length,用以指明TCP數據部分的長度。注意這個長度並不包含報文首部,所以在SYN包中Len是0
  • WS是表明發起端192.168.199.170可以處理Selective AcknowledgementsTSvalTSecr是時間戳選項相關的內容。這三個參數本文不做介紹,有興趣的朋友可以自行查閱。what is 'WS' 'TSval' and 'SACK_PERM' mean in packet info columns???

我們需要關心的是Seq字段。

在連接建立的過程中客戶端和服務器會互相通告自己的ISN,也就是SYN包中我們看到的Seq字段的值。需要注意的是只有在SYN包中Seq字段纔是發送端的ISN

ISN = Initial Sequence Number

Seq是序號的意思,它可以描述當前發送的數據報中的數據相對於整體數據開始位置的偏移量,單位是字節。與之類似的是ACK數據報中的Ack字段,它是爲了通告對端已經接收的數據相對於整體數據開始位置的偏移量(也可以理解爲對端期待接收的數據相對於整體數據開始位置的偏移量)。

下圖描述的是一個最簡單的TCP連接和中斷的過程。

TCP連接和中斷的過程

首先報文段1通告了srv的ISN也就是圖中的1415531521。之後報文段2通告了bsdi的ISN也就是1823083521,但是它多了一條ack,注意它的數值是1415531522 = 1415531521 + 1!表明bsdi已經確認收到了srv的SYN包。

SYNFIN包會讓Seq加一,你可以簡單認爲是一個長度爲1的數據報。

注意報文段4,srv的Seq被設置成1415531522。因爲對端已經ack了SYN包,也就意味着我們發送的數據應該從這之後開始。

但是我們需要注意,包括上面我們截圖的三條報文,打開Wireshark你去觀察任一一個SYN包,裏面的Seq字段永遠都是0,而不是我們流程圖裏那一長串的數字。這是因爲Wireshark展示的SeqAck字段全部都是相對數值也就是Seq/Ack - ISN。

Wireshark裏的SYN/ACK

我們必須要思考的一個問題就是,ISN是如何選擇的?如果僅僅是爲了標記收發數據的偏移量,我們完全可以默認從0開始計算而不必加上ISN。這似乎更加簡單。

RFC 793中有關這部分做了討論,它首先提出了一個問題: how does the TCP identify duplicate segments from previous incarnations of the connection? 比如在一個連接(四元組不變)上短時間內快速的重複打開關閉,或者一個連接因爲內存不足而斷開繼而復位。連接的化身很可能會接收到之前連接存留在網絡內的數據報。

我們上文介紹的TIME_WAIT設計的初衷,部分就是爲了避免這種混淆。但是這並不保險。爲了解決這個問題,TCP選擇初始一個ISN,並在此基礎之上累加,從而讓連接的化身能夠正確分辨數據報。

socket編程中,我們可以指定SO_REUSEADDR選項讓處於TIME_WAIT狀態的端口可用
主機或者TCP模塊的崩潰也會遺失狀態的記錄。

ISN的生成器實質上是一個32比特的計數器,每隔一定的時間加1(通常是4ms,但不同系統實現不一樣)。選擇這樣的生成方式是爲了考慮到一種更極端的情況: even if a TCP crashes and loses all knowledge of the sequence numbers it has been using。ISN的生成器實質是和TCP模塊互相獨立的。

ISN的範圍是0 ~ 2 ^ 32 - 1,達到最大之後ISN會環回到0開始。在4ms加1這種實現的系統裏,大約需要4.55小時ISN環回一遍。這個時間是遠遠大於TIME_WAIT的,所以不必擔心TIME_WAIT期間ISN發生迴繞從而重複。

上文我們說過TIME_WAIT有一個特例:在源自Berkeley的實現當中,如果到達的ISN大於之前連接的結束序列號,那麼Berkeley的實現是允許當前處於TIME_WAIT的端口複用。簡單來看這麼做是沒有問題的,因爲FIN包中的Seq一定是當前連接最大的Sequence Number。如果新連接的ISN大於這個Seq那麼顯而易見,這個SYN包肯定不屬於之前連接的。

但是問題出在ISN的選擇是環回的!當Sequence Number達到最大也就是2^32 - 1時會環回到0重新開始。假設之前連接的通訊過程中Sequence Number發生了環回,我們上文的結論也就不成立了。所以這種特例是存在陷阱的。

通常在一個高速通道上Sequence Number非常容易發生環回,造成的問題不僅僅是我們提到TIME_WAIT,中間超時重傳的包也可能會讓對端造成錯誤的理解。

使用窗口擴大選項的TCP連接,最大的窗口接近2 ^ 30!這意味着按照最大窗口發送,第五個數據報Seq就會發生環回。舉一個簡單的例子,假設我們需要傳輸6G的數據。

序號 方向 數據
1. A —> B Seq 0G : 1G
2. A —> B Seq 1G : 2G
3. A —> B Seq 2G : 3G
4. A —> B Seq 3G : 4G
5. A —> B Seq 0G : 1G
6. A —> B Seq 1G : 2G

假如第二個數據報發生丟失,在發送第六個數據報發生重傳,那麼接收端就會發生混淆,這個時候單單依靠Seq是沒有辦法判斷數據報的先後順序的。爲此TCP引入了時間戳選項來解決這個問題,作爲32比特的Seq的一個拓展。

需要強調的有兩點
一是Seq數值的增長是和數據的傳輸速度有關的,而ISN是根據定時器線性增長的。二是實際發生這種情況的條件非常苛刻。因爲如果發生環回的時間大於MSL,那麼我們上文提到的第二個數據報在第六個數據報發送時,一定消失在網絡當中了。所以發生這種情況一般是在高速通道上。在RFC 1185 TCP Extension for High-Speed Paths中做了詳細的討論。

ISN是三次握手需要協商約定的一個重要選項。

除此之外SYN包的TCP首部中,選項裏最常見的一個字段就是MSS(Maximum Segment Size)。雙方在建立連接的時候會互相通告對方己端能夠接收的最大報文長度,目的是爲了避免發生分片。需要注意的是MSS的值是不包括IP首部和TCP首部的,例如在MTU位1500的外出接口上,通告的MSS應該是1460。但是這個選項的侷限在於它僅僅只在SYN包出現,這也就意味着如果通訊建立的過程當中MSS的數值發生了變化,對端是無法感知的。另外,MSS僅僅只聲明瞭自己的約束,如果中間網絡的MTU小於兩端通告的MSS,那麼分片依然是無法避免的。

IPv6是期待以1280打天下的。它要求硬件提供的最小MTU是1280

有關三次握手的討論,暫時告一段落。爲了與之呼應,我們再來看一看TCP斷開連接的四次揮手。TCP作爲全雙工的通信,在連接建立完成之後實際存在了兩條虛擬信道:客戶端 —> 服務器服務器 —> 客戶端。因此我們在關閉的時候也要逐一的拆除。

但和連接建立不同的是,雙方的傳輸任務無法保證在同一時間結束。這意味着在某一端發起關閉的時候,我們必須要保證,在拆除其中一條信道的同時,不影響另一條信道的通訊。這也就是半關閉的由來。

在socket編程中,關閉連接的方式通常是close()函數。每次調用close()函數時會把對應的描述符sockfd引用計數減一,在計數爲0時同時關閉讀和寫也就是完全關閉。爲了應對半關閉的情況,我們會使用shutdown()函數,指定第二個參數爲SHUT_WR來實現半關閉


交互數據流和成塊數據流

在書中的十九和二十章,討論了交互數據流和成塊數據流兩種情況的傳輸策略。但是書中並未就這兩種數據流給出明確的定義。在瞭解它們各自策略是如何實現之前,明確它們的特點和定義還是十分有必要的。

什麼是交互數據流和成塊數據流

顧名思義,交互數據流的特點表現在交互上,這也就是說數據流流動的方向是雙向的,本質是通信兩端的信息交換。通常情況下客戶端向服務器發出一條消息,服務器除了會返回ACK對消息進行確認之外,還會針對客戶端的請求反饋相關的信息。交互數據流的每一個報文通常都會比較小

我們採用客戶端-服務器模型,並且規定主動發起的一方爲客戶端。之後的例子在沒有特殊說明的情況下默認都是這樣約定

在書中有過這樣一段描述

一些有關TCP通信量的研究發現,如果按照分組數量計算,約有一半的TCP報文段包含成塊數據(如FTP、電子郵件和Usenet新聞),另一半則包含交互數據(如Telnet和Rlogin)。如果按字節計算,則成塊數據與交互數據比例約爲 90%和10%。這是因爲成塊數據的報文段基本上都是滿長渡的,而交互數據則小的多。

成塊數據流的特點與交互數據流相反,它的側重在於單向的傳輸,所承擔的是要把一個較大的數據儘快的送抵到對端的任務。

我們可以做一個簡單的比喻來幫助理解:交互數據流類似QQ上兩人的聊天;而成塊數據流則是在傳輸文件。

交互數據流

交互數據流的傳輸策略有兩個重點

1. 經受時延的確認

通常TCP在接收到數據時並不立即發送ACK,而是推遲發送等待一段時間,之後如果有相同方向的數據需要傳遞,會捎帶ack一起發送。

屏幕快照 2018-01-11 下午8.49.22.png

絕大多數的實現裏是以200ms作爲最大的時延等待。這裏需要說明的是,時延並不是以數據到達目的端開始計算的,而是以TCP的實現當中一個200ms的定時器爲準。如果有ack需要發送那麼會在定時器下一次的溢出時執行。考慮到數據到達的時間是隨機的,那麼ack的發送時機也就不固定,範圍在0 - 200ms。

與之類似的是TCP超時定時器

使用ack捎帶的一個好處在於它提高了TCP的性能,原因在於它提高了有效載荷的比例。

ack捎帶

當我們嘗試通過TCP發送數據的時候,無論是1個字節或是MSS大小的數據報,每一份報文都是以固定的格式發出的(這裏忽略各類首部的額外選項)

IP首部 + TCP首部 + 有效載荷

假設我們需要傳輸一份大小爲N的數據,需要分拆成m個包來完成,那麼傳輸的數據總量就是

m * (20 + 20) + N

每一個報文儘可能多的裝載數據,或者說使用儘可能少的分包來完成數據的傳遞,有效載荷佔傳輸總量的比例也就越高。

ack捎帶的處理可以壓縮我們需要傳輸的數據,節省了原先ack首部的字節傳輸;在等待的同時,如果有多份數據抵達,那麼這些數據的確認可以合併成一個ack報文,減少了報文的數量。考慮到交互數據流每一個報文數據量相對較小,ack報文的壓縮帶來的效率提升會更加明顯。

其次ack捎帶可以有效避免糊塗窗口綜合徵。

糊塗窗口綜合徵指的是當發送端應用進程產生數據很慢、或者接收端應用進程處理接收緩衝區數據很慢,或者兩個情況同時存在;使得通信兩端傳輸的報文段很小(特別是有效載荷很小)的情況。

極端情況下,有效載荷可能只有1個字節;而傳輸開銷有40字節(20字節的IP頭+20字節的TCP頭)

爲了避免這種情況的發生,我們可以從發送端和接收端兩邊入手解決。這個問題我們留在講解完滑動窗口之後再討論,現在需要明確的是對於接收端而言,ack捎帶是一個可行的解決方案。

2. Nagle算法

Nagle算法要求在一個TCP連接上最多只能有一個未被確認的分組,在該分組被確認之前不能夠發送其他的分組。如果在等待期間有需要發送的分組。會被收集起來在收到確認以後用同一個分組發出。

該算法的優越之處在於是自適應的:數據被對端確認的越快,發送端數據發送的也就越快;在相對低速的環境下可以有效減少微小分組的數據,提高TCP的傳輸效率。

在上文介紹ack捎帶的時候我們提到壓縮報文數量可以提高有效載荷在總體傳輸數據當中的比例,一定程度上提高了TCP傳輸的效率。另一點需要強調的是,TCP提供的傳輸服務是有序的。考慮到傳輸過程中數據報可能會丟失、亂序,接收端必須要對數據報進行處理。即使是微小分組,也是一個獨立的數據報,過多的微小分組很顯然會給接收端帶來相應的處理壓力,這不是我們所期望的。

微小分組指的是有效載荷非常小的報文

LAN上的通信相對簡單,一般不會出現擁塞,傳輸速率相對WAN也比較高。我們做的大部分處理更多是要爲低速的WAN考慮。這裏可以簡單做一個例子說明。

屏幕快照 2018-01-12 下午7.38.22.png

參照上圖可以看出,LAN內一個字節從被髮送到收到確認和回顯的平均往返時間約爲t = 16ms。如果我們的輸入速度小於60個字節每秒,那麼Nagle算法並不會對我們的傳輸造成任何影響,因爲每次我們準備好下一個輸出字節的時候,上一個字節已經送抵對端並且收到確認和回顯了。

1s = 1000ms。 1000/16 = 62.5 ≈ 60

當平均往返時間 t 增加時(比如在WAN內)情況會發生變化。很可能我們鍵入新的字節但是之前數據還未被確認,這時我們鍵入的數據都會被收集等待確認一起發送。在某些應用程序上可能會感受到卡頓和反饋不及時。

比如X窗口系統服務器,用以標示鼠標移動的微小分組必須無時延地發送,以便提供實時的反饋消息。

Nagle算法在某些情況下甚至可能成爲我們網絡通信的瓶頸。假設這樣一種情況:客戶端發送了一條報文給服務端,之後等待服務端的確認;而服務器在收到報文之後並不立即返回ack而是等待。等待的原因可以是ack捎帶,也可以是認爲服務器認爲客戶端提供的信息不足夠重複等待期望更多數據,無論是哪一種情況都勢必陷入一個死鎖的狀態:雙方都在等待對方的消息。通常情況超時才能打破這種僵局,但這顯然是一種無意義的消耗。

socket編程中,可以設置TCP_NODELAY來關閉Nagle算法

之前看到這樣一個問題:如果客戶端發送了一條消息之後,因爲某些原因沒有收到確認發生了超時,在這期間如果客戶端收集了新的數據,超時之後發送的這個數據報應該如果發送?

TCP有一個實際存在的緩衝區,客戶端發送的數據會留有備份,在接收到對應ack之後纔會移除。如果發生超時客戶端只需要把緩衝區的數據發送出去即可(我們假設所有的數據可以放在一個報文裏發出)。

爲什麼說是一個實際存在的緩衝區呢。因爲udp雖然有緩衝區這個概念,但是並不存在,所謂緩衝區的大小隻是一個最大udp報文長度的限定。

Nagle算法也可以避免糊塗窗口綜合徵。這是從發送端入手的一種解決方式。

總結

兩種傳輸策略實質是分別從發送端(Nagle算法)和接收端(ACK捎帶)兩端入手,通過壓縮報文的數量來提高交互數據流整體傳輸的速率。

成塊數據流

在上文中我們討論了交互數據流的Nagle算法,在低速的WAN上經常會造成時延。這對於成塊數據流的傳輸是不太能夠接受的。我們必須要考慮到既然是交互,那麼每一條消息的發出除了對端的確認,額外的反饋消息也是非常重要的,因爲這很可能會影響到後續交互的邏輯。但是在成塊數據流上則沒有這個麻煩,在大部分的情況下它的目的非常明確:儘快地將數據搬運到對端。出於效率的考慮,TCP使用另一種傳輸策略,允許發送方在停止並等待確認前可以連續發送多個分組。

舉一個例子:假設登錄過程就是一次交互,那麼客戶端傳遞了用戶名和密碼之後,必須要等待服務器的反饋:這對用戶名密碼是否正確。之後客戶端必須根據判定的結果來繼續下一步的操作。

書中二十章的內容全部在圍繞一個關鍵詞展開:窗口。要理解成塊數據流的傳輸策略,明確窗口的定義和它設計的意義,是非常有必要的。

首先我們來看一下數據傳遞的過程。

數據傳遞流程

數據在被送達對端之後,存儲在TCP的接收緩衝區,這是一個有限的空間。上層的應用進程會從接收緩衝區讀取數據,之後相應的數據會從TCP的接收緩衝區移除,用以騰出空間接收新的數據。通告窗口(advertise window)就是用以描述自身接收緩衝區中當前可用的空間量的,在通告發送端之後可以確保它發送的數據不會使接收緩衝區溢出。

這是TCP提供的一種流量控制。我們必須要思考成塊數據流的傳輸策略爲什麼要引入窗口這個概念?因爲TCP無法保證發送端傳輸的速率和接收端處理數據的速率保持一致!

如果發送端的傳輸速率相對接收端的處理速率較慢,那麼每次數據報送抵接收端都可以確保緩衝區有足夠的空間去接收。但是情況反過來,發送端儘可能快的將數據拋出,接收端會因爲緩衝區空間不足而丟棄分組。爲了避免這樣一種情況,TCP採用了滑動窗口協議。

在交互數據流中,情況相對簡單很多。因爲Nagle只允許網絡中存在最多一個未被確認的分組,一來一回的傳輸策略邏輯比較簡單;而且交互數據流報文相對較短,接收端壓力不會太大。

滑動窗口協議

注意圖中紅框標示的報文7和8。在這之前svr主機連續發送了三個報文(4 - 6),在第7個報文的ack只確認了4 - 5兩個報文的內容。我們可以合理推斷,在bsdi主機處理第4個報文時,執行了ack捎帶的操作,在這期間bsdi處理完報文 5,之後時延定時器發生溢出發出ack確認4 - 5。下一個時延定時器溢出bsdi處理完報文 6,發出報文 8確認了報文 6。注意win參數從3072 = 4096 - 1024!這說明報文 6還留在bsdi的TCP緩衝區裏,可用空間減少了相應的大小。

用另一種可視化的方式來展示這個過程

滑動窗口協議1

綠色部分代表已經被確認的報文;黃色部分是通告窗口大小,表示接收端緩衝區可以同時容納報文4 5 6 7 8 9;紅色部分標示的是後續待發送的數據,但因爲超出了通告窗口大小的限制當前不能發送。

但是這部分有一個需要強調的點是,發送端並非是從黃色部分的左側邊沿開始(圖中的報文 4)選擇報文發送。因爲上圖我們漏掉了一種可能:已經發送但尚未被確認的報文

繼續以上圖爲例,假設在接收端通告窗口的時候,雖然只確認了報文 1- 3,但是發送端實際已經發送了報文段 4 - 6,那麼實際可用的窗口大小實際是報文 7 8 9這個範圍。因爲那些未被確認的報文(inflight)我們假設它們尚在路上,會在之後得到確認。

實際窗口

通告窗口的大小並不是一成不變的,受各種條件的影響通告窗口兩端的邊界會滑動使得通告窗口縮小或者擴大。這也是爲什麼我們稱之爲滑動窗口協議的原因。

  1. 通告窗口左邊會隨着報文被接收端確認而向右移動,我們稱之爲窗口合攏。因爲確認過的報文不會被取消確認,所以窗口左邊不可能出現向左移動的情況。

  2. 接收端的應用進程從TCP緩衝區讀取數據之後,會騰出相應的空間來接收新的數據。這個時候通告窗口右邊會向右移動,我們稱之爲窗口張開

  3. 通告窗口右邊在極少數情況下會向左移動,我們稱之爲窗口收縮。雖然TCP被要求必須能夠在對端出現這種情況時進行處理,但這是極不推薦的一種方式。

滑動窗口

如果通告窗口的左邊沿和右邊沿發生合攏,那麼此時我們稱之爲零窗口。發送端無法繼續發送數據,必須等待接收端處理。

窗口更新

圖中紅框標示的報文 14就是我們提到的零窗口。之後接收端重新發送了一條ack(報文 15),但並沒有確認新的數據,只是更新了win告訴發送端可以繼續發送。這種情況我們稱之爲窗口更新

擁塞窗口

上文所示的例子有一個侷限:它們測試在LAN內,在傳輸的一開始就發出多個報文段直到接收端通告了窗口並且達到了窗口的限制。在LAN內當然沒有問題,因爲我們不需要考慮發送端和接收端之間可能存在的多個路由和鏈路,但是如果情況放在WAN內這顯然就不夠穩妥了。多個分組的發出在經過一些中間路由的時候可能需要被緩存,發送端不受限制的發送很可能會耗盡存儲器的空間。

最理想的情況應當是發送端發送數據的速率和接收到確認的速率保持一致(更快只會因爲接收端或者中間路由無法處理而丟包)。爲了探測出未知網絡環境下合理的發送速率,TCP引入了慢啓動算法和擁塞窗口(congestion window)概念。

所謂慢啓動,指的是發送端首先會發送一個分組,等待接收端的確認。在收到確認之後擁塞窗口會從初始的1個分組大小增加到2個。再次收到確認之後擁塞窗口會拓展爲4個分組大小。以此類推,在出現避讓之前擁塞窗口是以指數級別增長的。

這裏需要強調的是兩點:

  1. TCP發送的分組同時受通告窗口和擁塞窗口限制,兩者取較小的一個值。

  2. 雖然擁塞窗口和通告窗口一樣是以字節爲單位,但擁塞窗口通常是分組大小的整倍數。我們在描述擁塞窗口時,會以單個分組大小作爲單位1,這樣方便我們描述它的增長過程。

慢啓動算法實質模擬的是一個試探的過程。它在每次發送數據被確認之後都會拓展擁塞窗口來試探傳輸速率的極限,指數增長的方式讓擁塞窗口雖然初始數值很小,但增長確是爆炸式的,擁塞窗口會很快突破網絡的極限,導致中間路由丟棄分組。當丟包發生時,發送方會被通知擁塞窗口開的過大,需要作出修改。

爲什麼考慮的是中間路由丟棄分組而不是接收方緩衝區空間不足? 因爲發送的分組的大小同時受通告窗口和擁塞窗口限制。

TCP的流量控制依賴於丟包這個條件,TCP需要根據是否丟包來決定擴大發送分組還是減少。但問題在於TCP對丟包的實際情況瞭解的並不全面,實際TCP是不知道丟包的真正原因的!TCP認爲丟包就是網絡傳輸出現了擁堵,所以慢啓動算法裏出現丟包會讓擁塞窗口做指數級的避讓。也許這個假設在TCP發明的當時是成立的,但現在很多情況比如無限網絡中這個假設成了TCP的一個缺陷。例如信號干擾或者亂序誤判都可能讓發送方認爲丟包,但這種情況下避讓是完全沒有必要的。

有關超時和重傳的部分,受限於本文的篇幅會在下一篇展開。這裏就不做贅述。現在針對TCP慢啓動算法的缺陷也提出瞭解決方案(Google BBR算法),這個內容我們會留在後續介紹TCP改進的文章裏細說。

帶寬時延乘積

日常生活裏,有時候會聽到朋友抱怨網速太慢

" 這個小水管滴答滴答受不了了"

這確實是一個非常有趣的比喻。我們說TCP是一個無邊界的字節流傳輸協議,通信兩端存在一個虛擬的管道,數據在裏面靜靜的流淌。但是這個虛擬的管道要如何去描述它?理解了這個問題之後相信會對你TCP的學習有很大幫助。

假設我們用一個矩形來描述時間t內傳輸的數據S

屏幕快照 2018-01-11 下午7.57.33.png

如果t無限小,那麼S就會收縮成一條線,我們可以認爲這條線的高度就是管道瞬時的傳輸速率,我們稱之爲帶寬

屏幕快照 2018-01-11 下午7.58.25.png

之前我們提到過一個概念往返時延RTT(round-trip time) ,用以描述數據發出到被確認的時間。那麼在帶寬爲v的管道上經過時間RTT傳輸的數據就是整個管道的容量

屏幕快照 2018-01-11 下午7.59.04.png

我們稱之爲帶寬時延乘積,公式如下

帶寬時延乘積(capacity)[bit] = 帶寬(bandwidth)[b/s] x 往返時延RTT(round-trip time) [s]

現在讓我們考慮下圖的這種情況

屏幕快照 2018-01-11 下午8.24.05.png

這種情況非常普遍,局域網內的主機將數據發往處於另一個局域網的主機,需要經過相對低速的廣域網。我們可以明顯看到瓶頸出現在路由器R1這裏:R1左側的帶寬明顯大於右側,當輸入速率大於輸出速率時,就會擁塞。路由器R2則沒有問題,因爲它是從低速的WAN內向LAN傳輸數據。

需要注意的是經過路由器R 2的分組,之間的間隔和在WAN內保持一致的。如圖所示t1 = t2。雖然我們圖中看到的是WAN內帶寬打滿,但是每個分組左側邊沿所在的位置標示的纔是該分組實際發出的時間。兩個標示分組的矩形,它們左側邊沿之間的距離就是發出時間的間隔。

這可能有點比較難以理解,因爲我們將實際數據報的傳遞抽象成了流。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章