自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

徹底弄懂TCP協(xié)議:從三次握手說起

開發(fā) 開發(fā)工具
本文就是來說說這些頭疼點的,淺談一些 TCP 的疑難雜癥。那么從哪說起呢?當然是從三次握手和四次揮手說起啦,可能大家都知道 TCP 是三次交互完成連接的建立,四次交互來斷開一個連接,那為什么是三次握手和四次揮手呢?反過來不行嗎?

[[339497]]

作者:morganhuang,騰訊 IEG 后臺開發(fā)工程師

說到 TCP 協(xié)議,相信大家都比較熟悉了,對于 TCP 協(xié)議總能說個一二三來,但是 TCP 協(xié)議又是一個非常復雜的協(xié)議,其中有不少細節(jié)點讓人頭疼點。本文就是來說說這些頭疼點的,淺談一些 TCP 的疑難雜癥。那么從哪說起呢?當然是從三次握手和四次揮手說起啦,可能大家都知道 TCP 是三次交互完成連接的建立,四次交互來斷開一個連接,那為什么是三次握手和四次揮手呢?反過來不行嗎?

疑癥(1)TCP 的三次握手、四次揮手

下面兩圖大家再熟悉不過了,TCP 的三次握手和四次揮手見下面左邊的”TCP 建立連接”、”TCP 數(shù)據(jù)傳送”、”TCP 斷開連接”時序圖和右邊的”TCP 協(xié)議狀態(tài)機” 。

TCP三次握手、四次揮手時序圖

TCP協(xié)議狀態(tài)機

要弄清 TCP 建立連接需要幾次交互才行,我們需要弄清建立連接進行初始化的目標是什么。TCP 進行握手初始化一個連接的目標是:分配資源、初始化序列號(通知 peer 對端我的初始序列號是多少),知道初始化連接的目標,那么要達成這個目標的過程就簡單了,握手過程可以簡化為下面的四次交互:

1)client 端首先發(fā)送一個 SYN 包告訴 Server 端我的初始序列號是 X;2)Server 端收到 SYN 包后回復給 client 一個 ACK 確認包,告訴 client 說我收到了;3)接著 Server 端也需要告訴 client 端自己的初始序列號,于是 Server 也發(fā)送一個 SYN 包告訴 client 我的初始序列號是 Y;4)Client 收到后,回復 Server 一個 ACK 確認包說我知道了。

整個過程 4 次交互即可完成初始化,但是,細心的同學會發(fā)現(xiàn)兩個問題:

  • Server 發(fā)送 SYN 包是作為發(fā)起連接的 SYN 包,還是作為響應發(fā)起者的 SYN 包呢?怎么區(qū)分?比較容易引起混淆
  • Server 的 ACK 確認包和接下來的 SYN 包可以合成一個 SYN ACK 包一起發(fā)送的,沒必要分別單獨發(fā)送,這樣省了一次交互同時也解決了問題[1].這樣 TCP 建立一個連接,三次握手在進行最少次交互的情況下完成了 Peer 兩端的資源分配和初始化序列號的交換。

大部分情況下建立連接需要三次握手,也不一定都是三次,有可能出現(xiàn)四次握手來建立連接的。如下圖,當 Peer 兩端同時發(fā)起 SYN 來建立連接的時候,就出現(xiàn)了四次握手來建立連接(對于有些 TCP/IP 的實現(xiàn),可能不支持這種同時打開的情況)。

在三次握手過程中,細心的同學可能會有以下疑問:

  • 初始化序列號 X、Y 是可以是寫死固定的嗎,為什么不能呢?
  • 假如 Client 發(fā)送一個 SYN 包給 Server 后就掛了或是不管了,這個時候這個連接處于什么狀態(tài)呢?會超時嗎?為什么呢?

TCP 進行斷開連接的目標是:回收資源、終止數(shù)據(jù)傳輸。由于 TCP 是全雙工的,需要 Peer 兩端分別各自拆除自己通向 Peer 對端的方向的通信信道。這樣需要四次揮手來分別拆除通信信道,就比較清晰明了了。

1)Client 發(fā)送一個 FIN 包來告訴 Server 我已經(jīng)沒數(shù)據(jù)需要發(fā)給 Server 了;2)Server 收到后回復一個 ACK 確認包說我知道了;3)然后 server 在自己也沒數(shù)據(jù)發(fā)送給 client 后,Server 也發(fā)送一個 FIN 包給 Client 告訴 Client 我也已經(jīng)沒數(shù)據(jù)發(fā)給 client 了;4)Client 收到后,就會回復一個 ACK 確認包說我知道了。

到此,四次揮手,這個 TCP 連接就可以完全拆除了。在四次揮手的過程中,細心的同學可能會有以下疑問:

  • Client 和 Server 同時發(fā)起斷開連接的 FIN 包會怎么樣呢,TCP 狀態(tài)是怎么轉移的?
  • 左側圖中的四次揮手過程中,Server 端的 ACK 確認包能不能和接下來的 FIN 包合并成一個包呢,這樣四次揮手就變成三次揮手了。
  • 四次揮手過程中,首先斷開連接的一端,在回復最后一個 ACK 后,為什么要進行 TIME_WAIT 呢(超時設置是 2*MSL,RFC793 定義了 MSL 為 2 分鐘,Linux 設置成了 30s),在 TIME_WAIT 的時候又不能釋放資源,白白讓資源占用那么長時間,能不能省了 TIME_WAIT 呢,為什么?

疑癥(2)TCP 連接的初始化序列號能否固定

如果初始化序列號(縮寫為 ISN:Inital Sequence Number)可以固定,我們來看看會出現(xiàn)什么問題。假設 ISN 固定是 1,Client 和 Server 建立好一條 TCP 連接后,Client 連續(xù)給 Server 發(fā)了 10 個包,這 10 個包不知怎么被鏈路上的路由器緩存了(路由器會毫無先兆地緩存或者丟棄任何的數(shù)據(jù)包),這個時候碰巧 Client 掛掉了,然后 Client 用同樣的端口號重新連上 Server,Client 又連續(xù)給 Server 發(fā)了幾個包,假設這個時候 Client 的序列號變成了 5。

接著,之前被路由器緩存的 10 個數(shù)據(jù)包全部被路由到 Server 端了,Server 給 Client 回復確認號 10,這個時候,Client 整個都不好了,這是什么情況?我的序列號才到 5,你怎么給我的確認號是 10 了,整個都亂了。RFC793 中,建議 ISN 和一個假的時鐘綁在一起,這個時鐘會在每 4 微秒對 ISN 做加一操作,直到超過 2^32,又從 0 開始,這需要 4 小時才會產(chǎn)生 ISN 的回繞問題,這幾乎可以保證每個新連接的 ISN 不會和舊的連接的 ISN 產(chǎn)生沖突。這種遞增方式的 ISN,很容易讓攻擊者猜測到 TCP 連接的 ISN,現(xiàn)在的實現(xiàn)大多是在一個基準值的基礎上進行隨機的。

疑癥(3)初始化連接的 SYN 超時問題

Client 發(fā)送 SYN 包給 Server 后掛了,Server 回給 Client 的 SYN-ACK 一直沒收到 Client 的 ACK 確認,這個時候這個連接既沒建立起來,也不能算失敗。這就需要一個超時時間讓 Server 將這個連接斷開,否則這個連接就會一直占用 Server 的 SYN 連接隊列中的一個位置,大量這樣的連接就會將 Server 的 SYN 連接隊列耗盡,讓正常的連接無法得到處理。目前,Linux 下默認會進行 5 次重發(fā) SYN-ACK 包,重試的間隔時間從 1s 開始,下次的重試間隔時間是前一次的雙倍,5 次的重試時間間隔為 1s,2s, 4s, 8s,16s,總共 31s,第 5 次發(fā)出后還要等 32s 都知道第 5 次也超時了,所以,總共需要 1s + 2s +4s+ 8s+ 16s + 32s =63s,TCP 才會把斷開這個連接。

由于,SYN 超時需要 63 秒,那么就給攻擊者一個攻擊服務器的機會,攻擊者在短時間內發(fā)送大量的 SYN 包給 Server(俗稱 SYN flood 攻擊),用于耗盡 Server 的 SYN 隊列。對于應對 SYN 過多的問題,linux 提供了幾個 TCP 參數(shù):tcp_syncookies、tcp_synack_retries、tcp_max_syn_backlog、tcp_abort_on_overflow 來調整應對。

疑癥(4) TCP 的 Peer 兩端同時斷開連接

由上面的”TCP 協(xié)議狀態(tài)機“圖可以看出,TCP 的 Peer 端在收到對端的 FIN 包前發(fā)出了 FIN 包,那么該 Peer 的狀態(tài)就變成了 FIN_WAIT1,Peer 在 FIN_WAIT1 狀態(tài)下收到對端 Peer 對自己 FIN 包的 ACK 包的話,那么 Peer 狀態(tài)就變成 FIN_WAIT2,Peer 在 FIN_WAIT2 下收到對端 Peer 的 FIN 包,在確認已經(jīng)收到了對端 Peer 全部的 Data 數(shù)據(jù)包后,就響應一個 ACK 給對端 Peer,然后自己進入 TIME_WAIT 狀態(tài)。

但是如果 Peer 在 FIN_WAIT1 狀態(tài)下首先收到對端 Peer 的 FIN 包的話,那么該 Peer 在確認已經(jīng)收到了對端 Peer 全部的 Data 數(shù)據(jù)包后,就響應一個 ACK 給對端 Peer,然后自己進入 CLOSEING 狀態(tài),Peer 在 CLOSEING 狀態(tài)下收到自己的 FIN 包的 ACK 包的話,那么就進入 TIME WAIT 狀態(tài)。于是,TCP 的 Peer 兩端同時發(fā)起 FIN 包進行斷開連接,那么兩端 Peer 可能出現(xiàn)完全一樣的狀態(tài)轉移 FIN_WAIT1——>CLOSEING——->TIME_WAIT,也就會 Client 和 Server 最后同時進入 TIME_WAIT 狀態(tài)。同時關閉連接的狀態(tài)轉移如下圖所示:

疑癥(5)四次揮手能不能變成三次揮手呢??

答案是可能的。TCP 是全雙工通信,Cliet 在自己已經(jīng)不會在有新的數(shù)據(jù)要發(fā)送給 Server 后,可以發(fā)送 FIN 信號告知 Server,這邊已經(jīng)終止 Client 到對端 Server 那邊的數(shù)據(jù)傳輸。但是,這個時候對端 Server 可以繼續(xù)往 Client 這邊發(fā)送數(shù)據(jù)包。于是,兩端數(shù)據(jù)傳輸?shù)慕K止在時序上是獨立并且可能會相隔比較長的時間,這個時候就必須最少需要 2+2= 4 次揮手來完全終止這個連接。但是,如果 Server 在收到 Client 的 FIN 包后,在也沒數(shù)據(jù)需要發(fā)送給 Client 了,那么對 Client 的 ACK 包和 Server 自己的 FIN 包就可以合并成為一個包發(fā)送過去,這樣四次揮手就可以變成三次了(似乎 linux 協(xié)議棧就是這樣實現(xiàn)的)

疑癥(6) TCP 的頭號疼癥 TIME_WAIT 狀態(tài)

要說明 TIME_WAIT 的問題,需要解答以下幾個問題

一、Peer 兩端,哪一端會進入 TIME_WAIT 呢?為什么?

相信大家都知道,TCP 主動關閉連接的那一方會最后進入 TIME_WAIT。那么怎么界定主動關閉方呢?是否主動關閉是由 FIN 包的先后決定的,就是在自己沒收到對端 Peer 的 FIN 包之前自己發(fā)出了 FIN 包,那么自己就是主動關閉連接的那一方。對于疑癥(4)中描述的情況,那么 Peer 兩邊都是主動關閉的一方,兩邊都會進入 TIME_WAIT。為什么是主動關閉的一方進行 TIME_WAIT 呢,被動關閉的進入 TIME_WAIT 可以不呢?我們來看看 TCP 四次揮手可以簡單分為下面三個過程:

過程一.主動關閉方發(fā)送 FIN;過程二.被動關閉方收到主動關閉方的 FIN 后發(fā)送該 FIN 的 ACK,被動關閉方發(fā)送 FIN;過程三.主動關閉方收到被動關閉方的 FIN 后發(fā)送該 FIN 的 ACK,被動關閉方等待自己 FIN 的 ACK。

問題就在過程三中,據(jù) TCP 協(xié)議規(guī)范,不對 ACK 進行 ACK,如果主動關閉方不進入 TIME_WAIT,那么主動關閉方在發(fā)送完 ACK 就走了的話,如果最后發(fā)送的 ACK 在路由過程中丟掉了,最后沒能到被動關閉方,這個時候被動關閉方?jīng)]收到自己 FIN 的 ACK 就不能關閉連接,接著被動關閉方會超時重發(fā) FIN 包,但是這個時候已經(jīng)沒有對端會給該 FIN 回 ACK,被動關閉方就無法正常關閉連接了,所以主動關閉方需要進入 TIME_WAIT 以便能夠重發(fā)丟掉的被動關閉方 FIN 的 ACK。

二、TIME_WAIT 狀態(tài)是用來解決或避免什么問題呢?

TIME_WAIT 主要是用來解決以下幾個問題:

1)上面解釋為什么主動關閉方需要進入 TIME_WAIT 狀態(tài)中提到的:主動關閉方需要進入 TIME_WAIT 以便能夠重發(fā)丟掉的被動關閉方 FIN 包的 ACK。如果主動關閉方不進入 TIME_WAIT,那么在主動關閉方對被動關閉方 FIN 包的 ACK 丟失了的時候,被動關閉方由于沒收到自己 FIN 的 ACK,會進行重傳 FIN 包,這個 FIN 包到主動關閉方后,由于這個連接已經(jīng)不存在于主動關閉方了,這個時候主動關閉方無法識別這個 FIN 包,協(xié)議棧會認為對方瘋了,都還沒建立連接你給我來個 FIN 包?,于是回復一個 RST 包給被動關閉方,被動關閉方就會收到一個錯誤(我們見的比較多的:connect reset by peer,這里順便說下 Broken pipe,在收到 RST 包的時候,還往這個連接寫數(shù)據(jù),就會收到 Broken pipe 錯誤了),原本應該正常關閉的連接,給我來個錯誤,很難讓人接受。

2)防止已經(jīng)斷開的連接 1 中在鏈路中殘留的 FIN 包終止掉新的連接 2(重用了連接 1 的所有的 5 元素(源 IP,目的 IP,TCP,源端口,目的端口)),這個概率比較低,因為涉及到一個匹配問題,遲到的 FIN 分段的序列號必須落在連接 2 的一方的期望序列號范圍之內,雖然概率低,但是確實可能發(fā)生,因為初始序列號都是隨機產(chǎn)生的,并且這個序列號是 32 位的,會回繞。

3)防止鏈路上已經(jīng)關閉的連接的殘余數(shù)據(jù)包(a lost duplicate packet or a wandering duplicate packet) 干擾正常的數(shù)據(jù)包,造成數(shù)據(jù)流的不正常。這個問題和 2)類似。

三、TIME_WAIT 會帶來哪些問題呢?

TIME_WAIT 帶來的問題注意是源于:一個連接進入 TIME_WAIT 狀態(tài)后需要等待 2*MSL(一般是 1 到 4 分鐘)那么長的時間才能斷開連接釋放連接占用的資源,會造成以下問題:

  • 作為服務器,短時間內關閉了大量的 Client 連接,就會造成服務器上出現(xiàn)大量的 TIME_WAIT 連接,占據(jù)大量的 tuple,嚴重消耗著服務器的資源。
  • 作為客戶端,短時間內大量的短連接,會大量消耗的 Client 機器的端口,畢竟端口只有 65535 個,端口被耗盡了,后續(xù)就無法在發(fā)起新的連接了。

由于上面兩個問題,作為客戶端需要連本機的一個服務的時候,首選 UNIX 域套接字而不是 TCP)。TIME_WAIT 很令人頭疼,很多問題是由 TIME_WAIT 造成的,但是 TIME_WAIT 又不是多余的不能簡單將 TIME_WAIT 去掉,那么怎么來解決或緩解 TIME_WAIT 問題呢?可以進行 TIME_WAIT 的快速回收和重用來緩解 TIME_WAIT 的問題。有沒一些清掉 TIME_WAIT 的技巧呢?

四、TIME_WAIT 的快速回收和重用

【1】TIME_WAIT 快速回收linux 下開啟 TIME_WAIT 快速回收需要同時打開 tcp_tw_recycle 和 tcp_timestamps(默認打開)兩選項。Linux 下快速回收的時間為 3.5* RTO(Retransmission Timeout),而一個 RTO 時間為 200ms 至 120s。開啟快速回收 TIME_WAIT,可能會帶來(問題一、)中說的三點危險,為了避免這些危險,要求同時滿足以下三種情況的新連接要被拒絕掉:

  • 來自同一個對端 Peer 的 TCP 包攜帶了時間戳;
  • 之前同一臺 peer 機器(僅僅識別 IP 地址,因為連接被快速釋放了,沒了端口信息)的某個 TCP 數(shù)據(jù)在 MSL 秒之內到過本 Server;
  • Peer 機器新連接的時間戳小于 peer 機器上次 TCP 到來時的時間戳,且差值大于重放窗口戳(TCP_PAWS_WINDOW)。

初看起來正常的數(shù)據(jù)包同時滿足下面 3 條幾乎不可能,因為機器的時間戳不可能倒流的,出現(xiàn)上述的 3 點均滿足時,一定是老的重復數(shù)據(jù)包又回來了,丟棄老的 SYN 包是正常的。到此,似乎啟用快速回收就能很大程度緩解 TIME_WAIT 帶來的問題。但是,這里忽略了一個東西就是 NAT。

在一個 NAT 后面的所有 Peer 機器在 Server 看來都是一個機器,NAT 后面的那么多 Peer 機器的系統(tǒng)時間戳很可能不一致,有些快,有些慢。這樣,在 Server 關閉了與系統(tǒng)時間戳快的 Client 的連接后,在這個連接進入快速回收的時候,同一 NAT 后面的系統(tǒng)時間戳慢的 Client 向 Server 發(fā)起連接,這就很有可能同時滿足上面的三種情況,造成該連接被 Server 拒絕掉。所以,在是否開啟 tcp_tw_recycle 需要慎重考慮了

【2】TIME_WAIT 重用

linux 上比較完美的實現(xiàn)了 TIME_WAIT 重用問題。只要滿足下面兩點中的一點,一個 TW 狀態(tài)的四元組(即一個 socket 連接)可以重新被新到來的 SYN 連接使用。

[1]. 新連接 SYN 告知的初始序列號比 TIME_WAIT 老連接的末序列號大;[2]. 如果開啟了 tcp_timestamps,并且新到來的連接的時間戳比老連接的時間戳大。

要同時開啟 tcp_tw_reuse 選項和 tcp_timestamps 選項才可以開啟 TIME_WAIT 重用,還有一個條件是:重用 TIME_WAIT 的條件是收到最后一個包后超過 1s。細心的同學可能發(fā)現(xiàn) TIME_WAIT 重用對 Server 端來說并沒解決大量 TIME_WAIT 造成的資源消耗的問題,因為不管 TIME_WAIT 連接是否被重用,它依舊占用著系統(tǒng)資源。即便如此,TIME_WAIT 重用還是有些用處的,它解決了整機范圍拒絕接入的問題,雖然一般一個單獨的 Client 是不可能在 MSL 內用同一個端口連接同一個服務的,但是如果 Client 做了 bind 端口那就是同個端口了。時間戳重用 TIME_WAIT 連接的機制的前提是 IP 地址唯一性,得出新請求發(fā)起自同一臺機器,但是如果是 NAT 環(huán)境下就不能這樣保證了,于是在 NAT 環(huán)境下,TIME_WAIT 重用還是有風險的。

有些同學可能會混淆 tcp_tw_reuse 和 SO_REUSEADDR 選項,認為是相關的一個東西,其實他們是兩個完全不同的東西,可以說兩個半毛錢關系都沒。tcp_tw_reuse 是內核選項,而 SO_REUSEADDR 用戶態(tài)的選項,使用 SO_REUSEADDR 是告訴內核,如果端口忙,但 TCP 狀態(tài)位于 TIME_WAIT,可以重用端口。如果端口忙,而 TCP 狀態(tài)位于其他狀態(tài),重用端口時依舊得到一個錯誤信息,指明 Address already in use”。如果你的服務程序停止后想立即重啟,而新套接字依舊使用同一端口,此時 SO_REUSEADDR 選項非常有用。但是,使用這個選項就會有(問題二、)中說的三點危險,雖然發(fā)生的概率不大。

五、清掉 TIME_WAIT 的奇技怪巧

可以用下面兩種方式控制服務器的 TIME_WAIT 數(shù)量:

【1】修改 tcp_max_tw_buckets

tcp_max_tw_buckets 控制并發(fā)的 TIME_WAIT 的數(shù)量,默認值是 180000。如果超過默認值,內核會把多的 TIME_WAIT 連接清掉,然后在日志里打一個警告。官網(wǎng)文檔說這個選項只是為了阻止一些簡單的 DoS 攻擊,平常不要人為的降低它。

【2】利用 RST 包從外部清掉 TIME_WAIT 鏈接

根據(jù) TCP 規(guī)范,收到任何的發(fā)送到未偵聽端口、已經(jīng)關閉的連接的數(shù)據(jù)包、連接處于任何非同步狀態(tài)(LISTEN,SYS-SENT,SYN-RECEIVED)并且收到的包的 ACK 在窗口外,或者安全層不匹配,都要回執(zhí)以 RST 響應(而收到滑動窗口外的序列號的數(shù)據(jù)包,都要丟棄這個數(shù)據(jù)包,并回復一個 ACK 包),內核收到 RST 將會產(chǎn)生一個錯誤并終止該連接。我們可以利用 RST 包來終止掉處于 TIME_WAIT 狀態(tài)的連接,其實這就是所謂的 RST 攻擊了。

為了描述方便:假設 Client 和 Server 有個連接 Connect1,Server 主動關閉連接并進入了 TIME_WAIT 狀態(tài),我們來描述一下怎么從外部使得 Server 的處于 TIME_WAIT 狀態(tài)的連接 Connect1 提前終止掉。要實現(xiàn)這個 RST 攻擊,首先我們要知道 Client 在 Connect1 中的端口 port1(一般這個端口是隨機的,比較難猜到,這也是 RST 攻擊較難的一個點),利用 IP_TRANSPARENT 這個 socket 選項,它可以 bind 不屬于本地的地址,因此可以從任意機器綁定 Client 地址以及端口 port1,然后向 Server 發(fā)起一個連接,Server 收到了窗口外的包于是響應一個 ACK,這個 ACK 包會路由到 Client 處。

這個時候 99%的可能 Client 已經(jīng)釋放連接 connect1 了,這個時候 Client 收到這個 ACK 包,會發(fā)送一個 RST 包,server 收到 RST 包然后就釋放連接 connect1 提前終止 TIME_WAIT 狀態(tài)了。提前終止 TIME_WAIT 狀態(tài)是可能會帶來(問題二)中說的三點危害,具體的危害情況可以看下 RFC1337。RFC1337 中建議,不要用 RST 過早的結束 TIME_WAIT 狀態(tài)。

至此,上面的疑癥都解析完畢,然而細心的同學會有下面的疑問:

  • TCP 的可靠傳輸是確認號來實現(xiàn)的,那么 TCP 的確認機制是怎樣的呢?是收到一個包就馬上確認,還是可以稍等一下在確認呢?
  • 假如發(fā)送一個包,一直都沒收到確認呢?什么時候重傳呢?超時機制的怎樣的?
  • TCP 兩端 Peer 的處理能力不對等的時候,比如發(fā)送方處理能力很強,接收方處理能力很弱,這樣發(fā)送方是否能夠不管接收方死活狂發(fā)數(shù)據(jù)呢?如果不能,流量控制機制的如何的?
  • TCP 是端到端的協(xié)議,也就是 TCP 對端 Peer 只看到對方,看不到網(wǎng)絡上的其他點,那么 TCP 的兩端怎么對網(wǎng)絡情況做出反映呢?發(fā)生擁塞的時候,擁塞控制機制是如何的?

疑癥(7)TCP 的延遲確認機制

按照 TCP 協(xié)議,確認機制是累積的,也就是確認號 X 的確認指示的是所有 X 之前但不包括 X 的數(shù)據(jù)已經(jīng)收到了。確認號(ACK)本身就是不含數(shù)據(jù)的分段,因此大量的確認號消耗了大量的帶寬,雖然大多數(shù)情況下,ACK 還是可以和數(shù)據(jù)一起捎帶傳輸?shù)?,但是如果沒有捎帶傳輸,那么就只能單獨回來一個 ACK,如果這樣的分段太多,網(wǎng)絡的利用率就會下降。為緩解這個問題,RFC 建議了一種延遲的 ACK,也就是說,ACK 在收到數(shù)據(jù)后并不馬上回復,而是延遲一段可以接受的時間,延遲一段時間的目的是看能不能和接收方要發(fā)給發(fā)送方的數(shù)據(jù)一起回去,因為 TCP 協(xié)議頭中總是包含確認號的,如果能的話,就將數(shù)據(jù)一起捎帶回去,這樣網(wǎng)絡利用率就提高了。

延遲 ACK 就算沒有數(shù)據(jù)捎帶,那么如果收到了按序的兩個包,那么只要對第二包做確認即可,這樣也能省去一個 ACK 消耗。由于 TCP 協(xié)議不對 ACK 進行 ACK 的,RFC 建議最多等待 2 個包的積累確認,這樣能夠及時通知對端 Peer,我這邊的接收情況。Linux 實現(xiàn)中,有延遲 ACK 和快速 ACK,并根據(jù)當前的包的收發(fā)情況來在這兩種 ACK 中切換。一般情況下,ACK 并不會對網(wǎng)絡性能有太大的影響,延遲 ACK 能減少發(fā)送的分段從而節(jié)省了帶寬,而快速 ACK 能及時通知發(fā)送方丟包,避免滑動窗口停等,提升吞吐率。

關于 ACK 分段,有個細節(jié)需要說明一下,ACK 的確認號,是確認按序收到的最后一個字節(jié)序,對于亂序到來的 TCP 分段,接收端會回復相同的 ACK 分段,只確認按序到達的最后一個 TCP 分段。TCP 連接的延遲確認時間一般初始化為最小值 40ms,隨后根據(jù)連接的重傳超時時間(RTO)、上次收到數(shù)據(jù)包與本次接收數(shù)據(jù)包的時間間隔等參數(shù)進行不斷調整。

疑癥(8)TCP 的重傳機制以及重傳的超時計算

【1】TCP 的重傳超時計算

TCP 交互過程中,如果發(fā)送的包一直沒收到 ACK 確認,是要一直等下去嗎?顯然不能一直等(如果發(fā)送的包在路由過程中丟失了,對端都沒收到又如何給你發(fā)送確認呢?),這樣協(xié)議將不可用,既然不能一直等下去,那么該等多久呢?等太長時間的話,數(shù)據(jù)包都丟了很久了才重發(fā),沒有效率,性能差;等太短時間的話,可能 ACK 還在路上快到了,這時候卻重傳了,造成浪費,同時過多的重傳會造成網(wǎng)絡擁塞,進一步加劇數(shù)據(jù)的丟失。也是,我們不能去猜測一個重傳超時時間,應該是通過一個算法去計算,并且這個超時時間應該是隨著網(wǎng)絡的狀況在變化的。為了使我們的重傳機制更高效,如果我們能夠比較準確知道在當前網(wǎng)絡狀況下,一個數(shù)據(jù)包從發(fā)出去到回來的時間 RTT——Round Trip Time,那么根據(jù)這個 RTT 我們就可以方便設置 TimeOut——RTO(Retransmission TimeOut)了。

為了計算這個 RTO,RFC793 中定義了一個經(jīng)典算法,算法如下:

  • [1] 首先采樣計算RTT值
  • [2] 然后計算平滑的RTT,稱為Smoothed Round Trip Time (SRTT),SRTT = ( ALPHA * SRTT ) + ((1-ALPHA) * RTT)
  • [3] RTO = min[UBOUND,max[LBOUND,(BETA*SRTT)]]

其中:UBOUND 是 RTO 值的上限;例如:可以定義為 1 分鐘,LBOUND 是 RTO 值的下限,例如,可以定義為 1 秒;ALPHA is a smoothing factor (e.g., .8 to .9), and BETA is a delay variance factor(e.g., 1.3 to 2.0).

然而這個算法有個缺點就是:在算 RTT 樣本的時候,是用第一次發(fā)數(shù)據(jù)的時間和 ack 回來的時間做 RTT 樣本值,還是用重傳的時間和 ACK 回來的時間做 RTT 樣本值?不管是怎么選擇,總會造成會要么把 RTT 算過長了,要么把 RTT 算過短了。如下圖:(a)就計算過長了,而(b)就是計算過短了。

針對上面經(jīng)典算法的缺陷,于是提出 Karn / Partridge Algorithm 對經(jīng)典算法進行了改進(算法大特點是——忽略重傳,不把重傳的 RTT 做采樣),但是這個算法有問題:如果在某一時間,網(wǎng)絡閃動,突然變慢了,產(chǎn)生了比較大的延時,這個延時導致要重轉所有的包(因為之前的 RTO 很小),于是,因為重轉的不算,所以,RTO 就不會被更新,這是一個災難。于是,為解決上面兩個算法的問題,又有人推出來了一個新的算法,這個算法叫 Jacobson / Karels Algorithm(參看 FC6289),這個算法的核心是:除了考慮每兩次測量值的偏差之外,其變化率也應該考慮在內,如果變化率過大,則通過以變化率為自變量的函數(shù)為主計算 RTT(如果陡然增大,則取值為比較大的正數(shù),如果陡然減小,則取值為比較小的負數(shù),然后和平均值加權求和),反之如果變化率很小,則取測量平均值。

公式如下:(其中的 DevRTT 是 Deviation RTT 的意思)

  1. SRTT = SRTT + α (RTT – SRTT)  —— 計算平滑RTT 
  2. DevRTT = (1-β)*DevRTT + β*(|RTT-SRTT|) ——計算平滑RTT和真實的差距(加權移動平均) 
  3. RTO= µ * SRTT + ∂ *DevRTT —— 神一樣的公式 
  4. (其中:在Linux下,α = 0.125,β = 0.25, μ = 1,∂ = 4 ——這就是算法中的“調得一手好參數(shù)”,nobody knows why, it just works…) 最后的這個算法在被用在今天的TCP協(xié)議中并工作非常好 

最后的這個算法在被用在今天的 TCP 協(xié)議中并工作非常好。

知道超時怎么計算后,很自然就想到定時器的設計問題。一個簡單直觀的方案就是為 TCP 中的每一個數(shù)據(jù)包維護一個定時器,在這個定時器到期前沒收到確認,則進行重傳。這種設計理論上是很合理的,但是實現(xiàn)上,這種方案將會有非常多的定時器,會帶來巨大內存開銷和調度開銷。既然不能每個包一個定時器,那么多少個包一個定時器才好呢,這個似乎比較難確定。可以換個思路,不要以包量來確定定時器,以連接來確定定時器會不會比較合理呢?目前,采取每一個 TCP 連接單一超時定時器的設計則成了一個默認的選擇,并且 RFC2988 給出了每連接單一定時器的設計建議算法規(guī)則:

[1].每一次一個包含數(shù)據(jù)的包被發(fā)送(包括重發(fā)),如果還沒開啟重傳定時器,則開啟它,使得它在 RTO 秒之后超時(按照當前的 RTO 值)。[2]. 當接收到一個 ACK 確認一個新的數(shù)據(jù);如果所有的發(fā)出數(shù)據(jù)都被確認了,關閉重傳定時器;[3].當接收到一個 ACK 確認一個新的數(shù)據(jù),還有數(shù)據(jù)在傳輸,也就是還有沒被確認的數(shù)據(jù),重新啟動重傳定時器,使得它在 RTO 秒之后超時(按照當前的 RTO 值)。

當重傳定時器超時后,依次做下列 3 件事情:[4.1]. 重傳最早的尚未被 TCP 接收方 ACK 的數(shù)據(jù)包;[4.2]. 重新設置 RTO 為 RTO *2(“還原定時器”),但是新 RTO 不應該超過 RTO 的上限(RTO 有個上限值,這個上限值最少為 60s);[4.3]. 重啟重傳定時器。

上面的建議算法體現(xiàn)了一個原則:沒被確認的包必須可以超時,并且超時的時間不能太長,同時也不要過早重傳。規(guī)則[1][3][4.3]共同說明了只要還有數(shù)據(jù)包沒被確認,那么定時器一定會是開啟著的(這樣滿足沒被確認的包必須可以超時的原則)。規(guī)則[4.2]說明定時器的超時值是有上限的(滿足超時的時間不能太長)。

規(guī)則[3]說明,在一個 ACK 到來后重置定時器可以保護后發(fā)的數(shù)據(jù)不被過早重傳;因為一個 ACK 到來了,說明后續(xù)的 ACK 很可能會依次到來,也就是說丟失的可能性并不大。規(guī)則[4.2]也是在一定程度上避免過早重傳,因為,在出現(xiàn)定時器超時后,有可能是網(wǎng)絡出現(xiàn)擁塞了,這個時候應該延長定時器,避免出現(xiàn)大量的重傳進一步加劇網(wǎng)絡的擁塞。

【2】TCP 的重傳機制

通過上面我們可以知道,TCP 的重傳是由超時觸發(fā)的,這會引發(fā)一個重傳選擇問題,假設 TCP 發(fā)送端連續(xù)發(fā)了 1、2、3、4、5、6、7、8、9、10 共 10 包,其中 4、6、8 這 3 個包全丟失了,由于 TCP 的 ACK 是確認最后連續(xù)收到序號,這樣發(fā)送端只能收到 3 號包的 ACK,這樣在 TIME_OUT 的時候,發(fā)送端就面臨下面兩個重傳選擇:[1].僅重傳 4 號包 [2].重傳 3 號后面所有的包,也就是重傳 4~10 號包

對于,上面兩個選擇的優(yōu)缺點都比較明顯。方案[1],優(yōu)點:按需重傳,能夠最大程度節(jié)省帶寬。缺點:重傳會比較慢,因為重傳 4 號包后,需要等下一個超時才會重傳 6 號包。方案[2],優(yōu)點:重傳較快,數(shù)據(jù)能夠較快交付給接收端。缺點:重傳了很多不必要重傳的包,浪費帶寬,在出現(xiàn)丟包的時候,一般是網(wǎng)絡擁塞,大量的重傳又可能進一步加劇擁塞。

上面的問題是由于單純以時間驅動來進行重傳的,都必須等待一個超時時間,不能快速對當前網(wǎng)絡狀況做出響應,如果加入以數(shù)據(jù)驅動呢?TCP 引入了一種叫 Fast Retransmit(快速重傳)的算法,就是在連續(xù)收到 3 次相同確認號的 ACK,那么就進行重傳。這個算法基于這么一個假設,連續(xù)收到 3 個相同的 ACK,那么說明當前的網(wǎng)絡狀況變好了,可以重傳丟失的包了。

快速重傳解決了 timeout 的問題,但是沒解決重傳一個還是重傳多個的問題。出現(xiàn)難以決定是否重傳多個包問題的根源在于,發(fā)送端不知道那些非連續(xù)序號的包已經(jīng)到達接收端了,但是接收端是知道的,如果接收端告訴一下發(fā)送端不就可以解決這個問題嗎?于是,RFC2018 提出了 Selective Acknowledgment(SACK,選擇確認)機制,SACK 是 TCP 的擴展選項,包括(1)SACK 允許選項(Kind=4,Length=2,選項只允許在有 SYN 標志的 TCP 包中),(2)SACK 信息選項 Kind=5,Length)。一個 SACK 的例子如下圖,紅框說明:接收端收到了 0-5500,8000-8500,7000-7500,6000-6500 的數(shù)據(jù)了,這樣發(fā)送端就可以選擇重傳丟失的 5500-6000,6500-7000,7500-8000 的包。

SACK 依靠接收端的接收情況反饋,解決了重傳風暴問題,這樣夠了嗎?接收端能不能反饋更多的信息呢?顯然是可以的,于是,RFC2883 對對 SACK 進行了擴展,提出了 D-SACK,也就是利用第一塊 SACK 數(shù)據(jù)中描述重復接收的不連續(xù)數(shù)據(jù)塊的序列號參數(shù),其他 SACK 數(shù)據(jù)則描述其他正常接收到的不連續(xù)數(shù)據(jù)。這樣發(fā)送方利用第一塊 SACK,可以發(fā)現(xiàn)數(shù)據(jù)段被網(wǎng)絡復制、錯誤重傳、ACK 丟失引起的重傳、重傳超時等異常的網(wǎng)絡狀況,使得發(fā)送端能更好調整自己的重傳策略。D-SACK,有幾個優(yōu)點:

1)發(fā)送端可以判斷出,是發(fā)包丟失了,還是接收端的 ACK 丟失了。(發(fā)送方,重傳了一個包,發(fā)現(xiàn)并沒有 D-SACK 那個包,那么就是發(fā)送的數(shù)據(jù)包丟了;否則就是接收端的 ACK 丟了,或者是發(fā)送的包延遲到達了);2)發(fā)送端可以判斷自己的 RTO 是不是有點小了,導致過早重傳(如果收到比較多的 D-SACK 就該懷疑是 RTO 小了);3)發(fā)送端可以判斷自己的數(shù)據(jù)包是不是被復制了。(如果明明沒有重傳該數(shù)據(jù)包,但是收到該數(shù)據(jù)包的 D-SACK);4)發(fā)送端可以判斷目前網(wǎng)絡上是不是出現(xiàn)了有些包被 delay 了,也就是出現(xiàn)先發(fā)的包卻后到了。

疑癥(9)TCP 的流量控制

我們知道 TCP 的窗口(window)是一個 16bit 位字段,它代表的是窗口的字節(jié)容量,也就是 TCP 的標準窗口最大為 2^16-1=65535 個字節(jié)。另外在 TCP 的選項字段中還包含了一個 TCP 窗口擴大因子,option-kind 為 3,option-length 為 3 個字節(jié),option-data 取值范圍 0-14。窗口擴大因子用來擴大 TCP 窗口,可把原來 16bit 的窗口,擴大為 31bit。

這個窗口是接收端告訴發(fā)送端自己還有多少緩沖區(qū)可以接收數(shù)據(jù)。于是發(fā)送端就可以根據(jù)這個接收端的處理能力來發(fā)送數(shù)據(jù),而不會導致接收端處理不過來。也就是,發(fā)送端是根據(jù)接收端通知的窗口大小來調整自己的發(fā)送速率的,以達到端到端的流量控制。盡管流量控制看起來簡單明了,就是發(fā)送端根據(jù)接收端的限制來控制自己的發(fā)送就好了,但是細心的同學還是會有些疑問的。

1)發(fā)送端是怎么做到比較方便知道自己哪些包可以發(fā),哪些包不能發(fā)呢?2)如果接收端通知一個零窗口給發(fā)送端,這個時候發(fā)送端還能不能發(fā)送數(shù)據(jù)呢?如果不發(fā)數(shù)據(jù),那一直等接收端口通知一個非 0 窗口嗎,如果接收端一直不通知呢?3)如果接收端處理能力很慢,這樣接收端的窗口很快被填滿,然后接收處理完幾個字節(jié),騰出幾個字節(jié)的窗口后,通知發(fā)送端,這個時候發(fā)送端馬上就發(fā)送幾個字節(jié)給接收端嗎?發(fā)送的話會不會太浪費了,就像一艘萬噸油輪只裝上幾斤的油就開去目的地一樣。對于發(fā)送端產(chǎn)生數(shù)據(jù)的能力很弱也一樣,如果發(fā)送端慢吞吞產(chǎn)生幾個字節(jié)的數(shù)據(jù)要發(fā)送,這個時候該不該立即發(fā)送呢?還是累積多點在發(fā)送?

疑問 1)的解決:

發(fā)送方要知道那些可以發(fā),哪些不可以發(fā),一個簡明的方案就是按照接收方的窗口通告,發(fā)送方維護一個一樣大小的發(fā)送窗口就可以了,在窗口內的可以發(fā),窗口外的不可以發(fā),窗口在發(fā)送序列上不斷后移,這就是 TCP 中的滑動窗口。如下圖所示,對于 TCP 發(fā)送端其發(fā)送緩存內的數(shù)據(jù)都可以分為 4 類:[1]-已經(jīng)發(fā)送并得到接收端 ACK 的;[2]-已經(jīng)發(fā)送但還未收到接收端 ACK 的;[3]-未發(fā)送但允許發(fā)送的(接收方還有空間);[4]-未發(fā)送且不允許發(fā)送(接收方?jīng)]空間了)。

其中,[2]和[3]兩部分合起來稱之為發(fā)送窗口。

下面兩圖演示的窗口的滑動情況,收到 36 的 ACK 后,窗口向后滑動 5 個 byte。

疑問 2)的解決

由問題 1)我們知道,發(fā)送端的發(fā)送窗口是由接收端控制的。下圖,展示了一個發(fā)送端是怎么受接收端控制的。

由上圖我們知道,當接收端通知一個 zero 窗口的時候,發(fā)送端的發(fā)送窗口也變成了 0,也就是發(fā)送端不能發(fā)數(shù)據(jù)了。如果發(fā)送端一直等待,直到接收端通知一個非零窗口在發(fā)數(shù)據(jù)的話,這似乎太受限于接收端,如果接收端一直不通知新的窗口呢?顯然發(fā)送端不能干等,起碼有一個主動探測的機制。為解決 0 窗口的問題,TCP 使用了 Zero Window Probe 技術,縮寫為 ZWP。發(fā)送端在窗口變成 0 后,會發(fā) ZWP 的包給接收方,來探測目前接收端的窗口大小,一般這個值會設置成 3 次,每次大約 30-60 秒(不同的實現(xiàn)可能會不一樣)。

如果 3 次過后還是 0 的話,有的 TCP 實現(xiàn)就會發(fā) RST 掉這個連接。正如有人的地方就會有商機,那么有等待的地方就很有可能出現(xiàn) DDoS 攻擊點。攻擊者可以在和 Server 建立好連接后,就向 Server 通告一個 0 窗口,然后 Server 端就只能等待進行 ZWP,于是攻擊者會并發(fā)大量的這樣的請求,把 Server 端的資源耗盡。

疑問點 3)的解決

疑點 3)本質就是一個避免發(fā)送大量小包的問題。造成這個問題原因有二:

1)接收端一直在通知一個小的窗口;2)發(fā)送端本身問題,一直在發(fā)送小包。這個問題,TCP 中有個術語叫 Silly Window Syndrome(糊涂窗口綜合癥)。

解決這個問題的思路有兩種,1)接收端不通知小窗口,2)發(fā)送端積累一下數(shù)據(jù)在發(fā)送。

思路 1)是在接收端解決這個問題,David D Clark’s 方案,如果收到的數(shù)據(jù)導致 window size 小于某個值,就 ACK 一個 0 窗口,這就阻止發(fā)送端在發(fā)數(shù)據(jù)過來。等到接收端處理了一些數(shù)據(jù)后 windows size 大于等于了 MSS,或者 buffer 有一半為空,就可以通告一個非 0 窗口。

思路 2)是在發(fā)送端解決這個問題,有個著名的 Nagle’s algorithm。Nagle 算法的規(guī)則:[1]如果包長度達到 MSS ,則允許發(fā)送;[2]如果該包含有 FIN ,則允許發(fā)送;[3]設置了 TCP_NODELAY 選項,則允許發(fā)送;[4]設置 TCP_CORK 選項時,若所有發(fā)出去的小數(shù)據(jù)包(包長度小于 MSS)均被確認,則允許發(fā)送;[5]上述條件都未滿足,但發(fā)生了超時(一般為 200ms ),則立即發(fā)送。

規(guī)則[4]指出 TCP 連接上最多只能有一個未被確認的小數(shù)據(jù)包。從規(guī)則[4]可以看出 Nagle 算法并不禁止發(fā)送小的數(shù)據(jù)包(超時時間內),而是避免發(fā)送大量小的數(shù)據(jù)包。由于 Nagle 算法是依賴 ACK 的,如果 ACK 很快的話,也會出現(xiàn)一直發(fā)小包的情況,造成網(wǎng)絡利用率低。TCP_CORK 選項則是禁止發(fā)送小的數(shù)據(jù)包(超時時間內),設置該選項后,TCP 會盡力把小數(shù)據(jù)包拼接成一個大的數(shù)據(jù)包(一個 MTU)再發(fā)送出去,當然也不會一直等,發(fā)生了超時(一般為 200ms),也立即發(fā)送。Nagle 算法和 CP_CORK 選項提高了網(wǎng)絡的利用率,但是增加是延時。從規(guī)則[3]可以看出,設置 TCP_NODELAY 選項,就是完全禁用 Nagle 算法了。

這里要說一個小插曲,Nagle 算法和延遲確認(Delayed Acknoledgement)一起,當出現(xiàn)(write-write-read)的時候會引發(fā)一個 40ms 的延時問題,這個問題在 HTTP svr 中體現(xiàn)的比較明顯。場景如下:

客戶端在請求下載 HTTP svr 中的一個小文件,一般情況下,HTTP svr 都是先發(fā)送 HTTP 響應頭部,然后在發(fā)送 HTTP 響應 BODY(特別是比較多的實現(xiàn)在發(fā)送文件的實施采用的是 sendfile 系統(tǒng)調用,這就出現(xiàn) write-write-read 模式了)。當發(fā)送頭部的時候,由于頭部較小,于是形成一個小的 TCP 包發(fā)送到客戶端,這個時候開始發(fā)送 body,由于 body 也較小,這樣還是形成一個小的 TCP 數(shù)據(jù)包,根據(jù) Nagle 算法,HTTP svr 已經(jīng)發(fā)送一個小的數(shù)據(jù)包了,在收到第一個小包的 ACK 后或等待 200ms 超時后才能在發(fā)小包,HTTP svr 不能發(fā)送這個 body 小 TCP 包。

客戶端收到 http 響應頭后,由于這是一個小的 TCP 包,于是客戶端開啟延遲確認,客戶端在等待 Svr 的第二個包來在一起確認或等待一個超時(一般是 40ms)在發(fā)送 ACK 包;這樣就出現(xiàn)了你等我、然而我也在等你的死鎖狀態(tài),于是出現(xiàn)最多的情況是客戶端等待一個 40ms 的超時,然后發(fā)送 ACK 給 HTTP svr,HTTP svr 收到 ACK 包后在發(fā)送 body 部分。大家在測 HTTP svr 的時候就要留意這個問題了。

疑癥(10)TCP 的擁塞控制

談到擁塞控制,就要先談談?chuàng)砣囊蛩睾捅举|。本質上,網(wǎng)絡上擁塞的原因就是大家都想獨享整個網(wǎng)絡資源,對于 TCP,端到端的流量控制必然會導致網(wǎng)絡擁堵。這是因為 TCP 只看到對端的接收空間的大小,而無法知道鏈路上的容量,只要雙方的處理能力很強,那么就可以以很大的速率發(fā)包,于是鏈路很快出現(xiàn)擁堵,進而引起大量的丟包,丟包又引發(fā)發(fā)送端的重傳風暴,進一步加劇鏈路的擁塞。

另外一個擁塞的因素是鏈路上的轉發(fā)節(jié)點,例如路由器,再好的路由器只要接入網(wǎng)絡,總是會拉低網(wǎng)絡的總帶寬,如果在路由器節(jié)點上出現(xiàn)處理瓶頸,那么就很容易出現(xiàn)擁塞。由于 TCP 看不到網(wǎng)絡的狀況,那么擁塞控制是必須的并且需要采用試探性的方式來控制擁塞,于是擁塞控制要完成兩個任務:[1]公平性;[2]擁塞過后的恢復。

TCP 發(fā)展到現(xiàn)在,擁塞控制方面的算法很多,其中 Reno 是目前應用最廣泛且較為成熟的算法,下面著重介紹一下 Reno 算法(RFC5681)。介紹該算法前,首先介紹一個概念 duplicate acknowledgment(冗余 ACK、重復 ACK)一般情況下一個 ACK 被稱為冗余 ACK,要同時滿足下面幾個條件(對于 SACK,那么根據(jù) SACK 的一些信息來進一步判斷)。

[1] 接收 ACK 的那端已經(jīng)發(fā)出了一些還沒被 ACK 的數(shù)據(jù)包;[2] 該 ACK 沒有捎帶 data;[3] 該 ACK 的 SYN 和 FIN 位都是 off 的,也就是既不是 SYN 包的 ACK 也不是 FIN 包的 ACK;[4] 該 ACK 的確認號等于接收 ACK 那端已經(jīng)收到的 ACK 的最大確認號;[5] 該 ACK 通知的窗口等接收該 ACK 的那端上一個收到的 ACK 的窗口。

Reno 算法包含 4 個部分:[1]慢熱啟動算法 – Slow Start;[2]擁塞避免算法 – Congestion Avoidance;[3]快速重傳 - Fast Retransimit;[4]快速恢復算法 – Fast Recovery。

TCP 的擁塞控制主要原理依賴于一個擁塞窗口(cwnd)來控制,根據(jù)前面的討論,我們知道有一個接收端通告的接收窗口(rwnd)用于流量控制;加上擁塞控制后,發(fā)送端真正的發(fā)送窗口=min(rwnd,cwnd)。關于 cwnd 的單位,在 TCP 中是以字節(jié)來做單位的,我們假設 TCP 每次傳輸都是按照 MSS 大小來發(fā)送數(shù)據(jù),因此你可以認為 cwnd 按照數(shù)據(jù)包個數(shù)來做單位也可以理解,下面如果沒有特別說明是字節(jié),那么 cwnd 增加 1 也就是相當于字節(jié)數(shù)增加 1 個 MSS 大小。

【1】慢熱啟動算法 – Slow Start

慢啟動體現(xiàn)了一個試探的過程,剛接入網(wǎng)絡的時候先發(fā)包慢點,探測一下網(wǎng)絡情況,然后在慢慢提速。不要一上來就拼命發(fā)包,這樣很容易造成鏈路的擁堵,出現(xiàn)擁堵了在想到要降速來緩解擁堵這就有點成本高了,畢竟無數(shù)的先例告誡我們先污染后治理的成本是很高的。慢啟動的算法如下(cwnd 全稱 Congestion Window):1)連接建好的開始先初始化 cwnd = N,表明可以傳 N 個 MSS 大小的數(shù)據(jù);2)每當收到一個 ACK,++cwnd; 呈線性上升;3)每當過了一個 RTT,cwnd = cwnd*2; 呈指數(shù)讓升;4)還有一個慢啟動門限 ssthresh(slow start threshold),是一個上限,當 cwnd >= ssthresh 時,就會進入"擁塞避免算法 - Congestion Avoidance"。

根據(jù) RFC5681,如果 MSS > 2190 bytes,則 N = 2;如果 MSS < 1095 bytes,則 N =4;如果 2190 bytes >= MSS >= 1095 bytes,則 N = 3;一篇 Google 的論文《An Argument for Increasing TCP’s Initial Congestion Window》建議把 cwnd 初始化成了 10 個 MSS。Linux 3.0 后采用了這篇論文的建議。

【2】擁塞避免算法 – Congestion Avoidance

慢啟動的時候說過,cwnd 是指數(shù)快速增長的,但是增長是有個門限 ssthresh(一般來說大多數(shù)的實現(xiàn) ssthresh 的值是 65535 字節(jié))的,到達門限后進入擁塞避免階段。在進入擁塞避免階段后,cwnd 值變化算法如下:1)每收到一個 ACK,調整 cwnd 為 (cwnd + 1/cwnd) * MSS 個字節(jié);2)每經(jīng)過一個 RTT 的時長,cwnd 增加 1 個 MSS 大小。

TCP 是看不到網(wǎng)絡的整體狀況的,那么 TCP 認為網(wǎng)絡擁塞的主要依據(jù)是它重傳了報文段。前面我們說過 TCP 的重傳分兩種情況:1)出現(xiàn) RTO 超時,重傳數(shù)據(jù)包。這種情況下,TCP 就認為出現(xiàn)擁塞的可能性就很大,于是它反應非常'強烈' [1] 調整門限 ssthresh 的值為當前 cwnd 值的 1/2;[2] reset 自己的 cwnd 值為 1;[3] 然后重新進入慢啟動過程。

2)在 RTO 超時前,收到 3 個 duplicate ACK 進行重傳數(shù)據(jù)包。這種情況下,收到 3 個冗余 ACK 后說明確實有中間的分段丟失,然而后面的分段確實到達了接收端,因為這樣才會發(fā)送冗余 ACK,這一般是路由器故障或者輕度擁塞或者其它不太嚴重的原因引起的,因此此時擁塞窗口縮小的幅度就不能太大,此時進入快速重傳。

【3】快速重傳 - Fast Retransimit 做的事情有:

1) 調整門限 ssthresh 的值為當前 cwnd 值的 1/2;2) 將 cwnd 值設置為新的 ssthresh 的值;3) 重新進入擁塞避免階段。

在快速重傳的時候,一般網(wǎng)絡只是輕微擁堵,在進入擁塞避免后,cwnd 恢復的比較慢。針對這個,“快速恢復”算法被添加進來,當收到 3 個冗余 ACK 時,TCP 最后的[3]步驟進入的不是擁塞避免階段,而是快速恢復階段。

【4】快速恢復算法 – Fast Recovery :

快速恢復的思想是“數(shù)據(jù)包守恒”原則,即帶寬不變的情況下,在網(wǎng)絡同一時刻能容納數(shù)據(jù)包數(shù)量是恒定的。當“老”數(shù)據(jù)包離開了網(wǎng)絡后,就能向網(wǎng)絡中發(fā)送一個“新”的數(shù)據(jù)包。既然已經(jīng)收到了 3 個冗余 ACK,說明有三個數(shù)據(jù)分段已經(jīng)到達了接收端,既然三個分段已經(jīng)離開了網(wǎng)絡,那么就是說可以在發(fā)送 3 個分段了。

于是只要發(fā)送方收到一個冗余的 ACK,于是 cwnd 加 1 個 MSS。快速恢復步驟如下(在進入快速恢復前,cwnd 和 sshthresh 已被更新為:sshthresh = cwnd /2,cwnd = sshthresh):1)把 cwnd 設置為 ssthresh 的值加 3,重傳 Duplicated ACKs 指定的數(shù)據(jù)包;2)如果再收到 duplicated Acks,那么 cwnd = cwnd +1;3)如果收到新的 ACK,而非 duplicated Ack,那么將 cwnd 重新設置為【3】中 1)的 sshthresh 的值。然后進入擁塞避免狀態(tài)。

細心的同學可能會發(fā)現(xiàn)快速恢復有個比較明顯的缺陷就是:它依賴于 3 個冗余 ACK,并假定很多情況下,3 個冗余的 ACK 只代表丟失一個包。但是 3 個冗余 ACK 也很有可能是丟失了很多個包,快速恢復只是重傳了一個包,然后其他丟失的包就只能等待到 RTO 超時了。超時會導致 ssthresh 減半,并且退出了 Fast Recovery 階段,多個超時會導致 TCP 傳輸速率呈級數(shù)下降。出現(xiàn)這個問題的主要原因是過早退出了 Fast Recovery 階段。

為解決這個問題,提出了 New Reno 算法,該算法是在沒有 SACK 的支持下改進 Fast Recovery 算法(SACK 改變 TCP 的確認機制,把亂序等信息會全部告訴對方,SACK 本身攜帶的信息就可以使得發(fā)送方有足夠的信息來知道需要重傳哪些包,而不需要重傳哪些包),具體改進如下:

1)發(fā)送端收到 3 個冗余 ACK 后,重傳冗余 ACK 指示可能丟失的那個包 segment1,如果 segment1 的 ACK 通告接收端已經(jīng)收到發(fā)送端的全部已經(jīng)發(fā)出的數(shù)據(jù)的話,那么就是只丟失一個包,如果沒有,那么就是有多個包丟失了;2)發(fā)送端根據(jù) segment1 的 ACK 判斷出有多個包丟失,那么發(fā)送端繼續(xù)重傳窗口內未被 ACK 的第一個包,直到 sliding window 內發(fā)出去的包全被 ACK 了,才真正退出 Fast Recovery 階段。

我們可以看到,擁塞控制在擁塞避免階段,cwnd 是加性增加的,在判斷出現(xiàn)擁塞的時候采取的是指數(shù)遞減。為什么要這樣做呢?這是出于公平性的原則,擁塞窗口的增加受惠的只是自己,而擁塞窗口減少受益的是大家。這種指數(shù)遞減的方式實現(xiàn)了公平性,一旦出現(xiàn)丟包,那么立即減半退避,可以給其他新建的連接騰出足夠的帶寬空間,從而保證整個的公平性。

至此,TCP 的疑難雜癥基本介紹完畢了,總的來說 TCP 是一個有連接的、可靠的、帶流量控制和擁塞控制的端到端的協(xié)議。TCP 的發(fā)送端能發(fā)多少數(shù)據(jù),由發(fā)送端的發(fā)送窗口決定(當然發(fā)送窗口又被接收端的接收窗口、發(fā)送端的擁塞窗口限制)的,那么一個 TCP 連接的傳輸穩(wěn)定狀態(tài)應該體現(xiàn)在發(fā)送端的發(fā)送窗口的穩(wěn)定狀態(tài)上,這樣的話,TCP 的發(fā)送窗口有哪些穩(wěn)定狀態(tài)呢?TCP 的發(fā)送窗口穩(wěn)定狀態(tài)主要有上面三種穩(wěn)定狀態(tài):

【1】接收端擁有大窗口的經(jīng)典鋸齒狀

大多數(shù)情況下都是處于這樣的穩(wěn)定狀態(tài),這是因為,一般情況下機器的處理速度就是比較快,這樣 TCP 的接收端都是擁有較大的窗口,這時發(fā)送端的發(fā)送窗口就完全由其擁塞窗口 cwnd 決定了;網(wǎng)絡上擁有成千上萬的 TCP 連接,它們在相互爭用網(wǎng)絡帶寬,TCP 的流量控制使得它想要獨享整個網(wǎng)絡,而擁塞控制又限制其必要時做出犧牲來體現(xiàn)公平性。于是在傳輸穩(wěn)定的時候 TCP 發(fā)送端呈現(xiàn)出下面過程的反復:

[1]用慢啟動或者擁塞避免方式不斷增加其擁塞窗口,直到丟包的發(fā)生;[2]然后將發(fā)送窗口將下降到 1 或者下降一半,進入慢啟動或者擁塞避免階段(要看是由于超時丟包還是由于冗余 ACK 丟包);過程如下圖:

【2】接收端擁有小窗口的直線狀態(tài)

這種情況下是接收端非常慢速,接收窗口一直很小,這樣發(fā)送窗口就完全有接收窗口決定了。由于發(fā)送窗口小,發(fā)送數(shù)據(jù)少,網(wǎng)絡就不會出現(xiàn)擁塞了,于是發(fā)送窗口就一直穩(wěn)定的等于那個較小的接收窗口,呈直線狀態(tài)。

【3】兩個直連網(wǎng)絡端點間的滿載狀態(tài)下的直線狀態(tài)

這種情況下,Peer 兩端直連,并且只有位于一個 TCP 連接,那么這個連接將獨享網(wǎng)絡帶寬,這里不存在擁塞問題,在他們處理能力足夠的情況下,TCP 的流量控制使得他們能夠跑慢整個網(wǎng)絡帶寬。

通過上面我們知道,在 TCP 傳輸穩(wěn)定的時候,各個 TCP 連接會均分網(wǎng)絡帶寬的。相信大家學生時代經(jīng)常會發(fā)生這樣的場景,自己在看視頻的時候突然出現(xiàn)視頻卡頓,于是就大叫起來,哪個開了迅雷,趕緊給我停了。其實簡單的下載加速就是開啟多個 TCP 連接來分段下載就達到加速的效果,假設宿舍的帶寬是 1000K/s,一開始兩個在看視頻,每人平均網(wǎng)速是 500k/s,這速度看起視頻來那叫一個順溜。突然其中一個同學打打開迅雷開著 99 個 TCP 連接在下載愛情動作片,這個時候平均下來你能分到的帶寬就剩下 10k/s,這網(wǎng)速下你的視頻還不卡成幻燈片。

在通信鏈路帶寬固定(假設為 W),多人公用一個網(wǎng)絡帶寬的情況下,利用 TCP 協(xié)議的擁塞控制的公平性,多開幾個 TCP 連接就能多分到一些帶寬(當然要忽略有些用 UDP 協(xié)議帶來的影響),然而不管怎么最多也就能把整個帶寬搶到,于是在占滿整個帶寬的情況下,下載一個大小為 FS 的文件,那么最快需要的時間是 FS/W,難道就沒辦法加速了嗎?

答案是有的,這樣因為網(wǎng)絡是網(wǎng)狀的,一個節(jié)點是要和很多幾點互聯(lián)的,這就存在多個帶寬為 W 的通信鏈路,如果我們能夠將要下載的文件,一半從 A 通信鏈路下載,另外一半從 B 通信鏈路下載,這樣整個下載時間就減半了為 FS/(2W),這就是 p2p 加速。相信大家學生時代在下載愛情動作片的時候也遇到過這種情況,明明外網(wǎng)速度沒這么快的,自己下載的愛情動作片的速度卻達到幾 M/s,那是因為,你的左后或右后的宿友在幫你加速中。我們都知道 P2P 模式下載會快,并且越多人下載就越快,那么問題來了,P2P 下載加速理論上的加速比是多少呢?

11.附加題 1:P2P 理論上的加速比

傳統(tǒng)的 C/S 模式傳輸文件,在跑滿 Client 帶寬的情況下傳輸一個文件需要耗時 FS/BW,如果有 n 個客戶端需要下載文件,那么總耗時是 n*(FS/BW),當然啦,這并不一定是串行傳輸,可以并行來傳輸?shù)?,這樣總耗時也就是 FS/BW 了,但是這需要服務器的帶寬是 n 個 client 帶寬的總和 n*BW。C/S 模式一個明顯的缺點是服務要傳輸一個文件 n 次,這樣對服務器的性能和帶寬帶來比較大的壓力,我可以換下思路,服務器將文件傳給其中一個 Client 后,讓這些互聯(lián)的 Client 自己來交互那個文件,那服務器的壓力就減少很多了。這就是 P2P 網(wǎng)絡的好處,P2P 利用各個節(jié)點間的互聯(lián),提倡“人人為我,我為人人”。

知道 P2P 傳輸?shù)暮锰幒?,我們來談下理論上的最大加速比,為了簡化討論,一個簡單的網(wǎng)絡拓撲圖如下,有 4 個相互互聯(lián)的節(jié)點,并且每個節(jié)點間的網(wǎng)絡帶寬是 BW,傳輸一個大小為 FS 的文件最快的時間是多少呢?假設節(jié)點 N1 有個大小為 FS 的文件需要傳輸給 N2,N3,N4 節(jié)點,一種簡單的方式就是:節(jié)點 N1 同時將文件傳輸給節(jié)點 N2,N3,N4 耗時 FS/BW,這樣大家都擁有文件 FS 了。大家可以看出,整個過程只有節(jié)點 1 在發(fā)送文件,其他節(jié)點都是在接收,完全違反了 P2P 的“人人為我,我為人人”的宗旨。那怎么才能讓大家都做出貢獻了呢?解決方案是切割文件。

[1]首先,節(jié)點 N1 文件分成 3 個片段 FS2,FS3,FS4,接著將 FS2 發(fā)送給 N2,F(xiàn)S3 發(fā)送給 N3,F(xiàn)S4 發(fā)送給 N4,耗時 FS/(3*BW);[2]然后,N2,N3,N4 執(zhí)行“人人為我,我為人人”的精神,將自己擁有的 F2,F3,F4 分別發(fā)給沒有的其他的節(jié)點,這樣耗時 FS/(3*BW)完成交換。

于是總耗時為 2FS/(3BW)完成了文件 FS 的傳輸,可以看出耗時減少為原來的 2/3 了,如果有 n 個節(jié)點,那么時間就是原來的 2/(n-1),也就是加速比是 2/(n-1),這就是加速的理論上限了嗎?還沒發(fā)揮最多能量的,相信大家已經(jīng)看到分割文件的好處了,上面的文件分割粒度還是有點大,以至于,在第二階段[2]傳輸過程中,節(jié)點 N1 無所事事。為了最大化發(fā)揮大家的作用,我們需要將 FS2,FS3,FS4 在進行分割,假設將它們都均分為 K 等份,這樣就有 FS21,FS22…FS2K、FS31,FS32…FS3K、FS41,FS42…FS4K,一共 3K 個分段。于是下面就開始進行加速分發(fā):

[1]節(jié)點 N1 將分段 FS21,F(xiàn)S31,F(xiàn)S41 分別發(fā)送給 N2,N3,N4 節(jié)點。耗時,F(xiàn)S/(3K*BW) [2]節(jié)點 N1 將分段 FS22,F(xiàn)S32,F(xiàn)S42 分別發(fā)送給 N2,N3,N4 節(jié)點,同時節(jié)點 N2,N3,N4 將階段[1]收到的分段相互發(fā)給沒有的節(jié)點。耗時,F(xiàn)S/(3K*BW)。

[K]節(jié)點 N1 將分段 FS2K,F(xiàn)S3K,F(xiàn)S4K 分別發(fā)送給 N2,N3,N4 節(jié)點,同時節(jié)點 N2,N3,N4 將階段[K-1]收到的分段相互發(fā)給沒有的節(jié)點。耗時,F(xiàn)S/(3K*BW)。[K+1]節(jié)點 N2,N3,N4 將階段[K]收到的分段相互發(fā)給沒有的節(jié)點。耗時,F(xiàn)S/(3K*BW)。于是總的耗時為(K+1) (FS/(3KBW)) = FS/(3BW) +FS/(3KBW),當 K 趨于無窮大的時候,文件進行無限細分的時候,耗時變成了 FS/(3*BW),也就是當節(jié)點是 n+1 的時候,加速比是 n。這就是理論上的最大加速比了,最大加速比是 P2P 網(wǎng)絡節(jié)點個數(shù)減 1。

12.附加題 2:系統(tǒng)調用 listen() 的 backlog 參數(shù)指的是什么

要說明 backlog 參數(shù)的含義,首先需要說一下 Linux 的協(xié)議棧維護的 TCP 連接的兩個連接隊列:[1]SYN 半連接隊列;[2]accept 連接隊列。

[1]SYN 半連接隊列:Server 端收到 Client 的 SYN 包并回復 SYN,ACK 包后,該連接的信息就會被移到一個隊列,這個隊列就是 SYN 半連接隊列(此時 TCP 連接處于 非同步狀態(tài) )

[2]accept 連接隊列:Server 端收到 SYN,ACK 包的 ACK 包后,就會將連接信息從[1]中的隊列移到另外一個隊列,這個隊列就是 accept 連接隊列(這個時候 TCP 連接已經(jīng)建立,三次握手完成了)。

用戶進程調用 accept()系統(tǒng)調用后,該連接信息就會從[2]中的隊列中移走。相信不少同學就 backlog 的具體含義進行爭論過,有些認為 backlog 指的是[1]和[2]兩個隊列的和。而有些則認為是 backlog 指的是[2]的大小。其實,這兩個說法都對,在 linux kernel 2.2 之前 backlog 指的是[1]和[2]兩個隊列的和。而 2.2 以后,就指的是[2]的大小,那么在 kernel 2.2 以后,[1]的大小怎么確定的呢?兩個隊列的作用分別是什么呢?

【1】SYN 半連接隊列的作用

對于 SYN 半連接隊列的大小是由(/proc/sys/net/ipv4/tcp_max_syn_backlog)這個內核參數(shù)控制的,有些內核似乎也受 listen 的 backlog 參數(shù)影響,取得是兩個值的最小值。當這個隊列滿了,Server 會丟棄新來的 SYN 包,而 Client 端在多次重發(fā) SYN 包得不到響應而返回(connection time out)錯誤。但是,當 Server 端開啟了 syncookies,那么 SYN 半連接隊列就沒有邏輯上的最大值了,并且/proc/sys/net/ipv4/tcp_max_syn_backlog 設置的值也會被忽略。

【2】accept 連接隊列

accept 連接隊列的大小是由 backlog 參數(shù)和(/proc/sys/net/core/somaxconn)內核參數(shù)共同決定,取值為兩個中的最小值。當 accept 連接隊列滿了,協(xié)議棧的行為根據(jù)(/proc/sys/net/ipv4/tcp_abort_on_overflow)內核參數(shù)而定。如果 tcp_abort_on_overflow=1,server 在收到 SYN_ACK 的 ACK 包后,協(xié)議棧會丟棄該連接并回復 RST 包給對端,這個是 Client 會出現(xiàn)(connection reset by peer)錯誤。如果 tcp_abort_on_overflow=0,server 在收到 SYN_ACK 的 ACK 包后,直接丟棄該 ACK 包。這個時候 Client 認為連接已經(jīng)建立了,一直在等 Server 的數(shù)據(jù),直到超時出現(xiàn) read timeout 錯誤。

參考資料

http://blog.csdn.net/dog250/article/details/6612496

http://coolshell.cn/articles/11564.html

http://coolshell.cn/articles/11609.html

 

http://www.tcpipguide.com/free/t_TCPMessageSegmentFormat.html

【本文為51CTO專欄作者“騰訊技術工程”原創(chuàng)稿件,轉載請聯(lián)系原作者(微信號:Tencent_TEG)】

戳這里,看該作者更多好文

 

責任編輯:武曉燕 來源: 51CTO專欄
相關推薦

2023-09-02 21:57:52

網(wǎng)絡TCP協(xié)議

2017-09-25 21:27:07

TCP協(xié)議數(shù)據(jù)鏈

2023-09-07 16:46:54

TCP數(shù)據(jù)傳遞

2015-11-09 09:58:56

2023-10-24 15:22:09

TCPUDP

2022-10-10 07:34:36

TCP三次握手區(qū)塊鏈

2020-12-08 06:34:16

TCP握手SYN 報文

2023-11-01 08:04:08

WiresharkTCP協(xié)議

2024-01-12 08:23:11

TCPACK服務器

2015-10-13 09:42:52

TCP網(wǎng)絡協(xié)議

2019-06-12 11:26:37

TCP三次握手四次揮手

2020-03-02 14:41:04

運維架構技術

2019-12-12 10:36:43

TCPSYNIP

2022-07-07 09:00:17

TCP 連接HTTP 協(xié)議

2023-03-06 15:43:56

2018-07-05 14:25:01

TCP握手原理

2018-10-15 08:06:33

TCP握手原理

2021-03-08 18:08:08

TCP Connect 協(xié)議

2024-10-09 20:54:16

2022-07-25 07:07:35

TCP客戶端服務器
點贊
收藏

51CTO技術棧公眾號