TCP 滑動窗口原理解析
一、摘要
前些日子,在分享網絡編程知識文章的時候,有個網友私信給我留言了一條“能不能寫一篇關于 TCP 滑動窗口原理的文章”。
當時沒有立即回復,經過查詢多方資料,發現這個 TCP 真的非常非常的復雜,就像一個清澈的小溝,你以為很淺,結果一腳踩下去,感覺深不可測。
雖然之前也總結過一些關于網絡編程相關的技術知識,對于 TCP 協議棧也做過一些介紹,但是大體上都描述的比較簡單,沒有深入去了解,本篇在很大程度上彌補了我對計算機網絡知識的空白。
話不多說,直接上干貨!
二、TCP 數據傳輸
在之前的文章中我們了解到,TCP 協議能保證網絡上的計算機之間可靠無差錯的數據傳輸,比如上傳文件、下載文件、瀏覽網頁等都得益于它,實際的應用場景非常廣泛。
與 TCP 協議一并稱霸天下的還有 UDP 協議,不過 UDP 協議雖然傳輸效率更高,但是并不保證數據傳輸正確性,相比 TCP 要稍遜一些。
事實上,TCP 協議經過多年的發展,已經成為實現數據可靠傳輸的標準協議,所謂可靠,就是確保數據準確的、不重復、無延遲的到達目的地,那 TCP 協議是如何實現這些特點的呢?
其實要實現數據可靠傳輸,并不簡單,因為要考慮異常的情況比較多,例如數據丟失、數據順序混亂、網絡擁堵等,如果不能解決這些問題,也就無從談起可靠傳輸。
總的來說,TCP 協議是通過序列號、確認應答、重發控制、連接管理以及窗口控制等機制實現數據穩定可靠性的傳輸。
以下是 TCP 協議的報文格式。
圖片
TCP 報文段包括協議首部和數據兩部分,協議首部的固定部分是 20 個字節,頭部是固定部分,后面是選項部分。
下面是報文段首部各個字段的含義:
- 源端口號以及目的端口號:各占 2 個字節,端口是傳輸層和應用層的服務接口,用于尋找發送端和接收端的進程,一般來講,通過端口號和IP地址,可以唯一確定一個 TCP 連接,在網絡編程中,通常被稱為一個 socket 接口。
- 序號:Seq 序號,占 4 個字節。用來標識從 TCP 發送端向 TCP 接收端發送的數據字節流序號,發起方發送數據時對此進行標記。
- 確認序號:Ack 序號,占 4 個字節,包含接受端所期望收到的下一個序號。只有 ACK 標記位為 1 時,確認序號字段才有效,因此,確認序號應該是上次已經成功收到數據字節序號加 1,即 Ack = Seq + 1。
- 數據偏移:占 4 個字節,用于指出 TCP 首部長度。
- 保留字段:占 6 位,暫時可忽略,值全為 0。
- 六位標志位:值內容含義如下
URG(緊急):為1時表明緊急指針字段有效
ACK(確認):為1時表明確認號字段有效
PSH(推送):為1時接收方應盡快將這個報文段交給應用層
RST(復位):為1時表明TCP連接出現故障必須重建連接
SYN(同步):在連接建立時用來同步序號
FIN(終止):為1時表明發送端數據發送完畢要求釋放連接
- 窗口:占 2 個字節,用于流量控制和擁塞控制,表示當前接收緩沖區的大小。
- 校驗和:占 2 個字節,范圍包括首部和數據兩部分
- 緊急指針:指出了緊急數據的末尾在報文段中的位置,和 URG 搭配使用
- 選項和填充:是可選的,默認情況是不選。
計算機之間使用 TCP 協議進行傳輸數據時,每次連接都需要經過 3 個階段:創建連接、數據傳送和釋放連接,即傳輸數據之前,在發送端和接收端建立邏輯連接、然后傳輸數據、最后斷開連接,它保證兩臺計算機之間比較可靠的數據傳輸。
2.1、創建連接
當兩個設備之間準備傳輸數據之前,TCP 會建立連接,創建連接的階段需要三次握手,過程如下:
圖片
詳細過程如下:
- 第一次握手:客戶端向服務器端發出連接請求,等待服務器確認
- 第二次握手:服務器端收到請求后,向客戶端回送一個確認,通知客戶端收到了連接請求
- 第三次握手:客戶端再次向服務器端發送確認信息,確認連接
完成以上 3 次握手之后,可靠性連接建立完成,就可以進行數據傳輸了。
2.2、釋放連接
當數據傳輸完畢之后,TCP 會釋放連接,連接的釋放需要四次揮手,過程如下:
圖片
- 第一次揮手:客戶端向服務器端發出請求切斷連接,等待服務器確認
- 第二次揮手:服務器端收到請求后,向客戶端回送一個確認信息,并同意關閉請求
- 第三次揮手:服務器端再次向客戶端發出請求切斷連接,等待客戶端確認
- 第四次揮手:客戶端收到請求后,向服務器端回送一個確認信息,并同意關閉請求
完成以上 4 次揮手之后,連接釋放完成。
2.3、數據傳輸過程
通過以上的介紹,我們可以描繪出一個簡易版的 TCP 數據傳輸過程,如下圖所示。
圖片
通過序列號與確認應答機制,是 TCP 實現數據可靠傳輸的方式之一,也是最為重要的基石。
但是在復雜的網絡環境下,并不一定能如上圖所描述的那樣順利的進行數據傳輸,例如數據包丟失,針對這種問題,TCP 使用了重傳機制來解決。
三、重傳機制介紹
當網絡不穩定的時候,很容易出現數據包丟失,TCP 采用了哪些重傳手段來解決數據包丟失問題呢?
常見的重傳方式有以下幾種:
- 超時重傳
- 快速重傳
- SACK
- D-SACK
3.1、超時重傳
超時重傳,顧名思義,就是在發送數據時,設定一個定時器,當超過指定的時間后,沒有收到對方的 ACK 確認應答報文,就會重發數據。
TCP 會在以下兩種情況發生超時重傳:
- 發送的數據包丟失
- 確認應答丟失
其中比較關鍵的就是超時重傳時間如何來設定的問題。
我們先來看看正常的數據傳輸過程。
圖片
其中 RTT 指的是數據從網絡一端傳送到另一端所需的時間,也就是數據包發送出去的往返時間。
超時重傳時間,我們以 RTO (Retransmission Timeout 超時重傳時間)來表示。
當超時重傳時間設定過大,會出現什么情況呢?如下圖所示
圖片
當超時重傳時間設定過小,又會出現什么情況呢?如下圖所示
圖片
一路分析下來,可以得出如下結論:
- 當超時重發時間 RTO 設置較大時,會出現數據傳輸效率差的現象,比如數據包丟失之后,需要等很長時間才重發,性能差;
- 當超時重發時間 RTO 設置較小時,可能會出現并沒有丟失包就重發,多次重發會造成網絡擁堵,導致出現更多的超時,更多的超時意味著更多的重發;
因此可以得出一個結論,超時重發時間既不能設置過大,也不能設置過小,必須精準的計算。
以 Linux 操作系統為例,RTO 的計算過程如下!
- 首先對 TCP 數據傳輸所需的往返時間,也就是 RTT 值進行采樣,然后進行加權平均,算出一個平滑 RTT 的值,同時這個值隨著網絡狀態會不斷的變化。
- 除了采樣 RTT 值,還要記錄 RTT 的波動變化,避免 RTT 的變化較大,難以發現
圖片
其中SRTT是計算平滑的RTT ,DevRTR是計算平滑的RTT與最新RTT的差距,在 Linux 下,通常α = 0.125,β = 0.25,μ = 1,? = 4。
實際算出來的超時重傳時間RTO的值應該略大于報文往返RTT的值。
如果超時重發的數據,再次超時的時候,又需要重傳的時候,TCP 的策略是超時間隔加倍。
也就是說,每當遇到一次超時重傳的時候,會將下一次超時時間間隔設為先前值的兩倍,多次超時說明網絡環境差,不宜頻繁反復重發。
3.2、快速重傳
超時重傳雖然能解決數據丟包的問題,但是超時重發時間有時候可能會較長,有沒有一種更快的重傳方式呢?
快速重傳就是來補充超時重傳機制中時間過長的問題。
簡單的說,快速重傳不像超時重傳那樣通過時間來驅動重發,而是通過次數來驅動重發。
當收到報文重復的 ACK 數量,到達一定的閥值(一般為3),TCP 會在定時器過期之前,檢查丟失的報文段并重傳丟失的報文段。
大致的工作方式,可以用如下圖來描述!
圖片
在上圖,發送方向接受方發出了 1、2、3、4、5 份數據,大致執行的過程如下:
- 第一份 Seq1 先送到了,接受方就 Ack 回 2,表示 seq 1 已經收到,準備接受下一個序列號為 2 的包
- Seq2 因為某些原因沒收到,Seq3 到達了,因為 Seq2 缺失,還是 Ack 回 2
- 后面的 Seq4 和 Seq5 都到了,因為 Seq2 沒有收到,還是 Ack 回 2
- 發送端收到了三個 Ack = 2 的確認,知道了 Seq2 還沒有收到,就會在定時器過期之前,重傳丟失的 Seq2
- 最后接收方收到了 Seq2,此時因為 Seq3,Seq4,Seq5 都收到了,于是 Ack 回 6 。
因此,快速重傳的工作方式是當收到相同的 ACK 報文數量到達一個閥值,默認是 3,會在定時器過期之前,重傳丟失的報文段。
快速重傳機制彌補了超時重傳機制中時間過長的問題,但是它依然面臨著另外一個問題,那就是重傳的時候,是重傳之前的一個還是重傳所有的包?
例如上面的例子,是重傳 Seq2 呢?還是重傳 Seq2、Seq3、Seq4、Seq5 呢?
根據 TCP 不同的實現,以上兩種情況都有可能。
3.3、SACK 方法
為了解決不知道該重傳哪些 TCP 報文,天才師們想出來了SACK方法,英文全稱:Selective Acknowledgment,也被稱為選擇性確認。
具體實現就是需要在 TCP 頭部選項字段里加一個 SACK 的東西,接受方可以將緩存的數據地圖發送給發送方,這樣發送方就可以知道哪些數據收到了,哪些數據沒收到,知道了這些信息,就可以只重傳丟失的數據。
如下圖,當發送方收到了三次同樣的 ACK 確認報文,于是就會觸發快速重發機制,通過 SACK 信息發現只有200~299這段數據丟失,會將丟失的片段進行重發,以便提升數據傳輸可靠性效率。
圖片
需要主要的是,如果要支持 SACK 機制,必須發送方和接受方都要支持。在 Linux 操作系統中,開發者可以通過net.ipv4.tcp_sack參數打開這個功能(Linux 2.4 后默認打開)。
3.4、Duplicate SACK 方法
最后再來講講 Duplicate SACK 方法,又稱D-SACK,這個方法實現主要是使用 SACK和ACK來告訴發送方有哪些數據被重復接收了,以防止 TCP 反復的重發。
我們用個案例來介紹D-SACK的作用,例如 ACK 丟包的場景,如下圖!
圖片
過程分析:
- 發送方向成功向接受方發送了兩個數據包,但是接受方發給發送方兩個 ACK 確認應答都丟失了,發送方檢查超時后,重傳第一個數據包(100 ~ 199)
- 接收方發現數據是重復收到的,于是回了一個ACK 300和 SACK 100~199,告訴發送方100~299的數據早已被接收了,因為 ACK 都到300了,因此這個 SACK 可以稱為D-SACK。
- 當發送方知道數據沒有丟,是接收方的 ACK 確認報文丟了,就不會繼續重發數據包了
使用D-SACK方法的好處,可以讓發送方知道,是發出去的包丟了還是接收方回應的 ACK 包丟了,然后來決定是否需要繼續重發包。
在 Linux 操作系統下,可以通過net.ipv4.tcp_dsack參數來開啟/關閉這個功能(Linux 2.4 后默認打開)。
四、滑動窗口介紹
在上文中,我們有介紹到 TCP 協議的數據傳輸機制,當兩臺計算機之間建立連接之后,就可以進行傳輸數據了,TCP 每發送一個數據,都要進行一次確認應答,當上一個數據包收到了應答了, 再發送下一個,從而保證數據的可靠傳輸。
圖片
這種傳輸方式,雖然可靠但是缺點也比較明顯,傳輸數據的效率非常的低下,好比你現在跟某個人打電話,你說了一句話,只有等到對方回復了你,你才能說下一句,這顯然不現實。
為解決這個問題,TCP 引入了滑動窗口,可以一次性向窗口中發送多個數據包并不需要依次等待接受方的確認應答,即使在往返時間較長的情況下,它也不會降低數據傳輸效率。
那什么是滑動窗口呢?我們以高速路的收費站為例,做一個類比介紹。
上過高速的同學應該都知道,在高速路上有一個入口收費站和一個出口收費站。TCP 也是一樣的,除了入口有發送方滑動窗口,出口處也設立有接收方滑動窗口。
圖片
對于發送方滑動窗口,我們可以把數據包看成車輛,分類它們的狀態:
- 還未進入入口收費站的車輛:對應的是上圖Not Sent,Recipient Not Ready to Receive部分,這些屬于發送端未發送,同時接收端也未準備接收的數據
- 已進入收費站但未進入高速路的車輛:對應的是上圖Not Sent,Recipient Ready to Receive部分,這些屬于發送端未發送,但已經告知接收方的數據,其實已經在窗口中(發送端緩存)了,等待發送。
- 在高速公路上行駛的車輛:對應的是上圖Send But Not Yet Acknowledged部分,這些屬于發送端已發送出去,等到接收方接受的數據,屬于窗口內的數據。
- 到達出口收費站的車輛:對應的是上圖Sent and Acknowledged部分,這些屬于已經發送成功并已經被接受的數據,這些數據已經離開窗口了。
同樣,對于接受方滑動窗口,我們也可以把數據包看成車輛,分類它們的狀態:
- 還未到達出口收費站的車輛:狀態為Not Received,表示還沒有被接收的數據。
- 到達出口收費站但未完成繳費的車輛:狀態為Received Not ACK,表示已經被接受但是還沒有回復 ACK
- 繳完費并離開出口收費站的車輛:狀態為Received and ACK,表示已經被接受并回復了 ACK
通過以上的描述,相信大家對滑動窗口已經有了初步的認識,在整個數據傳輸過程中,光線傳輸類似于高速公路,滑動窗口類似于收費站,通過收費站可以做到對車輛進行適當的流量控制,以防止高速公路出現擁堵,滑動窗口也有同樣的作用。
4.1、發送方的滑動窗口
下圖就是發送方的滑動窗口樣例圖,根據處理的情況分成四個部分,其中深藍色方框是發送窗口,紫色方框是可用窗口。
圖片
含義解釋:
- #1表示已發送并收到 ACK 確認的數據:1~31 字節
- #2表示已發送但未收到 ACK 確認的數據:32~45 字節
- #3表示未發送但總大小在接收方處理范圍內:46~51字節
- #4表示未發送但總大小超過接收方處理范圍:52 字節以后
當發送方把數據全部都一下發送出去后,可用窗口的大小就為 0 了,表明可用窗口耗盡,在沒收到接受方 ACK 確認之前是無法繼續發送數據的。
圖片
當收到之前發送的數據32~36字節的 ACK 確認應答后,如果發送窗口的大小沒有變化,則滑動窗口往右邊移動 5 個字節,因為有 5 個字節的數據被應答確認,接下來52~56字節又變成了可用窗口,那么后續也就可以發送52~56這 5 個字節的數據了。
圖片
程序是如何精準的控制發送方的窗口數據呢?
TCP 滑動窗口方案使用三個指針來跟蹤在四個傳輸類別中的每一個類別中的字節。其中兩個指針是絕對指針(指特定的序列號),一個是相對指針(需要做偏移)。
圖片
含義解釋:
- SND.WND:表示發送窗口的大小(大小是由接收方指定的)
- SND.UNA:是一個絕對指針,它指向的是已發送但未收到確認的第一個字節的序列號,也就是#2的第一個字節
- SND.NXT:也是一個絕對指針,它指向未發送但可發送范圍的第一個字節的序列號,也就是#3的第一個字節
- 可用窗口大小:是一個相對指針,通過SND.WND - (SND.NXT - SND.UNA)公式計算得來
4.2、接受方的滑動窗口
接下來我們看看接收方的滑動窗口,接收窗口相對簡單一些,根據處理的情況劃分成三個部分。
圖片
含義解釋:
- #1和#2表示已成功接收并確認的數據,等待應用進程讀取
- #3表示未收到數據但可以接收的數據
- #4表示未收到數據并不可以接收的數據
其中三個接收部分,使用兩個指針進行劃分:
- RCV.WND:表示接收窗口的大小,它會通告給發送方
- RCV.NXT:是一個絕對指針,它指向期望從發送方發送來的下一個數據字節的序列號,也就是#3的第一個字節
- 可接受數據的最大值位置:它可以通過RCV.NXT + RCV.WND計算得出,也就是#4的第一個字節
五、小結
相比傳統的發送一個包,然后等待確認應答再發送包的數據傳輸模型,滑動窗口這種一次性批量發包然后等待確認應答的傳輸方式,可以顯著的提升數據傳輸效率,整個傳輸過程可以用如下圖來描述。
圖片
上圖中的 ACK 600 確認應答報文丟失,也不會影響數據傳輸,因為可以通過下一個確認應答進行確認,只要發送方收到了 ACK 700 確認應答,就表示 700 之前的所有數據接收方都收到了,這種確認應答模式叫累計確認或者累計應答。
在上文中,我們提到滑動窗口有一個很關鍵字的參數,就是窗口大小。
通常,窗口的大小是由接收方來決定的,接收端告訴發送端自己還有多少緩沖區可以接收數據,防止發送的數據量過大接受方處理不過來,會觸發發送方重發機制,從而導致網絡流量的無端的浪費。
通過控制窗口大小,可以避免發送方的數據超過接收方的可用窗口,也就是大家常說的流量控制。
除此之外,計算機網絡都處在一個共享的環境,難免會出現網絡擁堵的現象。當網絡出現擁堵時,流量控制的手段非常有限。
如果網絡出現擁堵時,發送方繼續發送大量數據包,可能會導致數據包時延、丟失等,這時 TCP 就會重傳數據,重傳就會導致網絡的負擔更重,于是會導致更大的延遲以及更多的丟包,此時可能會進入惡性循環….
因此,TCP 不能忽略網絡上發生的事,當網絡發生擁塞時,TCP 需要降低發送的數據量,避免發送方的數據填滿整個網絡,我們把這一行為稱為擁塞控制。
關于流量控制和擁塞控制的實現,鑒于文章篇幅過長,我們會在下篇文章中進行詳解。
本文整理了一些優秀網友分享的知識,在此特別感謝作者小林coding的圖解 tcp 滑動窗口文章分享,給予了很大的知識幫助,同時結合自己的理解比較全面的探討了 TCP 滑動窗口的原理,希望對大家有所幫助。