理解TCP滑動窗口是如何工作的,對于理解TCP的其他知識是至關重要的。
相比于更為簡單,同為傳輸層協議的UDP而言,TCP提供了對傳輸數據的質量保證。
在可靠性上,TCP確保傳輸的數據不丟失、不重復,也不會產生亂序。
同時,TCP還提供了流量控制,用于控制數據發送的速度,防止較快主機導致較慢主機的緩沖區溢出。
接下來,從不可靠的協議開始說起,看看TCP面向流的滑動窗口機制是如何演化而來的。
不可靠的協議
對于不可靠的協議(例如IP、UDP),當數據發出去之后,可能到達目的地,也可能丟失。
如果沒有提供消息反饋的機制,那么發送方將無法獲知數據是否已成功的送達目的地,因而也無法對丟失的數據進行重傳。
沒有消息反饋機制,是造成傳輸不可靠,沒有流控的一個主要原因。
支持重傳的確認機制
為了能夠在一個不可靠協議的基礎之上,進行可靠的數據傳輸,引入了消息反饋機制。
消息反饋
發送方發送一條信息,當接收方收到數據之后,給發送方返回一個接收成功的反饋信息。
簡單的實現流程如下:
- 主機A給主機B發送一條消息;
- 主機B收到后,給主機A發送一條確認消息,告訴A消息已經被成功的接收了;
- 主機A在收到B的確認消息之后,就知道該消息已發送成功。
但是這個簡單的機制,可能存在2個問題:
- 由于IP協議是不可靠的,實際上消息可能沒有到達目的地。這樣,主機A將會處于等待消息確認的狀態,可能永遠也收不到確認。
- 另一方面,也有可能主機B在收到了從A發送過來的消息之后,返回了確認消息,但這個消息卻意外的丟失了。
以上2種情況,都有可能導致主機A一直處于等待確認消息的狀態,但卻永遠也等不到。
超時重傳
為了解決這個問題,當主機A發送一條消息之后,可以同時啟動一個計時器。這個計時器的時間要足夠長,讓消息能夠送達主機B,同時讓確認消息也能夠返回給主機A。
如果在確認消息到達主機A之前,計時器超時了。那么,主機A會認為發送消息的過程中遇到了問題(可能是丟失了,或者網絡堵塞導致包延遲),于是就重傳該消息。
由于這種對消息的確認機制,既包含了對消息已收到的確認應答,也包含了發生超時后對消息的重傳,因此被稱為支持重傳的確認機制(PAR,Positive Acknowledgment with Retransmission)。
基于PAR機制,能夠給數據傳輸提供基本的可靠性保證。
但PAR機制存在一個缺點,就是在任何時候,只能有一條消息是未被確認的。這會使得系統變得非常慢,因為后續消息的發送都得等待前一條消息的確認。
消息標識和發送限制
PAR機制比較適用于傳輸少量數據,或者交互不是很頻繁的協議。由于PAR傳輸效率低下,因此也不適用于像TCP這樣的協議。
為了解決傳輸效率低下的問題,引入了消息標識。
消息標識
當發送方發送消息的時候,為這個消息增加一個唯一標識。接收方在收到消息之后,返回的確認消息也帶上該消息標識。
這樣,發送方就可以針對不同的消息使用不同的標識。發送方可以同時發送多條消息,在收到確認消息之后,只要根據消息標識進行匹配,確認對應的消息即可。
以上,只是從發送方的角度,解決了發送方的發送速率問題(在任何時候,只能有一條消息是未被確認的)。
發送限制
我們再換個角度,原來是發送一個消息,接收方處理完,返回確認消息后,等待下一條消息。這個過程,對于接收方來說,并不會存在什么問題。
在引入消息標識之后,發送方可以同時發送多條消息了。但如果發送過快,接收方可能由于處理不過來,被壓垮了。
因此,對于接收方來說,需要保證在自己能夠正常接收處理數據的情況下,通過一種機制,來通知發送方盡量不要發送得太快,發送的數據盡量按照自己的處理能力來。
比較容易想到的,就是在接收方返回確認消息的時候,將接收方的發送限制信息帶上,傳遞給發送方。
發送方在收到確認消息中的發送限制字段時,就可以按照接收方的要求來控制自己的發送速率。
有了這個發送限制,接收方就可以根據自己的實際負載情況,進行動態反饋以調整發送速率,最優化發送性能。
增加了消息標識和發送限制之后,可以認為是PAR機制的一個增強版。
對于數據的傳輸,現在可以提供可靠性、效率和基本的數據流控了。
TCP面向流的滑動窗口確認
在增加了消息標識和發送限制之后,整個消息的發送和確認過程,已經較為完善了。
但從傳輸的"數據"的角度來看,前面我們講的,其實都是面向消息的。也就是說,發送和確認都是以消息為單位。
而實際上,TCP是面向字節流的,本身并沒有消息的概念。TCP傳輸的數據,我們通常稱之為報文段。具體如何識別并組裝為一條完整的消息,是應用層的工作。
正因為如此,TCP是以字節為單位來處理數據流的。如果要對TCP傳輸的數據做標識,那就需要一個字節給一個標識。
這樣一來,TCP每次就只能發送一個字節的數據,然后每次也只能確認一個字節的數據,這顯然是不合理的。既浪費帶寬,又降低了發送效率。
TCP的設計者確實也沒有這么做,而是將數據劃分為一個個數據段,每次可以發送一段數據。
對于每一段數據,使用一個序號來進行標識。每次確認數據的時候,也是確認一段數據而不是一個字節。
事實上,序號是報文段中數據字節的偏移量,表示數據字節的位置。
TCP緩沖區
設想一個場景,主機A和主機B建立了一個新的連接:
- 主機A有一個很大的數據字節流需要傳輸,但是主機B沒有辦法全部接收;
- 所以主機B限制主機A報文段能夠傳輸的字節數大小,直到已發送的字節被確認;
- 主機B允許主機A發送更多的字節。
所以,每臺主機都需要跟蹤哪些字節已經發送了,哪些字節還沒有發送,哪些已經確認了。這個流程,和前面以消息為單位的發送限制機制很類似。
為此,TCP引入了緩沖區的概念,將發送方的緩沖區,劃分為四個部分。
發送方的緩沖區
如圖所示,發送方的緩沖區,被劃分為四個部分,具體說明如下。
第一部分:已發送,并且已確認的字節
(Bytes Sent And Acknowledged)
對應于圖中的 Category #1 部分,從第1到第31個字節,共31字節。
第二部分:已發送,但尚未確認的字節
(Bytes Sent But Not Yet Acknowledged)
對應于圖中的 Category #2 部分,從第32到第45個字節,共14字節。
第三部分:尚未發送,但可以立即發送的字節
(Bytes Not Yet Sent For Which Recipient Is Ready)
對應于圖中的 Category #3 部分,從第46到第51個字節,共6字節。
這部分,代表發送方當前還可以發送的字節數,也是接收方當前還可以接收的字節數。
第四部分:尚未發送,并且還不能發送的字節
(Bytes Not Yet Sent For Which Recipient Is Not Ready)
對應于圖中的 Category #4 部分,從第52到第95個字節,共44字節。
這部分字節,已經超出了接收方可以接收的最大窗口大小,因此暫不能發送。
發送窗口(Send Window)
黑色方框部分,表示發送窗口(Send Window),通常也簡單的稱為窗口。
發送窗口由兩部分組成,對應于發送緩沖區的第二部分(Category #2,灰色方框內)和第三部分(Category #3,紅色方框內)。
如圖所示,發送窗口大小為20字節,第二、三部分分別為14字節和6字節。
窗口的左邊緣為窗口的第1個字節(第32字節),右邊緣為窗口的最后一個字節(第51字節)。
窗口的左邊緣位置,決定了窗口的右邊緣位置。左邊緣是第一和第二部分的分界線,右邊緣是第三和第四部分的分界線。
發送窗口的大小,決定了發送方可以同時發送的字節數,即未被接收方確認的字節數。
可用窗口(Usable Window)
可用窗口(Usable Window),對應于緩沖區的第三部分(Category #3),即發送方當前仍可以發送的字節數。
發送消息之后窗口的變化
如果在可用窗口內,發送了一些數據,將會改變發送窗口的位置,以及各部分的大小。
在上面的例子中,發送方的可用窗口為6字節。如果發送方將6個字節的數據發送出去,那么這6個字節將會變成未確認狀態,合并到第二部分。
第二部分的范圍變為,從第32到第51個字節,共20個字節。
而可用窗口,即窗口的第三部分,將變為0。這通常被稱為零窗口(Zero Window)。
發送方需要等到有新的確認包到達,才會有可用的窗口可以繼續發送數據。
確認消息之后窗口的變化
接收方成功接收了數據之后,會給發送方返回一個確認包。在確認包中,包含了一個確認序號,用于通知發送方,在這個序號之前的數據都已經成功接收了。(確認序號的主要作用,是用來解決不丟包問題。通知接收端已經接收了哪些數據。)
假設,已發送未確認部分(第32到第45字節),是由4個TCP報文段進行傳輸的。每個報文段的范圍分別為:
- 第一段為32~34字節;
- 第二段為35~36字節;
- 第三段為37~41字節;
- 第四段為42~45字節。
假設第一段、第二段和第四段,都已經成功的發送給接收方了。但是,第三段還沒有送達。
那么,接收方將只能確認第一段和第二段,即第32到第36字節已經接收成功了。而對于第四段,還不能進行確認。
這是因為,TCP采用的是累積確認的機制,確認序號表示在這個序號之前(確認序號值減去1)的數據都已經被成功接收。
由于第三段接收方還沒有收到,因此不能越過這部分,去確認第四段。否則,發送方會認為第三段也已經被成功的接收了。
(事實上,現在的TCP支持SACK機制,如果啟用了,可以支持確認非連續的數據塊。)
發送方在接收到確認包之后,窗口將會向右移動5個字節(第32到36共確認5個字節)。而第32到36字節,將會合并到第一部分,變為已發送已確認狀態。
由于確認了5個字節,窗口右移之后,又新建了一個5個字節的可用窗口(第52到56字節)。
這個過程會在每次發送方接收到確認包時發生,然后窗口會進行相應的移動,以調整緩沖區各部分的大小。(滑動窗口很形象)
有了滑動窗口機制,就可以使用一個確認序號來確認一整段數據。這為TCP面向字節流的服務,提供了可靠性支持,并且不需要為每一個序號的確認而耗費時間。
丟失確認之后窗口的變化
發送方在發送了第52到56字節的數據之后,就停下來不再發送數據了。因為被卡在了第37到41字節上,這個范圍的字節仍然沒有返回確認包。
當然,和PAR機制類似的,TCP也支持超時檢測和重傳數據包。
因此,在計時器超時之后,發送方將重發已丟失的報文段,并希望這一次能夠到達目的地。
非常不幸的是,TCP的累積確認機制,仍存在一個缺點,它并不會獨立的確認每個報文段。
這意味著,TCP有可能會重復發送那些實際已經被接收方成功接收的報文段。
在我們的例子中,第四段(第42到45字節)已經被成功接收。但只要第三段沒有被確認,當超時進行重發時,第四段也會被重發。