通透,23 個問題 TCP 疑難雜症全解析

yes的練級攻略發表於2020-10-19

每個時代,都不會虧待會學習的人。

在進入今天主題之前我先拋幾個問題,這篇文章一共提出 23 個問題。

TCP 握手一定是三次?TCP 揮手一定是四次?

為什麼要有快速重傳,超時重傳不夠用?為什麼要有 SACK,為什麼要有 D-SACK?

都知道有滑動視窗,那由於接收方的太忙了滑動視窗降為了 0 怎麼辦?傳送方就永遠等著了?

Silly Window 又是什麼?

為什麼有滑動視窗流控還需要擁塞控制?

快速重傳一定要依賴三次重複 ACK ?

這篇文章我想由淺到深地過一遍 TCP,不是生硬的搬出各個知識點,從問題入手,然後從發展、演進的角度來看 TCP

起初我在學計算機網路的時候就有非常非常多的疑問,腦子裡簡直充滿了十萬個為什麼,而網路又非常的複雜,發展了這麼多年東西真的太多了,今天我就大致的淺顯地說一說我對 TCP 這些要點的理解

好了,廢話不多說,開始上正菜。

TCP 是用來解決什麼問題?

TCP 即 Transmission Control Protocol,可以看到是一個傳輸控制協議,重點就在這個控制

控制什麼?

控制可靠、按序地傳輸以及端與端之間的流量控制。夠了麼?還不夠,它需要更加智慧,因此還需要加個擁塞控制,需要為整體網路的情況考慮。

這就是出行你我他,安全靠大家

為什麼要 TCP,IP 層實現控制不行麼?

我們知道網路是分層實現的,網路協議的設計就是為了通訊,從鏈路層到 IP 層其實就已經可以完成通訊了。

你看鏈路層不可或缺畢竟我們們電腦都是通過鏈路相互連線的,然後 IP 充當了地址的功能,所以通過 IP 我們們找到了對方就可以進行通訊了。

那加個 TCP 層幹啥?IP 層實現控制不就完事了嘛?

之所以要提取出一個 TCP 層來實現控制是因為 IP 層涉及到的裝置更多,一條資料在網路上傳輸需要經過很多裝置,而裝置之間需要靠 IP 來定址。

假設 IP 層實現了控制,那是不是涉及到的裝置都需要關心很多事情?整體傳輸的效率是不是大打折扣了?

我舉個例子,假如 A 要傳輸給 F 一個積木,但是無法直接傳輸到,需要經過 B、C、D、E 這幾個中轉站之手。
這裡有兩種情況:

  • 假設 BCDE 都需要關心這個積木搭錯了沒,都拆開包裹仔細的看看,沒問題了再裝回去,最終到了 F 的手中。
  • 假設 BCDE 都不關心積木的情況,來啥包裹只管轉發就完事了,由最終的 F 自己來檢查這個積木答錯了沒。

你覺得哪種效率高?明顯是第二種,轉發的裝置不需要關心這些事,只管轉發就完事!

所以把控制的邏輯獨立出來成 TCP 層,讓真正的接收端來處理,這樣網路整體的傳輸效率就高了。

連線到底是什麼?

我們已經知道了為什麼需要獨立出 TCP 這一層,並且這一層主要是用來幹嘛的,接下來就來看看它到底是怎麼幹的。

我們都知道 TCP 是面向連線的,那這個連線到底是個什麼東西?真的是拉了一條線讓端與端之間連起來了?

所謂的連線其實只是雙方都維護了一個狀態,通過每一次通訊來維護狀態的變更,使得看起來好像有一條線關聯了對方。

TCP 協議頭

在具體深入之前我們需要先來看看一些 TCP 頭的格式,這很基礎也很重要。

圖來自網路

我就不一一解釋了,挑重點的說。

首先可以看到 TCP 包只有埠,沒有 IP。

Seq 就是 Sequence Number 即序號,它是用來解決亂序問題的。

ACK 就是 Acknowledgement Numer 即確認號,它是用來解決丟包情況的,告訴傳送方這個包我收到啦。

標誌位就是 TCP flags 用來標記這個包是什麼型別的,用來控制 TPC 的狀態。

視窗就是滑動視窗,Sliding Window,用來流控。

三次握手

明確了協議頭的要點之後,我們再來看三次握手。

三次握手真是個老生常談的問題了,但是真的懂了麼?不是浮在表面?能不能延伸出一些點別的?

我們先來看一下熟悉的流程。

圖來自網路

首先為什麼要握手,其實主要就是為了初始化Seq Numer,SYN 的全稱是 Synchronize Sequence Numbers,這個序號是用來保證之後傳輸資料的順序性。

你要說是為了測試保證雙方傳送接收功能都正常,我覺得也沒毛病,不過我認為重點在於同步序號

那為什麼要三次,就拿我和你這兩個角色來說,首先我告訴你我的初始化序號,你聽到了和我說你收到了。

然後你告訴我你的初始序號,然後我對你說我收到了。

這好像四次了?如果真的按一來一回就是四次,但是中間一步可以合在一起,就是你和我說你知道了我的初始序號的時候同時將你的初始序號告訴我。

因此四次握手就可以減到三次了。

不過你沒有想過這麼一種情形,我和你同時開口,一起告訴對方各自的初始序號,然後分別回應收到了,這不就是四次握手了?

我來畫個圖,清晰一點。

看看是不是四次握手了? 不過具體還是得看實現,有些實現可能不允許這種情況出現,但是這不影響我們思考,因為握手的重點就是同步初始序列號,這種情況也完成了同步的目標。

初始序列號 ISN 的取值

不知道大家有沒有想過 ISN 的值要設成什麼?程式碼寫死從零開始?

想象一下如果寫死一個值,比如 0 ,那麼假設已經建立好連線了,client 也發了很多包比如已經第 20 個包了,然後網路斷了之後 client 重新,埠號還是之前那個,然後序列號又從 0 開始,此時服務端返回第 20 個包的ack,客戶端是不是傻了?

所以 RFC793 中認為 ISN 要和一個假的時鐘繫結在一起
ISN 每四微秒加一,當超過 2 的 32 次方之後又從 0 開始,要四個半小時左右發生 ISN 迴繞

所以 ISN 變成一個遞增值,真實的實現還需要加一些隨機值在裡面,防止被不法份子猜到 ISN。

SYN 超時了怎麼處理?

也就是 client 傳送 SYN 至 server 然後就掛了,此時 server 傳送 SYN+ACK 就一直得不到回覆,怎麼辦?

我腦海中一想到的就是重試,但是不能連續快速重試多次,你想一下,假設 client 掉線了,你總得給它點時間恢復吧,所以呢需要慢慢重試,階梯性重試

在 Linux 中就是預設重試 5 次,並且就是階梯性的重試,間隔就是1s、2s、4s、8s、16s,再第五次發出之後還得等 32s 才能知道這次重試的結果,所以說總共等63s 才能斷開連線。

SYN Flood 攻擊

你看到沒 SYN 超時需要耗費服務端 63s 的時間斷開連線,也就說 63s 內服務端需要保持這個資源,所以不法分子就可以構造出大量的 client 向 server 發 SYN 但就是不回 server。

圖來自網路

使得 server 的 SYN 佇列耗盡,無法處理正常的建連請求。

所以怎麼辦?

可以開啟 tcp_syncookies,那就用不到 SYN 佇列了。

SYN 佇列滿了之後 TCP 根據自己的 ip、埠、然後對方的 ip、埠,對方 SYN 的序號,時間戳等一波操作生成一個特殊的序號(即 cookie)發回去,如果對方是正常的 client 會把這個序號發回來,然後 server 根據這個序號建連。

或者調整 tcp_synack_retries 減少重試的次數,設定 tcp_max_syn_backlog 增加 SYN 佇列數,設定 tcp_abort_on_overflow SYN 佇列滿了直接拒絕連線。

為什麼要四次揮手?

四次揮手和三次握手成雙成對,同樣也是 TCP 中的一線明星,讓我們重溫一下熟悉的圖。

圖來自網路

為什麼揮手需要四次?因為 TCP 是全雙工協議,也就是說雙方都要關閉,每一方都向對方傳送 FIN 和回應 ACK。

就像我對你說我資料發完了,然後你回覆好的你收到了。然後你對我說你資料發完了,然後我向你回覆我收到了。

所以看起來就是四次。

從圖中可以看到主動關閉方的狀態是 FIN_WAIT_1 到 FIN_WAIT_2 然後再到 TIME_WAIT,而被動關閉方是 CLOSE_WAIT 到 LAST_ACK。

四次揮手狀態一定是這樣變遷的嗎

狀態一定是這樣變遷的嗎?讓我們再來看個圖。

圖來自網路

可以看到雙方都主動發起斷開請求所以各自都是主動發起方,狀態會從 FIN_WAIT_1 都進入到 CLOSING 這個過度狀態然後再到 TIME_WAIT。

揮手一定需要四次嗎?

假設 client 已經沒有資料傳送給 server 了,所以它傳送 FIN 給 server 表明自己資料發完了,不再發了,如果這時候 server 還是有資料要傳送給 client 那麼它就是先回復 ack ,然後繼續傳送資料。

等 server 資料傳送完了之後再向 client 傳送 FIN 表明它也發完了,然後等 client 的 ACK 這種情況下就會有四次揮手。

那麼假設 client 傳送 FIN 給 server 的時候 server 也沒資料給 client,那麼 server 就可以將 ACK 和它的 FIN 一起發給client ,然後等待 client 的 ACK,這樣不就三次揮手了?

為什麼要有 TIME_WAIT?

斷開連線發起方在接受到接受方的 FIN 並回復 ACK 之後並沒有直接進入 CLOSED 狀態,而是進行了一波等待,等待時間為 2MSL。

MSL 是 Maximum Segment Lifetime,即報文最長生存時間,RFC 793 定義的 MSL 時間是 2 分鐘,Linux 實際實現是 30s,那麼 2MSL 是一分鐘。

那麼為什麼要等 2MSL 呢?

  • 就是怕被動關閉方沒有收到最後的 ACK,如果被動方由於網路原因沒有到,那麼它會再次傳送 FIN, 此時如果主動關閉方已經 CLOSED 那就傻了,因此等一會兒。

  • 假設立馬斷開連線,但是又重用了這個連線,就是五元組完全一致,並且序號還在合適的範圍內,雖然概率很低但理論上也有可能,那麼新的連線會被已關閉連線鏈路上的一些殘留資料干擾,因此給予一定的時間來處理一些殘留資料。

等待 2MSL 會產生什麼問題?

如果伺服器主動關閉大量的連線,那麼會出現大量的資源佔用,需要等到 2MSL 才會釋放資源。

如果是客戶端主動關閉大量的連線,那麼在 2MSL 裡面那些埠都是被佔用的,埠只有 65535 個,如果埠耗盡了就無法發起送的連線了,不過我覺得這個概率很低,這麼多埠你這是要建立多少個連線?

如何解決 2MSL 產生的問題?

快速回收,即不等 2MSL 就回收, Linux 的引數是 tcp_tw_recycle,還有 tcp_timestamps 不過預設是開啟的。

其實上面我們已經分析過為什麼需要等 2MSL,所以如果等待時間果斷就是出現上面說的那些問題。

所以不建議開啟,而且 Linux 4.12 版本後已經咔擦了這個引數了。

前不久剛有位朋友在群裡就提到了這玩意。

一問果然有 NAT 的身影。

現象就是請求端請求伺服器的靜態資源偶爾會出現 20-60 秒左右才會有響應的情況,從抓包看請求端連續三個 SYN 都沒有回應。

比如你在學校,對外可能就一個公網 IP,然後開啟了 tcp_tw_recycle(tcp_timestamps 也是開啟的情況下),在 60 秒內對於同源 IP 的連線請求中 timestamp 必須是遞增的,不然認為其是過期的資料包就會丟棄。

學校這麼多機器,你無法保證時間戳是一致的,因此就會出問題。

所以這玩意不推薦使用。

重用,即開啟 tcp_tw_reuse 當然也是需要 tcp_timestamps 的。

這裡有個重點,tcp_tw_reuse 是用在連線發起方的,而我們的服務端基本上是連線被動接收方

tcp_tw_reuse 是發起新連線的時候,可以複用超過 1s 的處於 TIME_WAIT 狀態的連線,所以它壓根沒有減少我們服務端的壓力。

它重用的是發起方處於 TIME_WAIT 的連線

這裡還有一個 SO_REUSEADDR ,這玩意有人會和 tcp_tw_reuse 混為一談,首先 tcp_tw_reuse 是核心選項而 SO_REUSEADDR 是使用者態選項。

然後 SO_REUSEADDR 主要用在你啟動服務的時候,如果此時的埠被佔用了並且這個連線處於 TIME_WAIT 狀態,那麼你可以重用這個埠,如果不是 TIME_WAIT,那就是給你個 Address already in use。

所以這兩個玩意好像都不行,而且 tcp_tw_reuse 和tcp_tw_recycle,其實是違反 TCP 協議的,說好的等我到天荒地老,你卻偷偷放了手?

要麼就是調小 MSL 的時間,不過也不太安全,要麼調整 tcp_max_tw_buckets 控制 TIME_WAIT 的數量,不過預設值已經很大了 180000,這玩意應該是用來對抗 DDos 攻擊的。

所以我給出的建議是服務端不要主動關閉,把主動關閉方放到客戶端。畢竟我們們伺服器是一對很多很多服務,我們的資源比較寶貴。

自己攻擊自己

還有一個很騷的解決方案,我自己瞎想的,就是自己攻擊自己。

Socket 有一個選項叫 IP_TRANSPARENT ,可以繫結一個非本地的地址,然後服務端把建連的 ip 和埠都記下來,比如寫入本地某個地方。

然後啟動一個服務,假如現在服務端資源很緊俏,那麼你就定個時間,過了多久之後就將處於 TIME_WAIT 狀態的對方 ip 和埠告訴這個服務。

然後這個服務就利用 IP_TRANSPARENT 偽裝成之前的那個 client 向服務端發起一個請求,然後服務端收到會給真的 client 一個 ACK, 那 client 都關了已經,說你在搞啥子,於是回了一個 RST,然後服務端就中止了這個連線。

超時重傳機制是為了解決什麼問題?

前面我們提到 TCP 要提供可靠的傳輸,那麼網路又是不穩定的如果傳輸的包對方沒收到卻又得保證可靠那麼就必須重傳。

TCP 的可靠性是靠確認號的,比如我發給你1、2、3、4這4個包,你告訴我你現在要 5 那說明前面四個包你都收到了,就是這麼回事兒。

不過這裡要注意,SeqNum 和 ACK 都是以位元組數為單位的,也就是說假設你收到了1、2、4 但是 3 沒有收到你不能 ACK 5,如果你回了 5 那麼傳送方就以為你5之前的都收到了。

所以只能回覆確認最大連續收到包,也就是 3。

而傳送方不清楚 3、4 這兩個包到底是還沒到呢還是已經丟了,於是傳送方需要等待,這等待的時間就比較講究了。

如果太心急可能 ACK 已經在路上了,你這重傳就是浪費資源了,如果太散漫,那麼接收方急死了,這死鬼怎麼還不發包來,我等的花兒都謝了。

所以這個等待超時重傳的時間很關鍵,怎麼搞?聰明的小夥伴可能一下就想到了,你估摸著正常來回一趟時間是多少不就好了,我就等這麼長。

這就來回一趟的時間就叫 RTT,即 Round Trip Time,然後根據這個時間制定超時重傳的時間 RTO,即 Retransmission Timeout。

不過這裡大概只好了 RTO 要參考下 RTT ,但是具體要怎麼算?首先肯定是取樣,然後一波加權平均得到 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 取樣的時間用一開始傳送資料的時間到收到 ACK 的時間作為樣本值還是重傳的時間到 ACK 的時間作為樣本值?

圖來自網路

從圖中就可以看到,一個時間算長了,一個時間算短了,這有點難,因為你不知道這個 ACK 到底是回覆誰的。

所以怎麼辦?發生重傳的來回我不取樣不就好了,我不知道這次 ACK 到底是回覆誰的,我就不管他,我就取樣正常的來回。

這就是 Karn / Partridge 演算法,不取樣重傳的RTT。

但是不取樣重傳會有問題,比如某一時刻網路突然就是很差,你要是不管重傳,那麼還是按照正常的 RTT 來算 RTO, 那麼超時的時間就過短了,於是在網路很差的情況下還瘋狂重傳加重了網路的負載。

因此 Karn 演算法就很粗暴的搞了個發生重傳我就將現在的 RTO 翻倍,哼!就是這麼簡單粗暴。

但是這種平均的計算很容易把一個突然間的大波動,平滑掉,所以又搞了個演算法,叫 Jacobson / Karels Algorithm。

它把最新的 RTT 和平滑過的 SRTT 做了波計算得到合適的 RTO,公式我就不貼了,反正我不懂,不懂就不嗶嗶了。

為什麼還需要快速重傳機制?

超時重傳是按時間來驅動的,如果是網路狀況真的不好的情況,超時重傳沒問題,但是如果網路狀況好的時候,只是恰巧丟包了,那等這麼長時間就沒必要。

於是又引入了資料驅動的重傳叫快速重傳,什麼意思呢?就是傳送方如果連續三次收到對方相同的確認號,那麼馬上重傳資料。

因為連續收到三次相同 ACK 證明當前網路狀況是 ok 的,那麼確認是丟包了,於是立馬重發,沒必要等這麼久。

圖來自網路

看起來好像挺完美的,但是你有沒有想過我傳送1、2、3、4這4個包,就 2 對方沒收到,1、3、4都收到了,然後不管是超時重傳還是快速重傳反正對方就回 ACK 2。

這時候要重傳 2、3、4 呢還是就 2 呢?

SACK 的引入是為了解決什麼問題?

SACK 即 Selective Acknowledgment,它的引入就是為了解決傳送方不知道該重傳哪些資料的問題。

我們來看一下下面的圖就知道了。

圖來自網路

SACK 就是接收方會回傳它已經接受到的資料,這樣傳送方就知道哪一些資料對方已經收到了,所以就可以選擇性的傳送丟失的資料。

如圖,通過 ACK 告知我接下來要 5500 開始的資料,並一直更新 SACK,6000-6500 我收到了,6000-7000的資料我收到了,6000-7500的資料我收到了,傳送方很明確的知道,5500-5999 的那一波資料應該是丟了,於是重傳。

而且如果資料是多段不連續的, SACK 也可以傳送,比如 SACK 0-500,1000-1500,2000-2500。就表明這幾段已經收到了。

D-SACK 又是什麼東西?

D-SACK 其實是 SACK 的擴充套件,它利用 SACK 的第一段來描述重複接受的不連續的資料序號,如果第一段描述的範圍被 ACK 覆蓋,說明重複了,比如我都 ACK 到6000了你還給我回 SACK 5000-5500 呢?

說白了就是從第一段的反饋來和已經接受到的 ACK 比一比,引數是 tcp_dsack,Linux 2.4 之後預設開啟。

那知道重複了有什麼用呢?

1、知道重複了說明對方收到剛才那個包了,所以是回來的 ACK 包丟了。
2、是不是包亂序的,先發的包後到?
3、是不是自己太著急了,RTO 太小了?
4、是不是被資料複製了,搶先一步呢?

滑動視窗乾嘛用?

我們已經知道了 TCP 有序號,並且還有重傳,但是這還不夠,因為我們不是愣頭青,還需要根據情況來控制一下傳送速率,因為網路是複雜多變的,有時候就會阻塞住,而有時候又很通暢。

所以傳送方需要知道接收方的情況,好控制一下傳送的速率,不至於蒙著頭一個勁兒的發然後接受方都接受不過來。

因此 TCP 就有個叫滑動視窗的東西來做流量控制,也就是接收方告訴傳送方我還能接受多少資料,然後傳送方就可以根據這個資訊來進行資料的傳送。

以下是傳送方維護的視窗,就是黑色圈起來的。

圖來自網路

圖中的 #1 是已收到 ACK 的資料,#2 是已經發出去但是還沒收到 ACK 的資料,#3 就是在視窗內可以傳送但是還沒傳送的資料。#4 就是還不能傳送的資料。

然後此時收到了 36 的 ACK,並且發出了 46-51 的位元組,於是視窗向右滑動了。

圖片來自網路

TCP/IP Guide 上還有一張完整的圖,畫的十分清晰,大家看一下。

如果接收方回覆的視窗一直是 0 怎麼辦?

上文已經說了傳送方式根據接收方回應的 window 來控制能發多少資料,如果接收方一直回應 0,那傳送方就杵著?

你想一下,傳送方發的資料都得到 ACK 了,但是呢回應的視窗都是 0 ,這傳送方此時不敢發了啊,那也不能一直等著啊,這 Window 啥時候不變 0 啊?

於是 TCP 有一個 Zero Window Probe 技術,傳送方得知視窗是 0 之後,會去探測探測這個接收方到底行不行,也就是傳送 ZWP 包給接收方。

具體看實現了,可以傳送多次,然後還有間隔時間,多次之後都不行可以直接 RST。

假設接收方每次回應視窗都很小怎麼辦?

你想象一下,如果每次接收方都說我還能收 1 個位元組,傳送方該不該發?

TCP + IP 頭部就 40 個位元組了,這傳輸不划算啊,如果傻傻的一直髮這就叫 Silly Window。

那咋辦,一想就是傳送端等著,等養肥了再發,要麼接收端自己自覺點,資料小於一個閾值就告訴傳送端視窗此時是 0 算了,也等養肥了再告訴傳送端。

傳送端等著的方案就是納格演算法,這個演算法相信看一下程式碼就知道了。

簡單的說就是當前能傳送的資料和視窗大於等於 MSS 就立即傳送,否則再判斷一下之前傳送的包 ACK 回來沒,回來再發,不然就攢資料。

接收端自覺點的方案是 David D Clark’s 方案,如果視窗資料小於某個閾值就告訴傳送方視窗 0 別發,等緩過來資料大於等於 MSS 或者接受 buffer 騰出一半空間了再設定正常的 window 值給傳送方。

對了提到納格演算法不得不再提一下延遲確認,納格演算法在等待接收方的確認,而開啟延遲確認則會延遲傳送確認,會等之後的包收到了再一起確認或者等待一段時候真的沒了再回復確認。

這就相互等待了,然後延遲就很大了,兩個不可同時開啟。

已經有滑動視窗了為什麼還要擁塞控制?

前面我已經提到了,加了擁塞控制是因為 TCP 不僅僅就管兩端之間的情況,還需要知曉一下整體的網路情形,畢竟只有大家都守規矩了道路才會通暢。

前面我們提到了重傳,如果不管網路整體的情況,肯定就是對方沒給 ACK ,那我就無腦重傳。

如果此時網路狀況很差,所有的連線都這樣無腦重傳,是不是網路情況就更差了,更加擁堵了?

然後越擁堵越重傳,一直衝沖沖!然後就 GG 了。

所以需要個擁塞控制,來避免這種情況的傳送。

擁塞控制怎麼搞?

主要有以下幾個步驟來搞:

1、慢啟動,探探路。
2、擁塞避免,感覺差不多了減速看看
3、擁塞發生快速重傳/恢復

慢啟動,就是新司機上路慢慢來,初始化 cwnd(Congestion Window)為 1,然後每收到一個 ACK 就 cwnd++ 並且每過一個 RTT ,cwnd = 2*cwnd 。

線性中帶著指數,指數中又夾雜著線性增。

然後到了一個閾值,也就是 ssthresh(slow start threshold)的時候就進入了擁塞避免階段。

這個階段是每收到一個 ACK 就 cwnd = cwnd + 1/cwnd並且每一個 RTT 就 cwnd++。

可以看到都是線性增。

然後就是一直增,直到開始丟包的情況發生,前面已經分析到重傳有兩種,一種是超時重傳,一種是快速重傳。

如果發生超時重傳的時候,那說明情況有點糟糕,於是直接把 ssthresh 置為當前 cwnd 的一半,然後 cwnd 直接變為 1,進入慢啟動階段。

如果是快速重傳,那麼這裡有兩種實現,一種是 TCP Tahoe ,和超時重傳一樣的處理。

一種是 TCP Reno,這個實現是把 cwnd = cwnd/2 ,然後把 ssthresh 設定為當前的 cwnd 。

然後進入快速恢復階段,將 cwnd = cwnd + 3(因為快速重傳有三次),重傳 DACK 指定的包,如果再收到一個DACK則 cwnd++,如果收到是正常的 ACK 那麼就將 cwnd 設為 ssthresh 大小,進入擁塞避免階段。

可以看到快速恢復就重傳了指定的一個包,那有可能是很多包都丟了,然後其他的包只能等待超時重傳,超時重傳就會導致 cwnd 減半,多次觸發就指數級下降。

所以又搞了個 New Reno,多加了個 New,它是在沒有SACK 的情況下改進快速恢復,它會觀察重傳 DACK 指定的包的響應 ACK 是否是已經傳送的最大 ACK,比如你發了1、2、3、4,對方沒收到 2,但是 3、4都收到了,於是你重傳 2 之後 ACK 肯定是 5,說明就丟了這一個包。

不然就是還有其他包丟了,如果就丟了一個包就是之前的過程一樣,如果還有其他包丟了就繼續重傳,直到 ACK 是全部的之後再退出快速恢復階段。

簡單的說就是一直探測到全部包都收到了再結束這個環節。

還有個 FACK,它是基於 SACK 用來作為重傳過程中的擁塞控制,相對於上面的 New Reno 我們就知道它有 SACK 所以不需要一個一個試過去,具體我不展開了。

還有哪些擁塞控制演算法?

從維基上看有這麼多。

本來我還想嗶嗶幾句了,嗶嗶了之後又刪了,感覺說了和沒說一樣,想深入但是實力不允許,有點惆悵啊。

各位看官自個兒查查吧,或者等我日後修煉有成再來嗶嗶。

總結

說了這麼多來總結一下吧。

TCP 是面向連線的,提供可靠、有序的傳輸並且還提供流控和擁塞控制,單獨提取出 TCP 層而不是在 IP層實現是因為 IP 層有更多的裝置需要使用,加了複雜的邏輯不划算。

三次握手主要是為了定義初始序列號為了之後的傳輸打下基礎,四次揮手是因為 TCP 是全雙工協議,因此雙方都得說拜拜。

SYN 超時了就階梯性重試,如果有 SYN攻擊,可以加大半佇列數,或減少重試次數,或直接拒絕。

TIME_WAIT 是怕對方沒收到最後一個 ACK,然後又發了 FIN 過來,並且也是等待處理網路上殘留的資料,怕影響新連線。

TIME_WAIT 不建議設小,或者破壞 TIME_WAIT 機制,如果真想那麼可以開啟快速回收,或者重用,不過注意受益的物件。

超時重傳是為了保證對端一定能收到包,快速重傳是為了避免在偶爾丟包的時候需要等待超時這麼長時間,SACK 是為了讓傳送方知道重傳哪些。

D-SACK 是為了讓傳送方知道這次重傳的原因是對方真的沒收到還是自己太心急了 RTO 整小了,不至於兩眼一抹黑。

滑動視窗是為了平衡傳送方的傳送速率和接收方的接受數率,不至於瞎發,當然還需要注意 Silly Window 的情況,同時還要注意納格演算法和延遲確認不能一起搭配。

而滑動視窗還不夠,還得有個擁塞控制,因為出行你我他,安全靠大家,TCP 還得跳出來看看關心下當前大局勢。

最後

至此就差不多了,不過還是有很多很多細節的,TCP 協議太複雜了,這可能是我文章裡面圖畫的最少的一篇了,你看複雜到我圖都畫不來了哈哈哈。

今天我就說了個皮毛,如有紕漏請趕緊後臺聯絡鞭撻我。

巨人的肩膀

http://www.tcpipguide.com/
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/rfc793
https://nmap.org/book/tcpip-ref.html


我是 yes,從一點點到億點點,我們下篇見

相關文章