萬字長文!23個問題TCP疑難雜癥全解析
每個時代,都不會虧待會學(xué)習(xí)的人。
在進(jìn)入今天主題之前我先拋幾個問題,這篇文章一共提出 23 個問題。
TCP 握手一定是三次?TCP 揮手一定是四次?
為什么要有快速重傳,超時重傳不夠用?為什么要有 SACK,為什么要有 D-SACK?
都知道有滑動窗口,那由于接收方的太忙了滑動窗口降為了 0 怎么辦?發(fā)送方就永遠(yuǎn)等著了?
Silly Window 又是什么?
為什么有滑動窗口流控還需要擁塞控制?
快速重傳一定要依賴三次重復(fù) ACK ?
這篇文章我想由淺到深地過一遍 TCP,不是生硬的搬出各個知識點,從問題入手,然后從發(fā)展、演進(jìn)的角度來看 TCP。
起初我在學(xué)計算機(jī)網(wǎng)絡(luò)的時候就有非常非常多的疑問,腦子里簡直充滿了十萬個為什么,而網(wǎng)絡(luò)又非常的復(fù)雜,發(fā)展了這么多年東西真的太多了,今天我就大致的淺顯地說一說我對 TCP 這些要點的理解。
好了,廢話不多說,開始上正菜。
TCP 是用來解決什么問題?
TCP 即 Transmission Control Protocol,可以看到是一個傳輸控制協(xié)議,重點就在這個控制。
控制什么?
控制可靠、按序地傳輸以及端與端之間的流量控制。夠了么?還不夠,它需要更加智能,因此還需要加個擁塞控制,需要為整體網(wǎng)絡(luò)的情況考慮。
這就是出行你我他,安全靠大家。
為什么要 TCP,IP 層實現(xiàn)控制不行么?
我們知道網(wǎng)絡(luò)是分層實現(xiàn)的,網(wǎng)絡(luò)協(xié)議的設(shè)計就是為了通信,從鏈路層到 IP 層其實就已經(jīng)可以完成通信了。
你看鏈路層不可或缺畢竟咱們電腦都是通過鏈路相互連接的,然后 IP 充當(dāng)了地址的功能,所以通過 IP 咱們找到了對方就可以進(jìn)行通信了。
那加個 TCP 層干啥?IP 層實現(xiàn)控制不就完事了嘛?
之所以要提取出一個 TCP 層來實現(xiàn)控制是因為 IP 層涉及到的設(shè)備更多,一條數(shù)據(jù)在網(wǎng)絡(luò)上傳輸需要經(jīng)過很多設(shè)備,而設(shè)備之間需要靠 IP 來尋址。
假設(shè) IP 層實現(xiàn)了控制,那是不是涉及到的設(shè)備都需要關(guān)心很多事情?整體傳輸?shù)男适遣皇谴蟠蛘劭哿耍?
我舉個例子,假如 A 要傳輸給 F 一個積木,但是無法直接傳輸?shù)?,需要?jīng)過 B、C、D、E 這幾個中轉(zhuǎn)站之手。這里有兩種情況:
- 假設(shè) BCDE 都需要關(guān)心這個積木搭錯了沒,都拆開包裹仔細(xì)的看看,沒問題了再裝回去,最終到了 F 的手中。
- 假設(shè) BCDE 都不關(guān)心積木的情況,來啥包裹只管轉(zhuǎn)發(fā)就完事了,由最終的 F 自己來檢查這個積木答錯了沒。
你覺得哪種效率高?明顯是第二種,轉(zhuǎn)發(fā)的設(shè)備不需要關(guān)心這些事,只管轉(zhuǎn)發(fā)就完事!
所以把控制的邏輯獨立出來成 TCP 層,讓真正的接收端來處理,這樣網(wǎng)絡(luò)整體的傳輸效率就高了。
連接到底是什么?
我們已經(jīng)知道了為什么需要獨立出 TCP 這一層,并且這一層主要是用來干嘛的,接下來就來看看它到底是怎么干的。
我們都知道 TCP 是面向連接的,那這個連接到底是個什么東西?真的是拉了一條線讓端與端之間連起來了?
所謂的連接其實只是雙方都維護(hù)了一個狀態(tài),通過每一次通信來維護(hù)狀態(tài)的變更,使得看起來好像有一條線關(guān)聯(lián)了對方。
TCP 協(xié)議頭
在具體深入之前我們需要先來看看一些 TCP 頭的格式,這很基礎(chǔ)也很重要。
圖來自網(wǎng)絡(luò)
我就不一一解釋了,挑重點的說。
首先可以看到 TCP 包只有端口,沒有 IP。
Seq 就是 Sequence Number 即序號,它是用來解決亂序問題的。
ACK 就是 Acknowledgement Numer 即確認(rèn)號,它是用來解決丟包情況的,告訴發(fā)送方這個包我收到啦。
標(biāo)志位就是 TCP flags 用來標(biāo)記這個包是什么類型的,用來控制 TPC 的狀態(tài)。
窗口就是滑動窗口,Sliding Window,用來流控。
三次握手
明確了協(xié)議頭的要點之后,我們再來看三次握手。
三次握手真是個老生常談的問題了,但是真的懂了么?不是浮在表面?能不能延伸出一些點別的?
我們先來看一下熟悉的流程。
圖來自網(wǎng)絡(luò)
首先為什么要握手,其實主要就是為了初始化Seq Numer,SYN 的全稱是 Synchronize Sequence Numbers,這個序號是用來保證之后傳輸數(shù)據(jù)的順序性。
你要說是為了測試保證雙方發(fā)送接收功能都正常,我覺得也沒毛病,不過我認(rèn)為重點在于同步序號。
那為什么要三次,就拿我和你這兩個角色來說,首先我告訴你我的初始化序號,你聽到了和我說你收到了。
然后你告訴我你的初始序號,然后我對你說我收到了。
這好像四次了?如果真的按一來一回就是四次,但是中間一步可以合在一起,就是你和我說你知道了我的初始序號的時候同時將你的初始序號告訴我。
因此四次握手就可以減到三次了。
不過你沒有想過這么一種情形,我和你同時開口,一起告訴對方各自的初始序號,然后分別回應(yīng)收到了,這不就是四次握手了?
我來畫個圖,清晰一點。
看看是不是四次握手了? 不過具體還是得看實現(xiàn),有些實現(xiàn)可能不允許這種情況出現(xiàn),但是這不影響我們思考,因為握手的重點就是同步初始序列號,這種情況也完成了同步的目標(biāo)。
初始序列號 ISN 的取值
不知道大家有沒有想過 ISN 的值要設(shè)成什么?代碼寫死從零開始?
想象一下如果寫死一個值,比如 0 ,那么假設(shè)已經(jīng)建立好連接了,client 也發(fā)了很多包比如已經(jīng)第 20 個包了,然后網(wǎng)絡(luò)斷了之后 client 重新,端口號還是之前那個,然后序列號又從 0 開始,此時服務(wù)端返回第 20 個包的ack,客戶端是不是傻了?
所以 RFC793 中認(rèn)為 ISN 要和一個假的時鐘綁定在一起ISN 每四微秒加一,當(dāng)超過 2 的 32 次方之后又從 0 開始,要四個半小時左右發(fā)生 ISN 回繞。
所以 ISN 變成一個遞增值,真實的實現(xiàn)還需要加一些隨機(jī)值在里面,防止被不法份子猜到 ISN。
SYN 超時了怎么處理?
也就是 client 發(fā)送 SYN 至 server 然后就掛了,此時 server 發(fā)送 SYN+ACK 就一直得不到回復(fù),怎么辦?
我腦海中一想到的就是重試,但是不能連續(xù)快速重試多次,你想一下,假設(shè) client 掉線了,你總得給它點時間恢復(fù)吧,所以呢需要慢慢重試,階梯性重試。
在 Linux 中就是默認(rèn)重試 5 次,并且就是階梯性的重試,間隔就是1s、2s、4s、8s、16s,再第五次發(fā)出之后還得等 32s 才能知道這次重試的結(jié)果,所以說總共等63s 才能斷開連接。
SYN Flood 攻擊
你看到?jīng)] SYN 超時需要耗費服務(wù)端 63s 的時間斷開連接,也就說 63s 內(nèi)服務(wù)端需要保持這個資源,所以不法分子就可以構(gòu)造出大量的 client 向 server 發(fā) SYN 但就是不回 server。
圖來自網(wǎng)絡(luò)
使得 server 的 SYN 隊列耗盡,無法處理正常的建連請求。
所以怎么辦?
可以開啟 tcp_syncookies,那就用不到 SYN 隊列了。
SYN 隊列滿了之后 TCP 根據(jù)自己的 ip、端口、然后對方的 ip、端口,對方 SYN 的序號,時間戳等一波操作生成一個特殊的序號(即 cookie)發(fā)回去,如果對方是正常的 client 會把這個序號發(fā)回來,然后 server 根據(jù)這個序號建連。
或者調(diào)整 tcp_synack_retries 減少重試的次數(shù),設(shè)置 tcp_max_syn_backlog 增加 SYN 隊列數(shù),設(shè)置 tcp_abort_on_overflow SYN 隊列滿了直接拒絕連接。
為什么要四次揮手?
四次揮手和三次握手成雙成對,同樣也是 TCP 中的一線明星,讓我們重溫一下熟悉的圖。
圖來自網(wǎng)絡(luò)
為什么揮手需要四次?因為 TCP 是全雙工協(xié)議,也就是說雙方都要關(guān)閉,每一方都向?qū)Ψ桨l(fā)送 FIN 和回應(yīng) ACK。
就像我對你說我數(shù)據(jù)發(fā)完了,然后你回復(fù)好的你收到了。然后你對我說你數(shù)據(jù)發(fā)完了,然后我向你回復(fù)我收到了。
所以看起來就是四次。
從圖中可以看到主動關(guān)閉方的狀態(tài)是 FIN_WAIT_1 到 FIN_WAIT_2 然后再到 TIME_WAIT,而被動關(guān)閉方是 CLOSE_WAIT 到 LAST_ACK。
四次揮手狀態(tài)一定是這樣變遷的嗎
狀態(tài)一定是這樣變遷的嗎?讓我們再來看個圖。
圖來自網(wǎng)絡(luò)
可以看到雙方都主動發(fā)起斷開請求所以各自都是主動發(fā)起方,狀態(tài)會從 FIN_WAIT_1 都進(jìn)入到 CLOSING 這個過度狀態(tài)然后再到 TIME_WAIT。
揮手一定需要四次嗎?
假設(shè) client 已經(jīng)沒有數(shù)據(jù)發(fā)送給 server 了,所以它發(fā)送 FIN 給 server 表明自己數(shù)據(jù)發(fā)完了,不再發(fā)了,如果這時候 server 還是有數(shù)據(jù)要發(fā)送給 client 那么它就是先回復(fù) ack ,然后繼續(xù)發(fā)送數(shù)據(jù)。
等 server 數(shù)據(jù)發(fā)送完了之后再向 client 發(fā)送 FIN 表明它也發(fā)完了,然后等 client 的 ACK 這種情況下就會有四次揮手。
那么假設(shè) client 發(fā)送 FIN 給 server 的時候 server 也沒數(shù)據(jù)給 client,那么 server 就可以將 ACK 和它的 FIN 一起發(fā)給client ,然后等待 client 的 ACK,這樣不就三次揮手了?
為什么要有 TIME_WAIT?
斷開連接發(fā)起方在接受到接受方的 FIN 并回復(fù) ACK 之后并沒有直接進(jìn)入 CLOSED 狀態(tài),而是進(jìn)行了一波等待,等待時間為 2MSL。
MSL 是 Maximum Segment Lifetime,即報文最長生存時間,RFC 793 定義的 MSL 時間是 2 分鐘,Linux 實際實現(xiàn)是 30s,那么 2MSL 是一分鐘。
那么為什么要等 2MSL 呢?
-
就是怕被動關(guān)閉方?jīng)]有收到最后的 ACK,如果被動方由于網(wǎng)絡(luò)原因沒有到,那么它會再次發(fā)送 FIN, 此時如果主動關(guān)閉方已經(jīng) CLOSED 那就傻了,因此等一會兒。
-
假設(shè)立馬斷開連接,但是又重用了這個連接,就是五元組完全一致,并且序號還在合適的范圍內(nèi),雖然概率很低但理論上也有可能,那么新的連接會被已關(guān)閉連接鏈路上的一些殘留數(shù)據(jù)干擾,因此給予一定的時間來處理一些殘留數(shù)據(jù)。
等待 2MSL 會產(chǎn)生什么問題?
如果服務(wù)器主動關(guān)閉大量的連接,那么會出現(xiàn)大量的資源占用,需要等到 2MSL 才會釋放資源。
如果是客戶端主動關(guān)閉大量的連接,那么在 2MSL 里面那些端口都是被占用的,端口只有 65535 個,如果端口耗盡了就無法發(fā)起送的連接了,不過我覺得這個概率很低,這么多端口你這是要建立多少個連接?
如何解決 2MSL 產(chǎn)生的問題?
快速回收,即不等 2MSL 就回收, Linux 的參數(shù)是 tcp_tw_recycle,還有 tcp_timestamps 不過默認(rèn)是打開的。
其實上面我們已經(jīng)分析過為什么需要等 2MSL,所以如果等待時間果斷就是出現(xiàn)上面說的那些問題。
所以不建議開啟,而且 Linux 4.12 版本后已經(jīng)咔擦了這個參數(shù)了。
前不久剛有位朋友在群里就提到了這玩意。
一問果然有 NAT 的身影。
現(xiàn)象就是請求端請求服務(wù)器的靜態(tài)資源偶爾會出現(xiàn) 20-60 秒左右才會有響應(yīng)的情況,從抓包看請求端連續(xù)三個 SYN 都沒有回應(yīng)。
比如你在學(xué)校,對外可能就一個公網(wǎng) IP,然后開啟了 tcp_tw_recycle(tcp_timestamps 也是打開的情況下),在 60 秒內(nèi)對于同源 IP 的連接請求中 timestamp 必須是遞增的,不然認(rèn)為其是過期的數(shù)據(jù)包就會丟棄。
學(xué)校這么多機(jī)器,你無法保證時間戳是一致的,因此就會出問題。
所以這玩意不推薦使用。
重用,即開啟 tcp_tw_reuse 當(dāng)然也是需要 tcp_timestamps 的。
這里有個重點,tcp_tw_reuse 是用在連接發(fā)起方的,而我們的服務(wù)端基本上是連接被動接收方。
tcp_tw_reuse 是發(fā)起新連接的時候,可以復(fù)用超過 1s 的處于 TIME_WAIT 狀態(tài)的連接,所以它壓根沒有減少我們服務(wù)端的壓力。
它重用的是發(fā)起方處于 TIME_WAIT 的連接。
這里還有一個 SO_REUSEADDR ,這玩意有人會和 tcp_tw_reuse 混為一談,首先 tcp_tw_reuse 是內(nèi)核選項而 SO_REUSEADDR 是用戶態(tài)選項。
然后 SO_REUSEADDR 主要用在你啟動服務(wù)的時候,如果此時的端口被占用了并且這個連接處于 TIME_WAIT 狀態(tài),那么你可以重用這個端口,如果不是 TIME_WAIT,那就是給你個 Address already in use。
所以這兩個玩意好像都不行,而且 tcp_tw_reuse 和tcp_tw_recycle,其實是違反 TCP 協(xié)議的,說好的等我到天荒地老,你卻偷偷放了手?
要么就是調(diào)小 MSL 的時間,不過也不太安全,要么調(diào)整 tcp_max_tw_buckets 控制 TIME_WAIT 的數(shù)量,不過默認(rèn)值已經(jīng)很大了 180000,這玩意應(yīng)該是用來對抗 DDos 攻擊的。
所以我給出的建議是服務(wù)端不要主動關(guān)閉,把主動關(guān)閉方放到客戶端。畢竟咱們服務(wù)器是一對很多很多服務(wù),我們的資源比較寶貴。
自己攻擊自己
還有一個很騷的解決方案,我自己瞎想的,就是自己攻擊自己。
Socket 有一個選項叫 IP_TRANSPARENT ,可以綁定一個非本地的地址,然后服務(wù)端把建連的 ip 和端口都記下來,比如寫入本地某個地方。
然后啟動一個服務(wù),假如現(xiàn)在服務(wù)端資源很緊俏,那么你就定個時間,過了多久之后就將處于 TIME_WAIT 狀態(tài)的對方 ip 和端口告訴這個服務(wù)。
然后這個服務(wù)就利用 IP_TRANSPARENT 偽裝成之前的那個 client 向服務(wù)端發(fā)起一個請求,然后服務(wù)端收到會給真的 client 一個 ACK, 那 client 都關(guān)了已經(jīng),說你在搞啥子,于是回了一個 RST,然后服務(wù)端就中止了這個連接。
超時重傳機(jī)制是為了解決什么問題?
前面我們提到 TCP 要提供可靠的傳輸,那么網(wǎng)絡(luò)又是不穩(wěn)定的如果傳輸?shù)陌鼘Ψ經(jīng)]收到卻又得保證可靠那么就必須重傳。
TCP 的可靠性是靠確認(rèn)號的,比如我發(fā)給你1、2、3、4這4個包,你告訴我你現(xiàn)在要 5 那說明前面四個包你都收到了,就是這么回事兒。
不過這里要注意,SeqNum 和 ACK 都是以字節(jié)數(shù)為單位的,也就是說假設(shè)你收到了1、2、4 但是 3 沒有收到你不能 ACK 5,如果你回了 5 那么發(fā)送方就以為你5之前的都收到了。
所以只能回復(fù)確認(rèn)最大連續(xù)收到包,也就是 3。
而發(fā)送方不清楚 3、4 這兩個包到底是還沒到呢還是已經(jīng)丟了,于是發(fā)送方需要等待,這等待的時間就比較講究了。
如果太心急可能 ACK 已經(jīng)在路上了,你這重傳就是浪費資源了,如果太散漫,那么接收方急死了,這死鬼怎么還不發(fā)包來,我等的花兒都謝了。
所以這個等待超時重傳的時間很關(guān)鍵,怎么搞?聰明的小伙伴可能一下就想到了,你估摸著正常來回一趟時間是多少不就好了,我就等這么長。
這就來回一趟的時間就叫 RTT,即 Round Trip Time,然后根據(jù)這個時間制定超時重傳的時間 RTO,即 Retransmission Timeout。
不過這里大概只好了 RTO 要參考下 RTT ,但是具體要怎么算?首先肯定是采樣,然后一波加權(quán)平均得到 RTO。
RFC793 定義的公式如下:
1、先采樣 RTT 2、SRTT = ( ALPHA * SRTT ) + ((1-ALPHA) * RTT) 3、RTO = min[UBOUND,max[LBOUND,(BETA*SRTT)]]
ALPHA 是一個平滑因子取值在 0.8~0.9之間,UBOUND 就是超時時間上界-1分鐘,LBOUND 是下界-1秒鐘,BETA 是一個延遲方差因子,取值在 1.3~2.0。
但是還有個問題,RTT 采樣的時間用一開始發(fā)送數(shù)據(jù)的時間到收到 ACK 的時間作為樣本值還是重傳的時間到 ACK 的時間作為樣本值?
圖來自網(wǎng)絡(luò)
從圖中就可以看到,一個時間算長了,一個時間算短了,這有點難,因為你不知道這個 ACK 到底是回復(fù)誰的。
所以怎么辦?發(fā)生重傳的來回我不采樣不就好了,我不知道這次 ACK 到底是回復(fù)誰的,我就不管他,我就采樣正常的來回。
這就是 Karn / Partridge 算法,不采樣重傳的RTT。
但是不采樣重傳會有問題,比如某一時刻網(wǎng)絡(luò)突然就是很差,你要是不管重傳,那么還是按照正常的 RTT 來算 RTO, 那么超時的時間就過短了,于是在網(wǎng)絡(luò)很差的情況下還瘋狂重傳加重了網(wǎng)絡(luò)的負(fù)載。
因此 Karn 算法就很粗暴的搞了個發(fā)生重傳我就將現(xiàn)在的 RTO 翻倍,哼!就是這么簡單粗暴。
但是這種平均的計算很容易把一個突然間的大波動,平滑掉,所以又搞了個算法,叫 Jacobson / Karels Algorithm。
它把最新的 RTT 和平滑過的 SRTT 做了波計算得到合適的 RTO,公式我就不貼了,反正我不懂,不懂就不嗶嗶了。
為什么還需要快速重傳機(jī)制?
超時重傳是按時間來驅(qū)動的,如果是網(wǎng)絡(luò)狀況真的不好的情況,超時重傳沒問題,但是如果網(wǎng)絡(luò)狀況好的時候,只是恰巧丟包了,那等這么長時間就沒必要。
于是又引入了數(shù)據(jù)驅(qū)動的重傳叫快速重傳,什么意思呢?就是發(fā)送方如果連續(xù)三次收到對方相同的確認(rèn)號,那么馬上重傳數(shù)據(jù)。
因為連續(xù)收到三次相同 ACK 證明當(dāng)前網(wǎng)絡(luò)狀況是 ok 的,那么確認(rèn)是丟包了,于是立馬重發(fā),沒必要等這么久。
圖來自網(wǎng)絡(luò)
看起來好像挺完美的,但是你有沒有想過我發(fā)送1、2、3、4這4個包,就 2 對方?jīng)]收到,1、3、4都收到了,然后不管是超時重傳還是快速重傳反正對方就回 ACK 2。
這時候要重傳 2、3、4 呢還是就 2 呢?
SACK 的引入是為了解決什么問題?
SACK 即 Selective Acknowledgment,它的引入就是為了解決發(fā)送方不知道該重傳哪些數(shù)據(jù)的問題。
我們來看一下下面的圖就知道了。
圖來自網(wǎng)絡(luò)
SACK 就是接收方會回傳它已經(jīng)接受到的數(shù)據(jù),這樣發(fā)送方就知道哪一些數(shù)據(jù)對方已經(jīng)收到了,所以就可以選擇性的發(fā)送丟失的數(shù)據(jù)。
如圖,通過 ACK 告知我接下來要 5500 開始的數(shù)據(jù),并一直更新 SACK,6000-6500 我收到了,6000-7000的數(shù)據(jù)我收到了,6000-7500的數(shù)據(jù)我收到了,發(fā)送方很明確的知道,5500-5999 的那一波數(shù)據(jù)應(yīng)該是丟了,于是重傳。
而且如果數(shù)據(jù)是多段不連續(xù)的, SACK 也可以發(fā)送,比如 SACK 0-500,1000-1500,2000-2500。就表明這幾段已經(jīng)收到了。
D-SACK 又是什么東西?
D-SACK 其實是 SACK 的擴(kuò)展,它利用 SACK 的第一段來描述重復(fù)接受的不連續(xù)的數(shù)據(jù)序號,如果第一段描述的范圍被 ACK 覆蓋,說明重復(fù)了,比如我都 ACK 到6000了你還給我回 SACK 5000-5500 呢?
說白了就是從第一段的反饋來和已經(jīng)接受到的 ACK 比一比,參數(shù)是 tcp_dsack,Linux 2.4 之后默認(rèn)開啟。
那知道重復(fù)了有什么用呢?
1、知道重復(fù)了說明對方收到剛才那個包了,所以是回來的 ACK 包丟了。2、是不是包亂序的,先發(fā)的包后到?3、是不是自己太著急了,RTO 太小了?4、是不是被數(shù)據(jù)復(fù)制了,搶先一步呢?
滑動窗口干嘛用?
我們已經(jīng)知道了 TCP 有序號,并且還有重傳,但是這還不夠,因為我們不是愣頭青,還需要根據(jù)情況來控制一下發(fā)送速率,因為網(wǎng)絡(luò)是復(fù)雜多變的,有時候就會阻塞住,而有時候又很通暢。
所以發(fā)送方需要知道接收方的情況,好控制一下發(fā)送的速率,不至于蒙著頭一個勁兒的發(fā)然后接受方都接受不過來。
因此 TCP 就有個叫滑動窗口的東西來做流量控制,也就是接收方告訴發(fā)送方我還能接受多少數(shù)據(jù),然后發(fā)送方就可以根據(jù)這個信息來進(jìn)行數(shù)據(jù)的發(fā)送。
以下是發(fā)送方維護(hù)的窗口,就是黑色圈起來的。
圖來自網(wǎng)絡(luò)
圖中的 #1 是已收到 ACK 的數(shù)據(jù),#2 是已經(jīng)發(fā)出去但是還沒收到 ACK 的數(shù)據(jù),#3 就是在窗口內(nèi)可以發(fā)送但是還沒發(fā)送的數(shù)據(jù)。#4 就是還不能發(fā)送的數(shù)據(jù)。
然后此時收到了 36 的 ACK,并且發(fā)出了 46-51 的字節(jié),于是窗口向右滑動了。
圖片來自網(wǎng)絡(luò)
TCP/IP Guide 上還有一張完整的圖,畫的十分清晰,大家看一下。
如果接收方回復(fù)的窗口一直是 0 怎么辦?
上文已經(jīng)說了發(fā)送方式根據(jù)接收方回應(yīng)的 window 來控制能發(fā)多少數(shù)據(jù),如果接收方一直回應(yīng) 0,那發(fā)送方就杵著?
你想一下,發(fā)送方發(fā)的數(shù)據(jù)都得到 ACK 了,但是呢回應(yīng)的窗口都是 0 ,這發(fā)送方此時不敢發(fā)了啊,那也不能一直等著啊,這 Window 啥時候不變 0 ?。?
于是 TCP 有一個 Zero Window Probe 技術(shù),發(fā)送方得知窗口是 0 之后,會去探測探測這個接收方到底行不行,也就是發(fā)送 ZWP 包給接收方。
具體看實現(xiàn)了,可以發(fā)送多次,然后還有間隔時間,多次之后都不行可以直接 RST。
假設(shè)接收方每次回應(yīng)窗口都很小怎么辦?
你想象一下,如果每次接收方都說我還能收 1 個字節(jié),發(fā)送方該不該發(fā)?
TCP + IP 頭部就 40 個字節(jié)了,這傳輸不劃算啊,如果傻傻的一直發(fā)這就叫 Silly Window。
那咋辦,一想就是發(fā)送端等著,等養(yǎng)肥了再發(fā),要么接收端自己自覺點,數(shù)據(jù)小于一個閾值就告訴發(fā)送端窗口此時是 0 算了,也等養(yǎng)肥了再告訴發(fā)送端。
發(fā)送端等著的方案就是納格算法,這個算法相信看一下代碼就知道了。
簡單的說就是當(dāng)前能發(fā)送的數(shù)據(jù)和窗口大于等于 MSS 就立即發(fā)送,否則再判斷一下之前發(fā)送的包 ACK 回來沒,回來再發(fā),不然就攢數(shù)據(jù)。
接收端自覺點的方案是 David D Clark’s 方案,如果窗口數(shù)據(jù)小于某個閾值就告訴發(fā)送方窗口 0 別發(fā),等緩過來數(shù)據(jù)大于等于 MSS 或者接受 buffer 騰出一半空間了再設(shè)置正常的 window 值給發(fā)送方。
對了提到納格算法不得不再提一下延遲確認(rèn),納格算法在等待接收方的確認(rèn),而開啟延遲確認(rèn)則會延遲發(fā)送確認(rèn),會等之后的包收到了再一起確認(rèn)或者等待一段時候真的沒了再回復(fù)確認(rèn)。
這就相互等待了,然后延遲就很大了,兩個不可同時開啟。
已經(jīng)有滑動窗口了為什么還要擁塞控制?
前面我已經(jīng)提到了,加了擁塞控制是因為 TCP 不僅僅就管兩端之間的情況,還需要知曉一下整體的網(wǎng)絡(luò)情形,畢竟只有大家都守規(guī)矩了道路才會通暢。
前面我們提到了重傳,如果不管網(wǎng)絡(luò)整體的情況,肯定就是對方?jīng)]給 ACK ,那我就無腦重傳。
如果此時網(wǎng)絡(luò)狀況很差,所有的連接都這樣無腦重傳,是不是網(wǎng)絡(luò)情況就更差了,更加擁堵了?
然后越擁堵越重傳,一直沖沖沖!然后就 GG 了。
所以需要個擁塞控制,來避免這種情況的發(fā)送。
擁塞控制怎么搞?
主要有以下幾個步驟來搞:
1、慢啟動,探探路。2、擁塞避免,感覺差不多了減速看看 3、擁塞發(fā)生快速重傳/恢復(fù)
慢啟動,就是新司機(jī)上路慢慢來,初始化 cwnd(Congestion Window)為 1,然后每收到一個 ACK 就 cwnd++ 并且每過一個 RTT ,cwnd = 2*cwnd 。
線性中帶著指數(shù),指數(shù)中又夾雜著線性增。
然后到了一個閾值,也就是 ssthresh(slow start threshold)的時候就進(jìn)入了擁塞避免階段。
這個階段是每收到一個 ACK 就 cwnd = cwnd + 1/cwnd并且每一個 RTT 就 cwnd++。
可以看到都是線性增。
然后就是一直增,直到開始丟包的情況發(fā)生,前面已經(jīng)分析到重傳有兩種,一種是超時重傳,一種是快速重傳。
如果發(fā)生超時重傳的時候,那說明情況有點糟糕,于是直接把 ssthresh 置為當(dāng)前 cwnd 的一半,然后 cwnd 直接變?yōu)?1,進(jìn)入慢啟動階段。
如果是快速重傳,那么這里有兩種實現(xiàn),一種是 TCP Tahoe ,和超時重傳一樣的處理。
一種是 TCP Reno,這個實現(xiàn)是把 cwnd = cwnd/2 ,然后把 ssthresh 設(shè)置為當(dāng)前的 cwnd 。
然后進(jìn)入快速恢復(fù)階段,將 cwnd = cwnd + 3(因為快速重傳有三次),重傳 DACK 指定的包,如果再收到一個DACK則 cwnd++,如果收到是正常的 ACK 那么就將 cwnd 設(shè)為 ssthresh 大小,進(jìn)入擁塞避免階段。
可以看到快速恢復(fù)就重傳了指定的一個包,那有可能是很多包都丟了,然后其他的包只能等待超時重傳,超時重傳就會導(dǎo)致 cwnd 減半,多次觸發(fā)就指數(shù)級下降。
所以又搞了個 New Reno,多加了個 New,它是在沒有SACK 的情況下改進(jìn)快速恢復(fù),它會觀察重傳 DACK 指定的包的響應(yīng) ACK 是否是已經(jīng)發(fā)送的最大 ACK,比如你發(fā)了1、2、3、4,對方?jīng)]收到 2,但是 3、4都收到了,于是你重傳 2 之后 ACK 肯定是 5,說明就丟了這一個包。
不然就是還有其他包丟了,如果就丟了一個包就是之前的過程一樣,如果還有其他包丟了就繼續(xù)重傳,直到 ACK 是全部的之后再退出快速恢復(fù)階段。
簡單的說就是一直探測到全部包都收到了再結(jié)束這個環(huán)節(jié)。
還有個 FACK,它是基于 SACK 用來作為重傳過程中的擁塞控制,相對于上面的 New Reno 我們就知道它有 SACK 所以不需要一個一個試過去,具體我不展開了。
還有哪些擁塞控制算法?
從維基上看有這么多。
本來我還想嗶嗶幾句了,嗶嗶了之后又刪了,感覺說了和沒說一樣,想深入但是實力不允許,有點惆悵啊。
各位看官自個兒查查吧,或者等我日后修煉有成再來嗶嗶。
總結(jié)
說了這么多來總結(jié)一下吧。
TCP 是面向連接的,提供可靠、有序的傳輸并且還提供流控和擁塞控制,單獨提取出 TCP 層而不是在 IP層實現(xiàn)是因為 IP 層有更多的設(shè)備需要使用,加了復(fù)雜的邏輯不劃算。
三次握手主要是為了定義初始序列號為了之后的傳輸打下基礎(chǔ),四次揮手是因為 TCP 是全雙工協(xié)議,因此雙方都得說拜拜。
SYN 超時了就階梯性重試,如果有 SYN攻擊,可以加大半隊列數(shù),或減少重試次數(shù),或直接拒絕。
TIME_WAIT 是怕對方?jīng)]收到最后一個 ACK,然后又發(fā)了 FIN 過來,并且也是等待處理網(wǎng)絡(luò)上殘留的數(shù)據(jù),怕影響新連接。
TIME_WAIT 不建議設(shè)小,或者破壞 TIME_WAIT 機(jī)制,如果真想那么可以開啟快速回收,或者重用,不過注意受益的對象。
超時重傳是為了保證對端一定能收到包,快速重傳是為了避免在偶爾丟包的時候需要等待超時這么長時間,SACK 是為了讓發(fā)送方知道重傳哪些。
D-SACK 是為了讓發(fā)送方知道這次重傳的原因是對方真的沒收到還是自己太心急了 RTO 整小了,不至于兩眼一抹黑。
滑動窗口是為了平衡發(fā)送方的發(fā)送速率和接收方的接受數(shù)率,不至于瞎發(fā),當(dāng)然還需要注意 Silly Window 的情況,同時還要注意納格算法和延遲確認(rèn)不能一起搭配。
而滑動窗口還不夠,還得有個擁塞控制,因為出行你我他,安全靠大家,TCP 還得跳出來看看關(guān)心下當(dāng)前大局勢。
最后
至此就差不多了,不過還是有很多很多細(xì)節(jié)的,TCP 協(xié)議太復(fù)雜了,這可能是我文章里面圖畫的最少的一篇了,你看復(fù)雜到我圖都畫不來了哈哈哈。
今天我就說了個皮毛,如有紕漏請趕緊后臺聯(lián)系鞭撻我。
巨人的肩膀
https://www.ionos.com/digitalguide/server/know-how/introduction-to-tcp/
https://www.ibm.com/developerworks/cn/linux/l-tcp-sack/
https://coolshell.cn/articles/11564.html/
https://tools.ietf.org/html/rfc793https://nmap.org/book/tcpip-ref.html
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺僅提供信息存儲服務(wù)。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!