在我們當初學習網絡編程的時候,都接觸過TCP,在TCP中,對於數據傳輸有各種策略,比如滑動窗口、擁塞窗口機制,又比如慢啟動、快速恢復、擁塞避免等。通過本文,我們將了解滑動窗口在TCP中是如何使用的。
滑動窗口實現了TCP流控制。首先明確滑動窗口的范疇:
- TCP是雙工的協議,會話的雙方都可以同時接收和發送數據。
- 會話的雙方都各自維護一個發送窗口和一個接收窗口。各自的接收窗口大小取決於應用、系統、硬件的限制(TCP傳輸速率不能大於應用的數據處理速率)。各自的發送窗口則要求取決於對端通告的接收窗口,要求相同。
滑動窗口解決的是流量控制的的問題,就是如果接收端和發送端對數據包的處理速度不同,如何讓雙方達成一致。接收端的緩存傳輸數據給應用層,但這個過程不一定是即時的,如果發送速度太快,會出現接收端數據overflow,流量控制解決的是這個問題。
發送端窗口
上圖是發送端滑動窗口的簡圖。 我們可以將數據分為4個部分:
- 發送和已確認的字節(藍色部分)
- 已發送但尚未確認的字節(黃色部分)
- 未發送的字節和接收方准備接收的字節,即在緩沖區buffer中(綠色部分)
- 未發送且接收方未准備接收的字節(灰色部分)
其中第三部分,也就是綠色部分,也稱為可用窗口,因為這是發送方可以使用的窗口。
發送窗口由黃色和綠色部分組成。 這些字節要么已經發送,要么可以發送。
當發送方發送21-25字節並使用可用窗口中的所有字節時,可用窗口可能為空,發送窗口保持不變(如下圖)。
當發送方收到第16-19字節的 ACK 時,發送窗口向右滑動 4 個字節。 更新的可用窗口可用於隊列中的以下字節(如下圖)。
為了便於理解,我們后續將窗口名使用簡稱,即:
- SND.WND,代表發送窗口
- SND.UNA, 代表Send Unacknowledged指針,指向發送窗口的第一個字節
- SND.NXT, 代表Send Next指針,指向可用窗口的第一個字節
使用簡寫后,如下圖所示:
基於這些定義,我們可以用公式表示可用的窗口大小。
可用窗口(可用窗口)大小 = SND.UNA + SND.WND - SND.NXT
接收端窗口
接收窗口有三種:
- 1、接收並且已經向發送端發送確認ACK
- 2、尚未接收但允發送端發送數據
- 3、尚未接收且不允許發送端發送數據
第二種稱為接收窗口,也稱為RCV.WND。 類似於發送窗口,指針RCV.NXT,代表Receive Next指針,指向接收窗口的第一個字節。
接收窗口不是靜態的。如果服務端性能高,讀取數據快,接收窗口可能會擴大。 否則,它可能會縮小。
接收方通過在TCP段報頭中的窗口字段中指示大小來傳達其接收窗口。 當發送方收到它時,這個窗口大小就成為可用窗口。
發送和接收數據需要時間。 因此,接收窗口不等於特定時刻的可用窗口。
下面,為了更好的理解滑動窗口在TCP中的使用,我們將使用一個簡單的例子進行模擬說明。
示例(大小不變)
我們模擬一個請求和響應,以更好地理解滑動窗口的工作原理。 為了模擬起來簡單,我們盡可能的簡化里面的過程,比如:
- 我們忽略最大段大小 (MSS)。 MSS 因選擇的網絡路由而不同。
- 使接收窗口等於可用窗口,並且在此過程中兩者保持不變。
上圖示例中,有10個步驟。 客戶端請求資源,服務器分三段響應:
- 1、一個 50 字節的包頭
- 2、一個 80 字節的數據1
- 3、一個 100 字節的數據2
每一方都可以同時是發送方和接收方。
我們假設客戶端的發送窗口 (SND.WND) 是 300 字節,接收窗口 (RCV.WND) 是 150 字節。 因此,服務器的 SND.WND 為 150 字節,RCV.WND 為 300 字節。
上圖客戶端的起始狀態。
我們假設它之前已經從服務器接收了300個字節,所以RCV.NXT指向301。由於它還沒有發送任何東西,SND.UNA和SND.NXT都指向1。
可用窗口(可用窗口)大小 = SND.UNA + SND.WND - SND.NXT
根據這個公式,客戶端的可用窗口大小為 1 + 300 - 1 = 300。
這是服務端的起始狀態,鏡像另一端即客戶端的狀態。
因為它已經發送了300個字節,所以SND.UNA和SND.NXT都指向301。
RCV.NXT指向1,因為客戶端尚未發送任何請求。 服務器的可用窗口是301 + 150 - 301 = 150。
現在,我們從步驟1開始:
客戶端發送它的第一個100字節請求。
此刻,窗戶發生了變化。
- 這 100 個字節已發送,但尚未收到 ACK。 因此,SND.NXT 向右滑動 100 個字節。
- 其他指針保持不變。
可用窗口更改為 1 + 300 - 101 = 200。
在第 2 步,我們的焦點轉移到服務器上,從服務端的角度來分析。
- 當服務器收到請求時,RCV.NXT 向右滑動 100 個字節。
- 然后它發送一個帶有 ACK 的 50 字節回復。 這 50 個字節已發送,但尚未發送 ACK,因此 SND.NXT 向右移動 50 個字節。
- SND.UNA不動。
可用窗口大小變為301 + 150 - 351 = 100。
讓我們現在繼續轉向客戶端。
- 當收到50字節的回復時,RCV.NXT向右移動50字節。
- SND.UNA 在收到前一個發送的 100 個字節的 ACK 時向右滑動。
- SND.NXT保持不變,因為客戶端不發送任何數據。
可用窗口更改為101 + 300 - 101 = 300。
再次移動到服務器端。
可用窗口為 100 字節。服務器可以發送 80 字節的段。
- SND.NXT 向右滑動 80 個字節。
- SND.UNA 保持不變,因為最后 50 字節尚未得到確認。
- RCV.NXT 保持不變,因為服務器沒有收到任何數據。
可用窗口更改為 301 + 150 - 431 = 20。
客戶端收到文件的第一部分並立即發送ACK。
- 當客戶端接收到 80 字節的數據時,RCV.NXT 向右移動。
- 其他部分不變。
可用窗口大小仍為300。
此時,服務器在發送 50 字節的回復時收到了第 2 步的 ACK。
- SND.UNA 向右移動 50 個字節。
- 其他部分保持不變。
可用窗口大小變為351 + 150 - 431 = 70。
當服務器發送數據1即80字節部分時,再次收到第4步的另一個ACK。
- SND.UNA 向右移動 80 個字節。
- 其他部分保持不變。
可用窗口大小變為431 + 150 - 431 = 150。
在第 8 步,服務器數據2,大小為100字節。
- SND.NXT向右移動 100 個字節。
- 其他部分保持不變。
可用窗口大小變為431 + 150 - 531 = 50。
繼續轉到客戶端。
- 當客戶端收到 100 字節時,RCV.NXT 向右移動 100 字節。
- 其他部分保持不變。
可用窗口大小保持不變。
最后,服務器收到前一個響應的 ACK。
- SND.UNA向右移動100個字節。
- 其他部分保持不變。
可用窗口大小變為531 + 150 - 531 = 150。
至此,對於滑動窗口不變的示例,講解完畢,那么對於滑動窗口大小變化的呢?在TCP中又是如果實現的呢?
示例(大小變化的窗口)
在前面的示例中,我們假設發送窗口和接收窗口保持不變。 這個假設本身在實際中就是不成立的,因為不存在。
兩個窗口中的字節都存在於操作系統緩沖區中,可以對其進行調整。 例如,當我們的應用程序沒有足夠快地從中讀取字節時,緩沖區中的可用空間就會縮小。
我們來介紹一下這種情況下的窗口變化,看看它是如何影響可用窗口的。
我們簡化了這種情況以將可用窗口集中在客戶端上。 在這個例子中,客戶端始終是發送方,而服務器是接收方。
當服務器發送 ACK 時,它也會在其中包含更新后的窗口大小。
一開始,客戶端發送第一個150字節的請求。
- 這 150 個字節已發送,但尚未發送 ACK。
- 可用窗口縮小到 150 字節。
發送窗口保持在300字節。
當服務器收到請求時,應用程序讀取前 50 個字節,還有 100 個字節仍在緩沖區中,從接收窗口中占用 100 個字節的可用空間。 因此,接收窗口縮小到 200 字節。
接下來,服務器發送帶有更新的 200 字節接收窗口的 ACK。
客戶端收到 ACK 並將其發送窗口大小更新為 200。
此時,可用窗口與發送窗口相同,因為所有 150 個字節都被確認。
客戶端再次發送另一個 200 字節的請求,使用可用窗口中的所有可用空間。
服務器接收到 200 字節后,應用程序仍然運行緩慢,總共只讀取了 70 字節,並在緩沖區中留下了 280 字節。
這會導致接收窗口再次縮小。 現在,我們只剩下 20 個字節了。
在 ACK 消息中,服務器與客戶端共享更新的窗口大小。
同樣,客戶端在收到 ACK 后將其發送窗口更新為 20 字節。 可用窗口也變為 20 字節。
在這種情況下,客戶端停止發送任何大於 20 字節的請求,直到它收到以下消息中的另一個窗口更新。
如果沒有更多來自服務器的消息,我們會被困在 20 字節的可用窗口嗎?
我們不會。 為了避免這種情況,客戶端的 TCP 會定期檢測窗口大小。 一旦釋放更多空間,可用窗口就會擴大,並且可以發送更多數據。
結語
可用窗口的計算是理解TCP滑動窗口的關鍵。
要學習可用窗口的計算,我們需要了解 3 個指針——SND.UNA、SND.NXT 和 RCV.NXT。
假設一個永不改變的窗口大小可以幫助我們了解進度。