當我們使用TCP,從客戶端傳送資料到伺服器,這個過程會是怎樣的呢?
首先,當然是耳熟能詳的三次握手過程,那當連線建立之後,就一股腦傳送所有資料嗎?
當然不是,一下子傳送太多資料,接收端可能沒有那麼大的空間,就浪費了流量。
TCP使用滑動視窗來管理傳送方和接收方之間的資料傳輸量。滑動視窗透過控制未確認資料包的數量,確保傳送方發出的包不會超出接收方的處理能力。
滑動視窗
滑動視窗的工作機制如下:
- TCP在每個ACK包中,通知對方自己目前能接收多少資料,即TCP頭部中的視窗大小(三次握手期間的ACK也會包含視窗大小)。
- 傳送方可以在這個視窗大小內,連續傳送多個資料包,而不必等待每個資料包的確認。
- 當傳送方收到接收方的
ACK
確認,視窗就會向前滑動,允許傳送方繼續傳送新的資料包。
在某個時間段,傳送方的TCP資料流如下圖所示,可以分成4個部分:
- 已傳送且已確認的資料:這部分資料已經沒用了,不再需要儲存。
- 已傳送但未確認的資料:需要儲存在緩衝區裡,如果丟包了,可以進行重傳。
- 未傳送,允許傳送的資料:沒有超過接收方緩衝區,可以傳送資料。
- 未傳送,不允許傳送的資料:超過對方緩衝區,不可以傳送。
滑動視窗的優點:
- 高效利用頻寬:滑動視窗允許傳送方連續傳送多個資料包,而無需等待每個資料包的確認,從而提高了頻寬利用率。
- 流量控制:透過動態調整視窗大小,滑動視窗機制能有效控制資料流量,防止網路擁堵。
視窗滿的情況
在接收方看來,資料可以分成三個部分:
- 成功接收並確認的資料
- 未收到但可以接收的資料
- 未收到且不可接收的資料
未收到但可以接收的部分,就是接收方的視窗大小。
接收方收到資料後,會存放到緩衝區中,等待上層應用獲取資料(socket呼叫read函式)。
如果上層應用繁忙,讀取效率較低,那麼這個視窗就會慢慢變小,甚至會變成0,也就是視窗滿的情況。
這時候,接收方會傳送一個ZeroWindow
的包,告訴傳送方,這邊已經不能再接收資料了,傳送方就不再傳送資料。
等到緩衝區的資料被讀取之後,接收方會發一個WindowUpdate
的ACK,告訴傳送方自己最新的視窗大小,傳送方就可以繼續傳送資料了。
但是這裡有個問題,如果這個WindowUpdate
的包丟失了的話,傳送方就只能繼續保持0視窗,資料在這裡就卡住不再傳送了。
為了解決這個問題,TCP設定了定時探測,傳送ZeroWindowProbe
,獲取接收端最新的視窗大小。
TCP協議本身並沒有一個明確的
Window Full
標記。然而,在實際使用中,有些網路監測工具和協議分析器(例如Wireshark)會標識或標記某些資料包,以表明傳送方的傳送視窗已經完全被使用。這種標記主要是用來幫助使用者理解和分析TCP連線中的流量控制和擁塞控制情況。
糊塗視窗綜合症
當接收方緩衝區滿時,視窗關閉,如果應用層讀取了一個位元組的資料,此時緩衝區就有了一個位元組的空間,這時候立刻傳送WindowUpdate
通知傳送方的話,那傳送方就可能發一個位元組的資料過來,一個TCP包只包含一個位元組的資料,這效率就很低下。
這種情況下,這個連線的視窗一直保持在很小的狀態,稱作糊塗視窗綜合症
。
為了解決這個問題,當視窗大小
小於min(MSS,快取空間/2)
,也就是小於MSS
與1/2
快取大小中的最小值時,就會向傳送方通告視窗為 0,也就阻止了傳送方再發資料過來。
等到接收方處理了一些資料後,視窗大小 >= MSS,或者接收方快取空間有一半可以使用,才更新視窗大小,讓傳送方傳送資料過來。
擁塞控制
滑動視窗控制的是一個TCP連線的流量,避免傳送方的資料填滿接收方的快取。但是,網路上不只一個TCP連線,如果不加以控制的話,就可能發生資料的擁堵,擁堵導致丟包,丟包需要重傳,則又加大了擁堵。
所以,TCP使用了擁塞控制來避免資料填滿整個網路。
擁塞視窗 cwnd 是傳送方維護的一個的狀態變數,它會根據網路的擁塞程度動態變化的 。當cwnd=n
時,表示傳送方可以傳送n
個MSS
大小的資料
擁塞控制主要是四個演算法:
- 慢啟動
- 擁塞避免
- 快速重傳
- 快速恢復
慢啟動
慢啟動的思路就是,不要一開始就傳送大量的資料,先探測一下網路的擁塞程度,也就是說由小到大逐漸增加cwnd
的大小,其演算法如下:
- 建立連線後,初始化
cwnd
為1
,可以傳送1個MSS
資料。 - 每次收到
ACK
,則將cwnd
加1。 cwnd
達到某一個閾值ssthresh
(slow start threshold)後,不再使用慢啟動,改用擁塞避免演算法。
從上圖中,可以看到,每一個rtt
時間,cwnd
都會翻倍,從而快速地增長。在良好的網路環境下,可以很快達到閾值,進入擁塞避免演算法。
在一些現代作業系統中(如 Linux 和 Windows),TCP 初始擁塞視窗的預設值為 10 個 MSS。這使得傳送方在建立連線後的初始資料傳輸中,可以一次傳送多達 10 個 MSS 的資料包,而不必經歷傳統的慢啟動階段。
擁塞避免
慢啟動時,起點低,但指數增長,速度快,達到一定程度後,就不能再繼續指數增長,以防止擁塞。擁塞避免的想法就是,在一個rtt
時間內,讓cwnd
不是翻倍,而是加一,緩慢增長。
那麼,如何讓cwnd
在一個rtt
中加一呢?在慢啟動演算法中,在某一輪次,cwnd=n
,此時連續傳送n
個MSS
,每次收到ACK
則cwnd+1
,收到n
個則cwnd+n
,形成翻倍的效果。同理,只要在每次收到ACK
時,將cwnd+1
改成cwnd+1/n
,那麼在n
個ACK
後,則形成cwnd+1
的結果。
在慢啟動和擁塞避免階段,如果出現超時,則重發超時的資料,然後處理如下:
- 將
ssthresh
設為cwnd/2
- 將
cwnd
設為1 - 進入慢啟動演算法
快速重傳
當檢測是否丟包時,每次都要等待超時的發生,會浪費很長時間,因此引入了快速重傳:傳送方只要收到3個重複的ACK
,即認為丟包發生,此時會立即重傳丟失的包,而不再等待超時的出現。
快速恢復
為了解決丟包後進入慢啟動引起的效率降低,在快速重傳的基礎上,又引入了快速恢復,在發生快速重傳之後,擁塞控制如下處理:
- 將
ssthresh
設為cwnd/2
。 - 將
cwnd
設為ssthresh+3
(+3是因為已經收到3個重複的ACK)。 - 如果再收到重複的ACK,則
cwnd+1
。 - 如果收到新的ACK,則快速恢復結束,進入擁塞避免。
參考資料
- 知乎 - 筆記:滑動視窗