面試被問TCP的可靠性是如何保證的?
我們知道TCP是可靠的,我們前面一篇文章講解了三次握手和四次揮手之后進(jìn)行數(shù)據(jù)傳輸,它們是建立在序列號(hào)機(jī)制和確認(rèn)應(yīng)答機(jī)制的基礎(chǔ)之上,如果保證這個(gè)機(jī)制的可靠性還需要一些其他輔助,TCP的可靠性保證包括:重傳機(jī)制,滑動(dòng)窗口,流量控制,擁塞控制等。
一、重傳機(jī)制
tcp的可靠性依賴于序列號(hào)機(jī)制和確認(rèn)應(yīng)答機(jī)制,即一端發(fā)送數(shù)據(jù)給另一端,另一端都會(huì)回復(fù)ack包,這樣才保證這條數(shù)據(jù)發(fā)送成功,而在這個(gè)過程中會(huì)有兩種可能發(fā)生:
- 一種是數(shù)據(jù)包未到達(dá)接收端,原因是數(shù)據(jù)丟失或者延時(shí)了;
- 一種是ack包未到達(dá)發(fā)送端,原因也是丟失或延時(shí)了。
前者數(shù)據(jù)未到達(dá)接收端,后者數(shù)據(jù)已經(jīng)到達(dá)接收端,只是回復(fù)的ack包丟失了,未到達(dá)發(fā)送端。
tcp采用重傳機(jī)制解決丟包和重復(fù)發(fā)送問題,tcp中重傳包括超時(shí)重傳,快速重傳,sack和d-sack。
1.超時(shí)重傳
顧名思義就是超過一定時(shí)間未收到回復(fù)就重新發(fā)送數(shù)據(jù),這里比較難以確定是超時(shí)重傳的時(shí)間RTO,這個(gè)時(shí)間太大和太小都不合適,應(yīng)該是比數(shù)據(jù)包一個(gè)來回的時(shí)間RTT多一點(diǎn)才合理,但是數(shù)據(jù)包一個(gè)來回的時(shí)間RTT不是固定的,會(huì)受到網(wǎng)絡(luò)波動(dòng)的影響,所以RTT的時(shí)間是按照幾次來回時(shí)間進(jìn)行加權(quán)平均值和RTT的波動(dòng)范圍計(jì)算出來的,而RTO是在此基礎(chǔ)上通過一些系數(shù)換算出來的。
2.快速重傳
雖然有超時(shí)重傳,但是有些數(shù)據(jù)包等到真的超時(shí)再重傳就有些太慢了,因此linux還有一種重傳機(jī)制叫做快速重傳。
原理是:當(dāng)發(fā)送方發(fā)送一條數(shù)據(jù)seq2后,未能得到ack數(shù)據(jù)包返回,此時(shí)如果后面又連續(xù)發(fā)送了幾條數(shù)據(jù)seq3,seq4,seq5,seq6,而后面這幾次收到的ack數(shù)據(jù)包都是ack2,ack2,ack2,ack2,意思是接收端已經(jīng)收到了seq2之前的數(shù)據(jù),但是seq2還沒有收到,快速重傳機(jī)制就是在發(fā)送端如果發(fā)送了多條數(shù)據(jù),但是每個(gè)數(shù)據(jù)包的回復(fù)包的序列號(hào)都是相同的,比如這個(gè)例子中seq3,seq4,seq5,seq6返回的數(shù)據(jù)包都是ack2,這個(gè)2指的是序列號(hào),表示接收端缺失的最大的數(shù)據(jù)的序列號(hào)是2,發(fā)送端應(yīng)該發(fā)送seq2過來,如果有連續(xù)的三個(gè)ack2,tcp就會(huì)判斷需要重發(fā)seq2,這種情況可以解決超時(shí)重傳等待時(shí)間過長的問題,但是新的問題是發(fā)送端不知道重新發(fā)送seq2還是重新發(fā)送seq3,seq4,seq5,seq6,這種情況下不同版本的linux有不同的實(shí)現(xiàn)。
3.sack
上面的問題拋出來了,linux后面怎么解決呢,引入sack,這個(gè)字段的值會(huì)放在tcp頭的選項(xiàng)字段上,就是發(fā)生上面例子中的情況下,后面接收端每次收到請(qǐng)求都會(huì)回復(fù)一個(gè)ack和sack,這兩個(gè)值中間的部分就是當(dāng)前接收端缺失的數(shù)據(jù),即ack<=x<sack。x就是缺失的那部分?jǐn)?shù)據(jù),這部分?jǐn)?shù)據(jù)的序列號(hào)起始是ack,末端是sack-1,這樣發(fā)送端就能知道接收端缺失的是哪部分?jǐn)?shù)據(jù),發(fā)送端只需要重新發(fā)送這些數(shù)據(jù)就可以了。
4.d-sack
還有一種情況就是發(fā)送端發(fā)送的數(shù)據(jù)在網(wǎng)絡(luò)中延時(shí)了,并沒有丟失,那么在發(fā)送端進(jìn)行重傳后,這個(gè)延時(shí)的數(shù)據(jù)又到達(dá)了,這樣就造成重復(fù)發(fā)送,這種情況下會(huì)采用d-sack方式,dsack其實(shí)就是利用sack處理重復(fù)數(shù)據(jù)的一種方式。依然是接收端回復(fù)ack+sack,只不過sack表示當(dāng)前重復(fù)的這條數(shù)據(jù)的序列號(hào),ack表示需要接受的序列號(hào),這樣發(fā)送端就能知道這條數(shù)據(jù)已經(jīng)發(fā)送過了。
不難發(fā)現(xiàn),可以通過ack和sack比較大小來區(qū)別這兩種模式,如果ack大于sack就說明是數(shù)據(jù)重復(fù)發(fā)送了,如果ack小于sack就說明是數(shù)據(jù)缺失了。
二、滑動(dòng)窗口
TCP為保證可靠性使用確認(rèn)應(yīng)答機(jī)制,理論上來說就是發(fā)送端發(fā)送一條數(shù)據(jù)到接收端,接收端收到后回復(fù)一個(gè)應(yīng)答數(shù)據(jù),一次對(duì)話才算結(jié)束,然后發(fā)送端才會(huì)發(fā)送下一條數(shù)據(jù)。
這樣的方式無疑是一種效率極低的方式,所以為了實(shí)現(xiàn)可靠以及高效,TCP引入滑動(dòng)窗口和流量控制
TCP推出滑動(dòng)窗口的概念,滑動(dòng)窗口就是接收端和發(fā)送端為每個(gè)socket開辟一塊空間,只有在接收端滑動(dòng)窗口空閑的時(shí)候才能處理發(fā)送端的數(shù)據(jù)。
原理:發(fā)送端每次發(fā)送數(shù)據(jù)到接收端,接收端都會(huì)返回一個(gè)滑動(dòng)窗口大小,表示接收端能接受的最大字節(jié)數(shù),發(fā)送方接收到這個(gè)滑動(dòng)窗口大小后可以連續(xù)發(fā)送多個(gè)數(shù)據(jù)包,只要在滑動(dòng)窗口范圍內(nèi)即可。
發(fā)送方的滑動(dòng)窗口有如下區(qū)域:
- 已發(fā)送還沒有收到回復(fù)區(qū)域
- 未發(fā)送區(qū)域
當(dāng)已發(fā)送的數(shù)據(jù)得到回復(fù)后,滑動(dòng)窗口右移。
接收端的滑動(dòng)窗口有如下區(qū)域:
- 已接收但是還沒有被應(yīng)用取走
- 還未接收的區(qū)域
如果接收的數(shù)據(jù)被應(yīng)用取走,窗口右移。
在滑動(dòng)窗口范圍內(nèi)發(fā)送端連續(xù)發(fā)送的幾個(gè)數(shù)據(jù)包,如果中間有一個(gè)包的ack丟失了,不一定需要重新發(fā)送,發(fā)送端可以通過下一個(gè)ack確定丟失的這個(gè)ack包需要不需要重發(fā)。也就是說有了滑動(dòng)窗口的概念,當(dāng)ack回復(fù)包丟失后不一定需要重發(fā)。
發(fā)送端的窗口大小是由接收端決定的。
三、流量控制
滑動(dòng)窗口的概念的提出,使得一次可以發(fā)送多個(gè)數(shù)據(jù)包,解決了一次只能處理一個(gè)數(shù)據(jù)包,效率低的問題。但是因?yàn)榻邮斩说奶幚砟芰κ怯邢薜?,作為發(fā)送端不能源源不斷的給接收端發(fā)送數(shù)據(jù),如果數(shù)據(jù)流量大了,接收端處理不了,就只能丟棄數(shù)據(jù)了,所以必須有一種機(jī)制可以控制數(shù)據(jù)的流量。
TCP的流量控制也恰恰是基于滑動(dòng)窗口的,滑動(dòng)窗口由接收端確認(rèn)后發(fā)送給發(fā)送端,發(fā)送端根據(jù)窗口大小進(jìn)行發(fā)送數(shù)據(jù),就能保證發(fā)送的數(shù)據(jù)在接收端都能被接收處理。
但是基于滑動(dòng)窗口實(shí)現(xiàn)流量控制TCP考慮了這樣幾個(gè)問題:
滑動(dòng)窗口是socket緩沖區(qū)中的一塊空間,socket對(duì)應(yīng)的緩沖區(qū)也不是一成不變的,所以如果緩沖區(qū)變化對(duì)滑動(dòng)窗口的同步就會(huì)造成一些影響。比如接收端通知發(fā)送端窗口大小為100,但是此時(shí)操作系統(tǒng)把socket緩沖區(qū)減小了到了50,收到的數(shù)據(jù)大于50,就會(huì)把包丟掉就出現(xiàn)了丟包現(xiàn)象。
還有一個(gè)問題,糊涂窗口問題,比如因?yàn)榻邮斩颂幚肀容^慢,滑動(dòng)窗口為0,窗口處于關(guān)閉狀態(tài),一段時(shí)間后,窗口出現(xiàn)了可能50個(gè)字節(jié)空閑空間,這時(shí)候就會(huì)把窗口=50通知發(fā)送端,發(fā)送端接收到窗口=50后,就會(huì)發(fā)送50字節(jié)的數(shù)據(jù)過來,但是要知道不管發(fā)送多少數(shù)據(jù),tcp都要給數(shù)據(jù)包上tcp頭,tcp頭就有20字節(jié),同時(shí)還要包ip頭,也是20字節(jié),足足40字節(jié),而發(fā)送的數(shù)據(jù)就只有10字節(jié),性價(jià)比是極低的。而且還會(huì)占用帶寬。
tcp在實(shí)現(xiàn)基于滑動(dòng)窗口實(shí)現(xiàn)流量控制的時(shí)候不得不考慮上面的問題,tcp如何解決呢?
tcp不允許同時(shí)減少緩沖區(qū)大小和窗口大小,如果需要減少緩沖區(qū)大小,必須先減少窗口大小,一段時(shí)間后再減少緩沖區(qū)大小。
要想解決糊涂窗口的問題,就要避免接收端給發(fā)送端回復(fù)較小的窗口和避免發(fā)送端發(fā)送小的數(shù)據(jù)包。
tcp規(guī)定接收端在窗口大小<min(mss,緩沖區(qū)的一半)的時(shí)候就要關(guān)閉窗口(回復(fù)接收端窗口為0),這樣就能保證接收端不會(huì)發(fā)送小的窗口給發(fā)送端了。
發(fā)送端也要解決發(fā)送小數(shù)據(jù)的問題,發(fā)送端是通過nagle算法進(jìn)行延遲處理,即滿足以下兩個(gè)條件中的一個(gè)才可以發(fā)送:
- 窗口大小大于mss或者數(shù)據(jù)大小大于mss
- 收到上一個(gè)數(shù)據(jù)發(fā)送回復(fù)的ack
只要滿足這兩個(gè)中一個(gè)就可以發(fā)送。但是這個(gè)算法一旦開啟就會(huì)造成一些數(shù)據(jù)本身就很小的包不能及時(shí)發(fā)送,所以這個(gè)算法開啟要慎重考慮。
tcp依靠上面的機(jī)制實(shí)現(xiàn)流量控制,具體的流程就是接收端每次都會(huì)給發(fā)送端回復(fù)窗口大小,當(dāng)接收端很忙的時(shí)候,可能窗口就會(huì)變小到0,就會(huì)通知發(fā)送端窗口關(guān)閉,此時(shí)發(fā)送端就不會(huì)再發(fā)送數(shù)據(jù)給接收端,當(dāng)接收端窗口變大后,就會(huì)主動(dòng)回復(fù)發(fā)送端窗口大小。
但是這個(gè)回復(fù)可能會(huì)丟失,那么這是就會(huì)出現(xiàn)互相等待的問題,可以理解為死鎖,tcp的解決方案是定義一個(gè)時(shí)鐘,就是一個(gè)定時(shí)器,當(dāng)?shù)竭_(dá)一定時(shí)間接收端還沒有通知窗口的話,發(fā)送端就會(huì)發(fā)送探測(cè)報(bào)文,一般每30-60秒發(fā)送一次,發(fā)送三次(這里不同實(shí)現(xiàn)可能不一樣,可以配置),如果回復(fù)了窗口就開始發(fā)送數(shù)據(jù),如果回復(fù)的窗口依然是0就重置時(shí)鐘重新計(jì)時(shí),如果最終都沒有打開窗口,發(fā)送端可能會(huì)發(fā)送rst包給服務(wù)端終止連接。
四、擁塞控制
滑動(dòng)窗口和流量控制保證了接收端繁忙的時(shí)候,數(shù)據(jù)不會(huì)因?yàn)闊o處安放而丟棄。
但是網(wǎng)絡(luò)是共享的,網(wǎng)絡(luò)也會(huì)存在很繁忙的情況,如果網(wǎng)絡(luò)擁堵,也會(huì)造成丟包,tcp在沒有收到ack的時(shí)候就會(huì)重傳,使得網(wǎng)絡(luò)更加擁堵,丟失數(shù)據(jù)會(huì)更多,就會(huì)使得整個(gè)網(wǎng)絡(luò)環(huán)境更加糟糕。
tcp為解決網(wǎng)絡(luò)擁堵帶來的數(shù)據(jù)丟失問題,提出了擁塞控制。
在擁塞控制機(jī)制中的兩個(gè)概念:擁塞窗口和擁塞算法
首先來看怎樣才算擁堵,tcp認(rèn)為只要是出現(xiàn)超時(shí)重傳就算擁堵了。
tcp在發(fā)送數(shù)據(jù)的時(shí)候,發(fā)送數(shù)據(jù)的大小受到滑動(dòng)窗口和擁塞窗口的限制,也就是發(fā)送端能發(fā)送的最大數(shù)據(jù)量是擁塞窗口和滑動(dòng)窗口的的最小值。
擁塞算法
(1) 慢啟動(dòng):
在剛剛建立連接的時(shí)候,滑動(dòng)窗口和擁塞窗口一致,接下來我們假設(shè)滑動(dòng)窗口和擁塞窗口初始值為100字節(jié),后面的說明都以此假設(shè)為基礎(chǔ)。
慢啟動(dòng),就是在初始值的基礎(chǔ)上,發(fā)送端向接收端發(fā)送100字節(jié)數(shù)據(jù)包(這里不要考慮數(shù)據(jù)包的個(gè)數(shù),直接以總字節(jié)數(shù)來說明),當(dāng)所有的ack返回后,擁塞窗口就會(huì)在100的基礎(chǔ)上加100。再循環(huán)一次就是在200的基礎(chǔ)上加200,再一次就是加800,這種算法的擁塞窗口是指數(shù)級(jí)增長的,并且增長速度很快,因此總要有個(gè)限制的,這個(gè)限制叫做擁塞門限值,這個(gè)值默認(rèn)65535個(gè)字節(jié)。當(dāng)擁塞窗口的值達(dá)到這個(gè)值后就進(jìn)入擁塞避免階段。
(2) 擁塞避免:
當(dāng)擁塞窗口的值達(dá)到擁塞門限值后,就會(huì)進(jìn)入擁塞避免階段,在這個(gè)階段,比如當(dāng)前擁塞窗口的值達(dá)到了65535個(gè)字節(jié),如果這個(gè)65535個(gè)字節(jié)都發(fā)送出去了,當(dāng)所有的ack返回后,就是在65535個(gè)字節(jié)的基礎(chǔ)上加65535個(gè)字節(jié),再一次就再加65535個(gè)字節(jié),再一次還是加65535個(gè)字節(jié),也就不再是指數(shù)級(jí)增長了,而是線性增長,說白了就是增長的速度降下來了。但是即便是這樣,擁塞窗口值也處于一個(gè)增長狀態(tài)。那什么時(shí)候是個(gè)頭呢,答案是出現(xiàn)超時(shí)重傳的時(shí)候。
(3) 擁塞發(fā)生:
當(dāng)發(fā)生重傳的時(shí)候就意味著擁塞現(xiàn)象發(fā)生了,擁塞發(fā)生是因?yàn)橹貍髟斐傻模貍鞣譃槌瑫r(shí)重傳和快速重傳,當(dāng)發(fā)生超時(shí)重傳的時(shí)候說明網(wǎng)絡(luò)確實(shí)很糟糕了,tcp的做法是將擁塞門限值設(shè)置為此時(shí)擁塞窗口值的一半,同時(shí)將擁塞窗口值設(shè)置為1.從而再次進(jìn)入慢啟動(dòng)階段。而如果發(fā)生的是快速重傳,說明網(wǎng)絡(luò)也不是很糟糕,擁塞窗口值會(huì)設(shè)置為此時(shí)擁塞窗口值的一半,擁塞門限值也會(huì)設(shè)置為擁塞窗口值的一半,此時(shí)如果什么都不做就會(huì)進(jìn)入擁塞避免階段,但是對(duì)于這種情況,tcp會(huì)進(jìn)入快速恢復(fù)算法。
(4) 快速恢復(fù):
就是在當(dāng)前在當(dāng)前擁塞門限值的基礎(chǔ)上加3來表示擁塞窗口的大小,接下來重發(fā)失敗的數(shù)據(jù),收到恢復(fù)后就加1,接下來發(fā)送新的數(shù)據(jù)并進(jìn)入擁塞避免階段。