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