想象一下這個場景:主機 A 一直向主機 B 傳送資料,不考慮主機 B 的接收能力,則可能導致主機 B 的接收緩衝區滿了而無法再接收資料,從而導致大量的資料丟包,引發重傳機制。而在重傳的過程中,若主機 B 的接收緩衝區情況仍未好轉,則會將大量的時間浪費在重傳資料上,降低傳送資料的效率。
所以引入了流量控制機制,主機 B 通過告訴主機 A 自己接收緩衝區的大小,來使主機 A 控制傳送的資料量。總結來說:所謂流量控制就是控制傳送方傳送速率,保證接收方來得及接收。
TCP 實現流量控制主要就是通過 滑動視窗協定。
對於傳送方來說,視窗大小就是指無需等待確認應答,可以連續傳送資料的最大值。
視窗大小具體由誰來設定呢?
視窗大小和 TCP 報文首部中 16 位的 視窗大小 Window 欄位有關:
該欄位的含義是指自己接收緩衝區的剩餘大小,於是傳送端就可以根據這個接收端的處理能力來傳送資料,而不會導致接收端處理不過來。
所以,通常來說視窗大小是由接收方來決定的。
站在傳送方的角度,滑動視窗可以分為四個部分:
第 2 和第 3 部分加起來就剛好就是接收方視窗大小,它規定了當前傳送方能傳送的最巨量資料量。
傳送方在收到確認應答報文之前,必須在視窗中保留已傳送的報文段(因為報文段可能在網路中丟失,所以必須把這些未確認的報文段保留這,以便必要時重傳);如果在規定時間間隔內收到接收方發來的確認應答報文,就可以將這些報文段從視窗中清除。
當傳送方收到接收方發來的確認應答後,就將視窗中那些被確認的報文清除出去,然後視窗向右移動,如下圖所示:
隨著雙方通訊的進行,視窗將不斷向右移動,因此被形象地稱為滑動視窗(Sliding Window)
對於 TCP 的接收方,視窗稍微簡單點,分為三個部分:
由於 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 個)老師說 "恐驚天上人",......
小夥伴們大家好呀,我是小牛肉,公眾號【飛天小牛肉】定期推播大廠面試題,提供背誦版 + 詳細版,知其然而知其所以然,讓八股文變得有價值!)