轉http://coolshell.cn/articles/11609.html
滑動窗口 -- 表征發送端和接收端的接收能力
擁塞窗口-- 表征中間設備的傳輸能力
TCP滑動窗口
需要說明一下,如果你不了解TCP的滑動窗口這個事,你等於不了解TCP協議。我們都知道,TCP必需要解決的可靠傳輸以及包亂序(reordering)的問題,所以,TCP必需要知道網絡實際的數據處理帶寬或是數據處理速度,這樣才不會引起網絡擁塞,導致丟包。
所以,TCP引入了一些技術和設計來做網絡流控,Sliding Window是其中一個技術。 前面我們說過,TCP頭里有一個字段叫Window,又叫Advertised-Window,這個字段是接收端告訴發送端自己還有多少緩沖區可以接收數據。於是發送端就可以根據這個接收端的處理能力來發送數據,而不會導致接收端處理不過來。 為了說明滑動窗口,我們需要先看一下TCP緩沖區的一些數據結構:

上圖中,我們可以看到:
- 接收端LastByteRead指向了TCP緩沖區中讀到的位置,NextByteExpected指向的地方是收到的連續包的最后一個位置,LastByteRcved指向的是收到的包的最后一個位置,我們可以看到中間有些數據還沒有到達,所以有數據空白區。
- 發送端的LastByteAcked指向了被接收端Ack過的位置(表示成功發送確認),LastByteSent表示發出去了,但還沒有收到成功確認的Ack,LastByteWritten指向的是上層應用正在寫的地方。
於是:
- 接收端在給發送端回ACK中會匯報自己的AdvertisedWindow = MaxRcvBuffer – LastByteRcvd – 1;
- 而發送方會根據這個窗口來控制發送數據的大小,以保證接收方可以處理。
下面我們來看一下發送方的滑動窗口示意圖:

(圖片來源)
上圖中分成了四個部分,分別是:(其中那個黑模型就是滑動窗口)
- #1已收到ack確認的數據。
- #2發還沒收到ack的。
- #3在窗口中還沒有發出的(接收方還有空間)。
- #4窗口以外的數據(接收方沒空間)
下面是個滑動后的示意圖(收到36的ack,並發出了46-51的字節):

下面我們來看一個接受端控制發送端的圖示:

TCP的擁塞處理 – Congestion Handling
上面我們知道了,TCP通過Sliding Window來做流控(Flow Control),但是TCP覺得這還不夠,因為Sliding Window需要依賴於連接的發送端和接收端,其並不知道網絡中間發生了什么。TCP的設計者覺得,一個偉大而牛逼的協議僅僅做到流控並不夠,因為流控只是網絡模型4層以上的事,TCP的還應該更聰明地知道整個網絡上的事。
具體一點,我們知道TCP通過一個timer采樣了RTT並計算RTO,但是,如果網絡上的延時突然增加,那么,TCP對這個事做出的應對只有重傳數據,但是,重傳會導致網絡的負擔更重,於是會導致更大的延遲以及更多的丟包,於是,這個情況就會進入惡性循環被不斷地放大。試想一下,如果一個網絡內有成千上萬的TCP連接都這么行事,那么馬上就會形成“網絡風暴”,TCP這個協議就會拖垮整個網絡。這是一個災難。
所以,TCP不能忽略網絡上發生的事情,而無腦地一個勁地重發數據,對網絡造成更大的傷害。對此TCP的設計理念是:TCP不是一個自私的協議,當擁塞發生的時候,要做自我犧牲。就像交通阻塞一樣,每個車都應該把路讓出來,而不要再去搶路了。
關於擁塞控制的論文請參看《Congestion Avoidance and Control》(PDF)
擁塞控制主要是四個算法:1)慢啟動,2)擁塞避免,3)擁塞發生,4)快速恢復。這四個算法不是一天都搞出來的,這個四算法的發展經歷了很多時間,到今天都還在優化中。 備注:
- 1988年,TCP-Tahoe 提出了1)慢啟動,2)擁塞避免,3)擁塞發生時的快速重傳
- 1990年,TCP Reno 在Tahoe的基礎上增加了4)快速恢復
慢熱啟動算法 – Slow Start
首先,我們來看一下TCP的慢熱啟動。慢啟動的意思是,剛剛加入網絡的連接,一點一點地提速,不要一上來就像那些特權車一樣霸道地把路占滿。新同學上高速還是要慢一點,不要把已經在高速上的秩序給搞亂了。
慢啟動的算法如下(cwnd全稱Congestion Window):
1)連接建好的開始先初始化cwnd = 1,表明可以傳一個MSS大小的數據。
2)每當收到一個ACK,cwnd++; 呈線性上升
3)每當過了一個RTT,cwnd = cwnd*2; 呈指數讓升
4)還有一個ssthresh(slow start threshold),是一個上限,當cwnd >= ssthresh時,就會進入“擁塞避免算法”(后面會說這個算法)
所以,我們可以看到,如果網速很快的話,ACK也會返回得快,RTT也會短,那么,這個慢啟動就一點也不慢。下圖說明了這個過程。

這里,我需要提一下的是一篇Google的論文《An Argument for Increasing TCP’s Initial Congestion Window》Linux 3.0后采用了這篇論文的建議——把cwnd 初始化成了 10個MSS。 而Linux 3.0以前,比如2.6,Linux采用了RFC3390,cwnd是跟MSS的值來變的,如果MSS< 1095,則cwnd = 4;如果MSS>2190,則cwnd=2;其它情況下,則是3。
擁塞避免算法 – Congestion Avoidance
前面說過,還有一個ssthresh(slow start threshold),是一個上限,當cwnd >= ssthresh時,就會進入“擁塞避免算法”。一般來說ssthresh的值是65535,單位是字節,當cwnd達到這個值時后,算法如下:
1)收到一個ACK時,cwnd = cwnd + 1/cwnd
2)當每過一個RTT時,cwnd = cwnd + 1
這樣就可以避免增長過快導致網絡擁塞,慢慢的增加調整到網絡的最佳值。很明顯,是一個線性上升的算法。
擁塞狀態時的算法
前面我們說過,當丟包的時候,會有兩種情況:
1)等到RTO超時,重傳數據包。TCP認為這種情況太糟糕,反應也很強烈。
- sshthresh = cwnd /2
- cwnd 重置為 1
- 進入慢啟動過程
2)Fast Retransmit算法,也就是在收到3個duplicate ACK時就開啟重傳,而不用等到RTO超時。
- TCP Tahoe的實現和RTO超時一樣。
- TCP Reno的實現是:
- cwnd = cwnd /2
- sshthresh = cwnd
- 進入快速恢復算法——Fast Recovery
上面我們可以看到RTO超時后,sshthresh會變成cwnd的一半,這意味着,如果cwnd<=sshthresh時出現的丟包,那么TCP的sshthresh就會減了一半,然后等cwnd又很快地以指數級增漲爬到這個地方時,就會成慢慢的線性增漲。我們可以看到,TCP是怎么通過這種強烈地震盪快速而小心得找到網站流量的平衡點的。
快速恢復算法 – Fast Recovery
TCP Reno
這個算法定義在RFC5681。快速重傳和快速恢復算法一般同時使用。快速恢復算法是認為,你還有3個Duplicated Acks說明網絡也不那么糟糕,所以沒有必要像RTO超時那么強烈。 注意,正如前面所說,進入Fast Recovery之前,cwnd 和 sshthresh已被更新:
- cwnd = cwnd /2
- sshthresh = cwnd
然后,真正的Fast Recovery算法如下:
- cwnd = sshthresh + 3 * MSS (3的意思是確認有3個數據包被收到了)
- 重傳Duplicated ACKs指定的數據包
- 如果再收到 duplicated Acks,那么cwnd = cwnd +1
- 如果收到了新的Ack,那么,cwnd = sshthresh ,然后就進入了擁塞避免的算法了。
如果你仔細思考一下上面的這個算法,你就會知道,上面這個算法也有問題,那就是——它依賴於3個重復的Acks。注意,3個重復的Acks並不代表只丟了一個數據包,很有可能是丟了好多包。但這個算法只會重傳一個,而剩下的那些包只能等到RTO超時,於是,進入了惡夢模式——超時一個窗口就減半一下,多個超時會超成TCP的傳輸速度呈級數下降,而且也不會觸發Fast Recovery算法了。
通常來說,正如我們前面所說的,SACK或D-SACK的方法可以讓Fast Recovery或Sender在做決定時更聰明一些,但是並不是所有的TCP的實現都支持SACK(SACK需要兩端都支持),所以,需要一個沒有SACK的解決方案。而通過SACK進行擁塞控制的算法是FACK(后面會講)
TCP New Reno
於是,1995年,TCP New Reno(參見 RFC 6582 )算法提出來,主要就是在沒有SACK的支持下改進Fast Recovery算法的——
- 當sender這邊收到了3個Duplicated Acks,進入Fast Retransimit模式,開發重傳重復Acks指示的那個包。如果只有這一個包丟了,那么,重傳這個包后回來的Ack會把整個已經被sender傳輸出去的數據ack回來。如果沒有的話,說明有多個包丟了。我們叫這個ACK為Partial ACK。
- 一旦Sender這邊發現了Partial ACK出現,那么,sender就可以推理出來有多個包被丟了,於是乎繼續重傳sliding window里未被ack的第一個包。直到再也收不到了Partial Ack,才真正結束Fast Recovery這個過程
我們可以看到,這個“Fast Recovery的變更”是一個非常激進的玩法,他同時延長了Fast Retransmit和Fast Recovery的過程。
算法示意圖
下面我們來看一個簡單的圖示以同時看一下上面的各種算法的樣子:

