【大廠面試必備系列】滑動視窗協定

2022-06-16 12:02:23

引言

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

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

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

對於傳送方來說,視窗大小就是指無需等待確認應答,可以連續傳送資料的最大值。


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

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

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

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

滑動視窗詳解

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

  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 個)老師說 "恐驚天上人",......


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