【大廠面試必備系列】滑動窗口協議

引言

想像一下這個場景:主機 A 一直向主機 B 發送數據,不考慮主機 B 的接收能力,則可能導致主機 B 的接收緩衝區滿了而無法再接收數據,從而導致大量的數據丟包,引發重傳機制。而在重傳的過程中,若主機 B 的接收緩衝區情況仍未好轉,則會將大量的時間浪費在重傳數據上,降低傳送數據的效率。

所以引入了流量控制機制,主機 B 通過告訴主機 A 自己接收緩衝區的大小,來使主機 A 控制發送的數據量。總結來說:所謂流量控制就是控制發送方發送速率,保證接收方來得及接收

TCP 實現流量控制主要就是通過 滑動窗口協議

對於發送方來說,窗口大小就是指無需等待確認應答,可以連續發送數據的最大值。

窗口大小具體由誰來設定呢?

窗口大小和 TCP 報文首部中 16 位的 窗口大小 Window 欄位有關:

TCP 報文段首部

該欄位的含義是指自己接收緩衝區的剩餘大小,於是發送端就可以根據這個接收端的處理能力來發送數據,而不會導致接收端處理不過來。

所以,通常來說窗口大小是由接收方來決定的

滑動窗口詳解

站在發送方的角度,滑動窗口可以分為四個部分:

  1. 已發送且已確認,這部分已經發送完畢,可以忽略;
  2. 已發送但未確認,這部分可能在網路中丟失,數據必須保留以便必要時重傳;
  3. 未發送但可發送,這部分接收方緩衝區還有空間保存,可以發出去;
  4. 未發送且暫不可發送,這部分已超出接收方緩衝區存儲空間,就算髮出去也沒意義;

第 2 和第 3 部分加起來就剛好就是接收方窗口大小,它規定了當前發送方能發送的最大數據量。

發送方在收到確認應答報文之前,必須在窗口中保留已發送的報文段(因為報文段可能在網路中丟失,所以必須把這些未確認的報文段保留這,以便必要時重傳);如果在規定時間間隔內收到接收方發來的確認應答報文,就可以將這些報文段從窗口中清除。

當發送方收到接收方發來的確認應答後,就將窗口中那些被確認的報文清除出去,然後窗口向右移動,如下圖所示:

隨著雙方通訊的進行,窗口將不斷向右移動,因此被形象地稱為滑動窗口(Sliding Window)

對於 TCP 的接收方,窗口稍微簡單點,分為三個部分:

  1. 已接收
  2. 未接收準備接收 (也即接收窗口,再強調一遍,接收窗口的大小決定發送窗口的大小)
  3. 未接收並未準備接收

由於 ACK 直接由 TCP 協議棧回復,默認無應用延遲,不存在 「已接收未回復 ACK」

綜上,舉個例子,假設發送方需要發送的數據總長度為 400 位元組,分成 4 個報文段,每個報文段長度是 100 位元組:

1)三次握手連接建立時接收方告訴發送方,我的接收窗口大小(rwnd) 是 300 位元組

此時的接收方滑動窗口如下:

接收方滑動窗口

此時的發送方滑動窗口如下:

發送方滑動窗口

2)發送方發送第一個報文段(序號 1 – 100),還能再發送 200 個位元組

3)發送方發送第二個報文段(序號 101 – 200),還能再發送 100 個位元組

4)發送方發送第三個報文段(序號 201 – 300),還能再發送 0 個位元組

此時,發送方的窗口中存了三個報文段了

此時的發送方滑動窗口如下:

發送方滑動窗口

5)接收方接收到了第一個報文段和第三個報文段,中間第二個報文段丟失。此時接收方返回一個報文段 ack = 101, rwnd = 200(假設這裡發生流量控制,把窗口大小降到了 200,允許發送方繼續發送起始序號為 101,長度為 200 的報文)

此時的接收方滑動窗口如下(本來窗口右端應該右移,但是這裡發生了流量控制,接收方希望縮小窗口大小,所以正好,這裡就不需要向右擴展了):

接收方滑動窗口

發送方收到了第一個報文段的確認,從窗口中移除掉第一個報文段

此時的發送方滑動窗口如下:

發送方滑動窗口

6)發送方一直沒有收到第二個報文段的確認應答,在等待超時後重傳第二個報文段(序號 101 – 200)

7)接收方成功收到第二個報文段(窗口中有第二個和第三個報文段了),於是向發送方返回一個報文段 ack = 301, rwnd = 100(假設這裡發生流量控制,把窗口大小降到了 100)

此時的接收方滑動窗口如下:(本來窗口右端應該右移,但是這裡發生了流量控制,接收方希望縮小窗口大小,所以正好,這裡就不需要向右擴展了)

接收方滑動窗口

發送方收到了第二個和第三個報文段的確認,從窗口中移除掉這倆報文段

8)發送方發送第四個報文段(序號 301 – 400)

此時的發送方滑動窗口如下:

發送方滑動窗口

⭐ 窗口的本質

說了半天,窗口好像只是一個虛無縹緲的概念,

實際上,由於 TCP 是內核維護的,所以窗口中的報文數據其實就是存放在內核緩衝區

注意這裡區分下內核緩衝區(buffer)和高速快取的概念

內核緩衝區大小一般是不會發生改變的,緩衝區大小 > 窗口大小,且窗口大小根據緩衝區中空閑空間的大小在不斷發生改變。

對於接收方來說:

  • 接收方根據緩衝區空閑的空間大小,計算出後續能夠接收多少位元組的報文(即接收窗口的大小)
  • 當內核接收到報文時,將其存放在緩衝區中,這樣緩衝區中空閑的空間就變小了,接收窗口也就隨之變小了
  • 當進程調用 read 函數後(將數據從內核緩衝區複製到用戶/進程緩衝區),報文數據被讀入了用戶空間,內核緩衝區就被清空,這意味著主機可以接收更多的報文,接收窗口就會變大

對於發送方來說,進程在發送報文之前會調用 write 函數(將數據從用戶/進程緩衝區寫到內核緩衝區),這樣,緩衝區中可用空間變小,窗口變小,可發送的數據就變少了,等收到這些發送出去的數據的確認應答後,再從緩衝區中清除掉,從而使得窗口變大。

通俗的例子

下面來更通俗地解釋下滑動窗口,看下面這個場景,老師(發送方)說一段話,學生(接收方)來記

最原始的模式,發送方一股腦把所有的報文段全都發出去。

老師說 “危樓高百尺,手可摘星辰,不敢高聲語,恐驚天上人”(咱把每個字看成一個報文段,總共 20 個報文段)

學生寫道”危樓高百尺,手可…….”

上面的模式過於簡單粗暴,發送方發送速度太快,接收方跟不上,並且重傳成本過高。

於是他們換了一種模式:每發送一個報文段就等待確認一個報文段,收到確認後才能發送下一個

老師說 “危”,學生說”確認”

老師說 “樓”,學生說”確認”

老師說 “高”,學生說”確認”

………

上面的模式每發一個報文段,必須等到確認後才能再次發送,效率低下。

於是他們又換了一種模式:累積確認,既不是一股腦把所有的報文段全都發出去,也不是一次只發一個報文段,而是分組發送,每次發幾個報文段。

老師說 “危樓高百尺” (5 個報文段),學生說 “確認”

老師說 “手可摘星辰”,學生說 “手可…”(3 個報文段丟失)

老師說 “不敢高聲語”,學生說 “確認”

老師一直沒有收到 “摘星辰” 的確認,於是重新說了一遍 “摘星辰”,學生說 “確認”

老師說 “恐驚天上人”,學生說 “確認”

上面的模式提高了效率,連續多個報文段一起進行發送, 但是到底該怎麼決定多少個報文段一起發送呢呢?

於是他們在上面模式的基礎上,做出了一些改進:滑動窗口,接收方認為狀態好(窗口比較大)的時候, 讓發送方每次多發一點;接收方認為狀態不好(窗口比較小)的時候,讓發送方每次少發送一點,起到一個流量控制的作用,限制發送方的速度。

學生告訴老師,我一次性可以接收 10 個報文段

老師說 “危樓高百尺,手可摘星辰”,學生說 “危樓高百尺,手可…”(3 個報文段丟失,返回 」可” 的確認應答,一共確認了 7 個報文段,老師的可用窗口右移,窗口中現在還有 「摘星辰」 3 個報文段)

學生說,我狀態不行,一次性現在只能接收 5 個報文段(流量控制,縮小窗口)

老師說 “不敢”(窗口中還有 「摘星辰」 3 個報文段,所以只能發送 2 個),學生說 “確認”

老師一直沒有收到 “摘星辰” 的確認,於是重新說了一遍,學生說 “確認”

(可用窗口恢復為 5 個)老師說 “恐驚天上人”,……


小夥伴們大家好呀,我是小牛肉,公眾號【飛天小牛肉】定期推送大廠面試題,提供背誦版 + 詳細版,知其然而知其所以然,讓八股文變得有價值!)