化妝品網(wǎng)站建設(shè)發(fā)帖秒收錄的網(wǎng)站
TCP詳解之流量控制
發(fā)送方不能無(wú)腦的發(fā)數(shù)據(jù)給接收方,要考慮接收方處理能力。
如果一直無(wú)腦的發(fā)數(shù)據(jù)給對(duì)方,但對(duì)方處理不過(guò)來(lái),那么就會(huì)導(dǎo)致觸發(fā)重發(fā)機(jī)制,從而導(dǎo)致網(wǎng)絡(luò)流量的無(wú)端的浪費(fèi)。
為了解決這種現(xiàn)象發(fā)生,TCP 提供一種機(jī)制可以讓「發(fā)送方」根據(jù)「接收方」的實(shí)際接收能力控制發(fā)送的數(shù)據(jù)量,這就是所謂的流量控制。
下面舉個(gè)栗子,為了簡(jiǎn)單起見(jiàn),假設(shè)以下場(chǎng)景:
- 客戶端是接收方,服務(wù)端是發(fā)送方
- 假設(shè)接收窗口和發(fā)送窗口相同,都為
200
- 假設(shè)兩個(gè)設(shè)備在整個(gè)傳輸過(guò)程中都保持相同的窗口大小,不受外界影響
根據(jù)上圖的流量控制,說(shuō)明下每個(gè)過(guò)程:
- 客戶端向服務(wù)端發(fā)送請(qǐng)求數(shù)據(jù)報(bào)文。這里要說(shuō)明下,本次例子是把服務(wù)端作為發(fā)送方,所以沒(méi)有畫(huà)出服務(wù)端的接收窗口。
- 服務(wù)端收到請(qǐng)求報(bào)文后,發(fā)送確認(rèn)報(bào)文和 80 字節(jié)的數(shù)據(jù),于是可用窗口
Usable
減少為 120 字節(jié),同時(shí)SND.NXT
指針也向右偏移 80 字節(jié)后,指向 321,這意味著下次發(fā)送數(shù)據(jù)的時(shí)候,序列號(hào)是 321 - 客戶端收到 80 字節(jié)數(shù)據(jù)后,于是接收窗口往右移動(dòng) 80 字節(jié),
RCV.NXT
也就指向 321,這意味著客戶端期望的下一個(gè)報(bào)文的序列號(hào)是 321,接著發(fā)送確認(rèn)報(bào)文給服務(wù)端。 - 服務(wù)端再次發(fā)送了 120 字節(jié)數(shù)據(jù),于是可用窗口耗盡為 0,服務(wù)端無(wú)法再繼續(xù)發(fā)送數(shù)據(jù)。
- 客戶端收到 120 字節(jié)的數(shù)據(jù)后,于是接收窗口往右移動(dòng) 120 字節(jié),
RCV.NXT
也就指向 441,接著發(fā)送確認(rèn)報(bào)文給服務(wù)端。 - 服務(wù)端收到對(duì) 80 字節(jié)數(shù)據(jù)的確認(rèn)報(bào)文后,
SND.UNA
指針往右偏移后指向 321,于是可用窗口Usable
增大到 80。 - 服務(wù)端收到對(duì) 120 字節(jié)數(shù)據(jù)的確認(rèn)報(bào)文后,
SND.UNA
指針往右偏移后指向 441,于是可用窗口Usable
增大到 200。 - 服務(wù)端可以繼續(xù)發(fā)送了,于是發(fā)送了 160 字節(jié)的數(shù)據(jù)后,
SND.NXT
指向 601,于是可用窗口Usable
減少到 40。 - 客戶端收到 160 字節(jié)后,接收窗口往右移動(dòng)了 160 字節(jié),
RCV.NXT
也就是指向了 601,接著發(fā)送確認(rèn)報(bào)文給服務(wù)端。 - 服務(wù)端收到對(duì) 160 字節(jié)數(shù)據(jù)的確認(rèn)報(bào)文后,發(fā)送窗口往右移動(dòng)了 160 字節(jié),于是
SND.UNA
指針偏移了 160 后指向 601,可用窗口Usable
也就增大至了 200。
操作系統(tǒng)緩沖區(qū)與滑動(dòng)窗口的關(guān)系
前面的流量控制例子,我們假定了發(fā)送窗口和接收窗口是不變的,但是實(shí)際上,發(fā)送窗口和接收窗口中所存放的字節(jié)數(shù),都是放在操作系統(tǒng)內(nèi)存緩沖區(qū)中的,而操作系統(tǒng)的緩沖區(qū),會(huì)被操作系統(tǒng)調(diào)整。
當(dāng)應(yīng)用進(jìn)程沒(méi)辦法及時(shí)讀取緩沖區(qū)的內(nèi)容時(shí),也會(huì)對(duì)我們的緩沖區(qū)造成影響。
那操作系統(tǒng)的緩沖區(qū),是如何影響發(fā)送窗口和接收窗口的呢?
我們先來(lái)看看第一個(gè)例子。
當(dāng)應(yīng)用程序沒(méi)有及時(shí)讀取緩存時(shí),發(fā)送窗口和接收窗口的變化。
考慮以下場(chǎng)景:
- 客戶端作為發(fā)送方,服務(wù)端作為接收方,發(fā)送窗口和接收窗口初始大小為
360
; - 服務(wù)端非常的繁忙,當(dāng)收到客戶端的數(shù)據(jù)時(shí),應(yīng)用層不能及時(shí)讀取數(shù)據(jù)。
根據(jù)上圖的流量控制,說(shuō)明下每個(gè)過(guò)程:
- 客戶端發(fā)送 140 字節(jié)數(shù)據(jù)后,可用窗口變?yōu)?220 (360 - 140)。
- 服務(wù)端收到 140 字節(jié)數(shù)據(jù),但是服務(wù)端非常繁忙,應(yīng)用進(jìn)程只讀取了 40 個(gè)字節(jié),還有 100 字節(jié)占用著緩沖區(qū),于是接收窗口收縮到了 260 (360 - 100),最后發(fā)送確認(rèn)信息時(shí),將窗口大小通告給客戶端。
- 客戶端收到確認(rèn)和窗口通告報(bào)文后,發(fā)送窗口減少為 260。
- 客戶端發(fā)送 180 字節(jié)數(shù)據(jù),此時(shí)可用窗口減少到 80。
- 服務(wù)端收到 180 字節(jié)數(shù)據(jù),但是應(yīng)用程序沒(méi)有讀取任何數(shù)據(jù),這 180 字節(jié)直接就留在了緩沖區(qū),于是接收窗口收縮到了 80 (260 - 180),并在發(fā)送確認(rèn)信息時(shí),通過(guò)窗口大小給客戶端。
- 客戶端收到確認(rèn)和窗口通告報(bào)文后,發(fā)送窗口減少為 80。
- 客戶端發(fā)送 80 字節(jié)數(shù)據(jù)后,可用窗口耗盡。
- 服務(wù)端收到 80 字節(jié)數(shù)據(jù),但是應(yīng)用程序依然沒(méi)有讀取任何數(shù)據(jù),這 80 字節(jié)留在了緩沖區(qū),于是接收窗口收縮到了 0,并在發(fā)送確認(rèn)信息時(shí),通過(guò)窗口大小給客戶端。
- 客戶端收到確認(rèn)和窗口通告報(bào)文后,發(fā)送窗口減少為 0。
可見(jiàn)最后窗口都收縮為 0 了,也就是發(fā)生了窗口關(guān)閉。當(dāng)發(fā)送方可用窗口變?yōu)?0 時(shí),發(fā)送方實(shí)際上會(huì)定時(shí)發(fā)送窗口探測(cè)報(bào)文,以便知道接收方的窗口是否發(fā)生了改變,這個(gè)內(nèi)容后面會(huì)說(shuō),這里先簡(jiǎn)單提一下。
我們先來(lái)看看第二個(gè)例子。
當(dāng)服務(wù)端系統(tǒng)資源非常緊張的時(shí)候,操作系統(tǒng)可能會(huì)直接減少了接收緩沖區(qū)大小,這時(shí)應(yīng)用程序又無(wú)法及時(shí)讀取緩存數(shù)據(jù),那么這時(shí)候就有嚴(yán)重的事情發(fā)生了,會(huì)出現(xiàn)數(shù)據(jù)包丟失的現(xiàn)象。
說(shuō)明下每個(gè)過(guò)程:
- 客戶端發(fā)送 140 字節(jié)的數(shù)據(jù),于是可用窗口減少到了 220。
- 服務(wù)端因?yàn)楝F(xiàn)在非常的繁忙,操作系統(tǒng)于是就把接收緩存減少了 120 字節(jié),當(dāng)收到 140 字節(jié)數(shù)據(jù)后,又因?yàn)閼?yīng)用程序沒(méi)有讀取任何數(shù)據(jù),所以 140 字節(jié)留在了緩沖區(qū)中,于是接收窗口大小從 360 收縮成了 100,最后發(fā)送確認(rèn)信息時(shí),通告窗口大小給對(duì)方。
- 此時(shí)客戶端因?yàn)檫€沒(méi)有收到服務(wù)端的通告窗口報(bào)文,所以不知道此時(shí)接收窗口收縮成了 100,客戶端只會(huì)看自己的可用窗口還有 220,所以客戶端就發(fā)送了 180 字節(jié)數(shù)據(jù),于是可用窗口減少到 40。
- 服務(wù)端收到了 180 字節(jié)數(shù)據(jù)時(shí),發(fā)現(xiàn)數(shù)據(jù)大小超過(guò)了接收窗口的大小,于是就把數(shù)據(jù)包丟失了。
- 客戶端收到第 2 步時(shí),服務(wù)端發(fā)送的確認(rèn)報(bào)文和通告窗口報(bào)文,嘗試減少發(fā)送窗口到 100,把窗口的右端向左收縮了 80,此時(shí)可用窗口的大小就會(huì)出現(xiàn)詭異的負(fù)值。
所以,如果發(fā)生了先減少緩存,再收縮窗口,就會(huì)出現(xiàn)丟包的現(xiàn)象。
為了防止這種情況發(fā)生,TCP 規(guī)定是不允許同時(shí)減少緩存又收縮窗口的,而是采用先收縮窗口,過(guò)段時(shí)間再減少緩存,這樣就可以避免了丟包情況。
窗口關(guān)閉
在前面我們都看到了,TCP 通過(guò)讓接收方指明希望從發(fā)送方接收的數(shù)據(jù)大小(窗口大小)來(lái)進(jìn)行流量控制。
如果窗口大小為 0 時(shí),就會(huì)阻止發(fā)送方給接收方傳遞數(shù)據(jù),直到窗口變?yōu)榉?0 為止,這就是窗口關(guān)閉。
窗口關(guān)閉潛在的危險(xiǎn)
接收方向發(fā)送方通告窗口大小時(shí),是通過(guò) ACK
報(bào)文來(lái)通告的。
那么,當(dāng)發(fā)生窗口關(guān)閉時(shí),接收方處理完數(shù)據(jù)后,會(huì)向發(fā)送方通告一個(gè)窗口非 0 的 ACK 報(bào)文,如果這個(gè)通告窗口的 ACK 報(bào)文在網(wǎng)絡(luò)中丟失了,那麻煩就大了。
這會(huì)導(dǎo)致發(fā)送方一直等待接收方的非 0 窗口通知,接收方也一直等待發(fā)送方的數(shù)據(jù),如不采取措施,這種相互等待的過(guò)程,會(huì)造成了死鎖的現(xiàn)象。
TCP 是如何解決窗口關(guān)閉時(shí),潛在的死鎖現(xiàn)象呢?
為了解決這個(gè)問(wèn)題,TCP 為每個(gè)連接設(shè)有一個(gè)持續(xù)定時(shí)器,只要 TCP 連接一方收到對(duì)方的零窗口通知,就啟動(dòng)持續(xù)計(jì)時(shí)器。
如果持續(xù)計(jì)時(shí)器超時(shí),就會(huì)發(fā)送窗口探測(cè) ( Window probe ) 報(bào)文,而對(duì)方在確認(rèn)這個(gè)探測(cè)報(bào)文時(shí),給出自己現(xiàn)在的接收窗口大小。
- 如果接收窗口仍然為 0,那么收到這個(gè)報(bào)文的一方就會(huì)重新啟動(dòng)持續(xù)計(jì)時(shí)器;
- 如果接收窗口不是 0,那么死鎖的局面就可以被打破了。
窗口探測(cè)的次數(shù)一般為 3 次,每次大約 30-60 秒(不同的實(shí)現(xiàn)可能會(huì)不一樣)。如果 3 次過(guò)后接收窗口還是 0 的話,有的 TCP 實(shí)現(xiàn)就會(huì)發(fā) RST
報(bào)文來(lái)中斷連接。
糊涂窗口綜合癥
如果接收方太忙了,來(lái)不及取走接收窗口里的數(shù)據(jù),那么就會(huì)導(dǎo)致發(fā)送方的發(fā)送窗口越來(lái)越小。
到最后,如果接收方騰出幾個(gè)字節(jié)并告訴發(fā)送方現(xiàn)在有幾個(gè)字節(jié)的窗口,而發(fā)送方會(huì)義無(wú)反顧地發(fā)送這幾個(gè)字節(jié),這就是糊涂窗口綜合癥。
要知道,我們的 TCP + IP
頭有 40
個(gè)字節(jié),為了傳輸那幾個(gè)字節(jié)的數(shù)據(jù),要搭上這么大的開(kāi)銷,這太不經(jīng)濟(jì)了。
就好像一個(gè)可以承載 50 人的大巴車,每次來(lái)了一兩個(gè)人,就直接發(fā)車。除非家里有礦的大巴司機(jī),才敢這樣玩,不然遲早破產(chǎn)。要解決這個(gè)問(wèn)題也不難,大巴司機(jī)等乘客數(shù)量超過(guò)了 25 個(gè),才認(rèn)定可以發(fā)車。
現(xiàn)舉個(gè)糊涂窗口綜合癥的栗子,考慮以下場(chǎng)景:
接收方的窗口大小是 360 字節(jié),但接收方由于某些原因陷入困境,假設(shè)接收方的應(yīng)用層讀取的能力如下:
- 接收方每接收 3 個(gè)字節(jié),應(yīng)用程序就只能從緩沖區(qū)中讀取 1 個(gè)字節(jié)的數(shù)據(jù);
- 在下一個(gè)發(fā)送方的 TCP 段到達(dá)之前,應(yīng)用程序還從緩沖區(qū)中讀取了 40 個(gè)額外的字節(jié);
每個(gè)過(guò)程的窗口大小的變化,在圖中都描述的很清楚了,可以發(fā)現(xiàn)窗口不斷減少了,并且發(fā)送的數(shù)據(jù)都是比較小的了。
所以,糊涂窗口綜合癥的現(xiàn)象是可以發(fā)生在發(fā)送方和接收方:
- 接收方可以通告一個(gè)小的窗口
- 而發(fā)送方可以發(fā)送小數(shù)據(jù)
于是,要解決糊涂窗口綜合癥,就要同時(shí)解決上面兩個(gè)問(wèn)題就可以了:
- 讓接收方不通告小窗口給發(fā)送方
- 讓發(fā)送方避免發(fā)送小數(shù)據(jù)
怎么讓接收方不通告小窗口呢?
接收方通常的策略如下:
當(dāng)「窗口大小」小于 min( MSS,緩存空間/2 ) ,也就是小于 MSS 與 1/2 緩存大小中的最小值時(shí),就會(huì)向發(fā)送方通告窗口為 0
,也就阻止了發(fā)送方再發(fā)數(shù)據(jù)過(guò)來(lái)。
等到接收方處理了一些數(shù)據(jù)后,窗口大小 >= MSS,或者接收方緩存空間有一半可以使用,就可以把窗口打開(kāi)讓發(fā)送方發(fā)送數(shù)據(jù)過(guò)來(lái)。
怎么讓發(fā)送方避免發(fā)送小數(shù)據(jù)呢?
發(fā)送方通常的策略如下:
使用 Nagle 算法,該算法的思路是延時(shí)處理,只有滿足下面兩個(gè)條件中的任意一個(gè)條件,才可以發(fā)送數(shù)據(jù):
- 條件一:要等到窗口大小 >=
MSS
并且 數(shù)據(jù)大小 >=MSS
; - 條件二:收到之前發(fā)送數(shù)據(jù)的
ack
回包;
只要上面兩個(gè)條件都不滿足,發(fā)送方一直在囤積數(shù)據(jù),直到滿足上面的發(fā)送條件。
Nagle 偽代碼如下:
if 有數(shù)據(jù)要發(fā)送 {if 可用窗口大小 >= MSS and 可發(fā)送的數(shù)據(jù) >= MSS {立刻發(fā)送MSS大小的數(shù)據(jù)} else {if 有未確認(rèn)的數(shù)據(jù) {將數(shù)據(jù)放入緩存等待接收ACK} else {立刻發(fā)送數(shù)據(jù)}}
}
注意,如果接收方不能滿足「不通告小窗口給發(fā)送方」,那么即使開(kāi)了 Nagle 算法,也無(wú)法避免糊涂窗口綜合癥,因?yàn)槿绻麑?duì)端 ACK 回復(fù)很快的話(達(dá)到 Nagle 算法的條件二),Nagle 算法就不會(huì)拼接太多的數(shù)據(jù)包,這種情況下依然會(huì)有小數(shù)據(jù)包的傳輸,網(wǎng)絡(luò)總體的利用率依然很低。
所以,接收方得滿足「不通告小窗口給發(fā)送方」+ 發(fā)送方開(kāi)啟 Nagle 算法,才能避免糊涂窗口綜合癥。
另外,Nagle 算法默認(rèn)是打開(kāi)的,如果對(duì)于一些需要小數(shù)據(jù)包交互的場(chǎng)景的程序,比如,telnet 或 ssh 這樣的交互性比較強(qiáng)的程序,則需要關(guān)閉 Nagle 算法。
可以在 Socket 設(shè)置 TCP_NODELAY
選項(xiàng)來(lái)關(guān)閉這個(gè)算法(關(guān)閉 Nagle 算法沒(méi)有全局參數(shù),需要根據(jù)每個(gè)應(yīng)用自己的特點(diǎn)來(lái)關(guān)閉)
setsockopt(sock_fd, IPPROTO_TCP, TCP_NODELAY, (char *)&value, sizeof(int));