1. 程式人生 > >在深談TCP/IP三步握手&四步揮手原理及衍生問題—長文解剖IP

在深談TCP/IP三步握手&四步揮手原理及衍生問題—長文解剖IP

下面就是TCP/IP(Transmission Control Protoco/Internet Protocol )協議頭部的格式,是理解其它內容的基礎,就關鍵欄位做一些說明

anatomy_figure_1.jpg

  • Source Port和Destination Port:分別佔用16位,表示源埠號和目的埠號;用於區別主機中的不同程序,而IP地址是用來區分不同的主機的,源埠號和目的埠號配合上IP首部中的源IP地址和目的IP地址就能唯一的確定一個TCP連線;

  • Sequence Number:TCP連線中傳送的位元組流中的每個位元組都按順序編號,用來標識從TCP傳送端向TCP收收端傳送的資料位元組流,它表示在這個報文段中的的第一個資料位元組在資料流中的序號;主要用來解決網路報亂序的問題;

  • Acknowledgment Number:期望收到對方下一個報文的第一個資料位元組的序號個序號,因此,確認序號應當是上次已成功收到資料位元組序號加1。不過,只有當標誌位中的ACK標誌(下面介紹)為1時該確認序列號的欄位才有效。主要用來解決不丟包的問題;

  • Offset:它指出TCP報文的資料距離TCP報文段的起始處有多遠,給出首部中32 bit字的數目,需要這個值是因為任選欄位的長度是可變的。這個欄位佔4bit(最多能表示15個32bit的的字,即4*15=60個位元組的首部長度),因此TCP最多有60位元組的首部。然而,沒有任選欄位,正常的長度是20位元組;

  • TCP Flags:TCP首部中有6個標誌位元,它們中的多個可同時被設定為1,主要是用於操控TCP的狀態機的,依次為URG,ACK,PSH,RST,FIN。每個標誌位的意思如下:

    • SYN (Synchronize Sequence Numbers)-同步序列編號-同步標籤

      The segment is a request to synchronize sequence numbers and establish a connection. The sequence number field contains the sender's initial sequence number.

      該標誌僅在三次握手建立TCP連線時有效。它提示TCP連線的服務端檢查序列編號,該序列編號為TCP連線初始端(一般是客戶端)的初始序列編號。在這裡,可以把TCP序列編號看作是一個範圍從0到4,294,967,295的32位計數器。通過TCP連線交換的資料中每一個位元組都經過序列編號。在TCP報頭中的序列編號欄包括了TCP分段中第一個位元組的序列編號。

      在連線建立時用來同步序號。當SYN=1而ACK=0時,表明這是一個連線請求報文。對方若同意建立連線,則應在響應報文中使SYN=1和ACK=1. 因此, SYN置1就表示這是一個連線請求或連線接受報文。

    • ACK (Acknowledgement Number)-確認編號-確認標誌

      The segment carries an acknowledgement and the value of the acknowledgement number field is valid and contains the next sequence number that is expected from the receiver.

      大多數情況下該標誌位是置位的。TCP報頭內的確認編號欄內包含的確認編號(w+1,Figure-1)為下一個預期的序列編號,同時提示遠端系統已經成功接收所有資料。

      TCP協議規定,只有ACK=1時有效,也規定連線建立後所有傳送的報文的ACK必須為1

      網路上有很多錯誤說法,比如:ACK是可能與SYN,FIN等同時使用的,比如SYN和ACK可能同時為1,它表示的就是建立連線之後的響應,如果只是單個的一個SYN,它表示的只是建立連線。TCP的幾次握手就是通過這樣的ACK表現出來的。其實:ACK&SYN是標誌位,

    • FIN (Finish)-結束標誌

      The sender wants to close the connection

      用來釋放一個連線。

      當 FIN = 1 時,表明此報文段的傳送方的資料已經發送完畢,並要求釋放連線。

    • URG (The urgent pointer)-緊急標誌

      Segment is urgent and the urgent pointer field carries valid information.

      當URG=1,表明緊急指標欄位有效。告訴系統此報文段中有緊急資料

    • PSH (Push)-推標誌

      The data in this segment should be immediately pushed to the application layer on arrival.

      PSH為1的情況,一般只出現在 DATA內容不為0的包中,也就是說PSH=1表示有真正的TCP資料包內容被傳遞。

    • RST (Reset)-復位標誌

      There was some problem and the sender wants to abort the connection.

      當RST=1,表明TCP連線中出現嚴重差錯,必須釋放連線,然後再重新建立連線

po-1-1.png

Window(Advertised-Window)—視窗大小:滑動視窗,用來進行流量控制。佔2位元組,指的是通知接收方,傳送本報文你需要有多大的空間來接受

CWR (Congestion Window Reduced)

Set by an ECN-Capable sender when it reduces its congestion window (due to a retransmit timeout, a fast retransmit or in response to an ECN notification.

ECN (Explicit Congestion Notification)

During the three-way handshake it indicates that sender is capable of performing explicit congestion notification. Normally it means that a packet with the IP Congestion Experienced flag set was received during normal transmission. See RFC 3168 for more information.

 TCP的連線建立和連線關閉,都是通過請求-響應的模式完成的。我們來看下圖,應該基本能夠理解TCP握手揮手過程

TCP三次握手四次揮手過程

Three-way Handshake 三次握手 

三次握手的目的是:為了防止已失效的連線請求報文段突然又傳送到了服務端,因而產生錯誤。推薦閱讀《TCP的三次握手與四次揮手(詳解+動圖

當然,如果那邊同時開啟,就有可能是四次握手

TCP四次握手

 TCP 連線的初始化序列號能否固定

單個TCP包每次打包1448位元組的資料進行傳送(乙太網Ethernet最大的資料幀是1518位元組,乙太網幀的幀頭14位元組和幀尾CRC校驗4位元組(共佔18位元組),剩下承載上層協議的地方也就是Data域最大就只剩1500位元組. 這個值我們就把它稱之為MTU(Maximum Transmission Unit))。

那麼一次性發送大量資料,就必須分成多個包。比如,一個 10MB 的檔案,需要傳送7100多個包。

傳送的時候,TCP 協議為每個包編號(sequence number,簡稱 SEQ),以便接收的一方按照順序還原。萬一發生丟包,也可以知道丟失的是哪一個包。

第一個包的編號是一個隨機數—初始化序列號(縮寫為ISN:Inital Sequence Number)

為了便於理解,這裡就把它稱為1號包。假定這個包的負載長度是100位元組,那麼可以推算出下一個包的編號應該是101。這就是說,每個資料包都可以得到兩個編號:自身的編號,以及下一個包的編號。接收方由此知道,應該按照什麼順序將它們還原成原始檔案。

如果初始化序列號可以固定,我們來看看會出現什麼問題

假設ISN固定是1,Client和Server建立好一條TCP連線後,Client連續給Server發了10個包,這10個包不知怎麼被鏈路上的路由器快取了(路由器會毫無先兆地快取或者丟棄任何的資料包),這個時候碰巧Client掛掉了,然後Client用同樣的埠號重新連上Server,Client又連續給Server發了幾個包,假設這個時候Client的序列號變成了5。接著,之前被路由器快取的10個數據包全部被路由到Server端了,Server給Client回覆確認號10,這個時候,Client整個都不好了,這是什麼情況?我的序列號才到5,你怎麼給我的確認號是10了,整個都亂了。

RFC793中,建議ISN和一個假的時鐘綁在一起,這個時鐘會在每4微秒對ISN做加一操作,直到超過2^32,又從0開始,這需要4小時才會產生ISN的迴繞問題,這幾乎可以保證每個新連線的ISN不會和舊的連線的ISN產生衝突。這種遞增方式的ISN,很容易讓攻擊者猜測到TCP連線的ISN,現在的實現大多是在一個基準值的基礎上進行隨機的。

ip資料包.png

初始化連線的 SYN 超時問題

Client傳送SYN包給Server後掛了,Server回給Client的SYN-ACK一直沒收到Client的ACK確認,這個時候這個連線既沒建立起來,也不能算失敗。這就需要一個超時時間讓Server將這個連線斷開,否則這個連線就會一直佔用Server的SYN連線佇列中的一個位置,大量這樣的連線就會將Server的SYN連線佇列耗盡,讓正常的連線無法得到處理。

目前,Linux下預設會進行5次重發SYN-ACK包,重試的間隔時間從1s開始,下次的重試間隔時間是前一次的雙倍,5次的重試時間間隔為1s, 2s, 4s, 8s, 16s,總共31s,第5次發出後還要等32s都知道第5次也超時了,所以,總共需要 1s + 2s + 4s+ 8s+ 16s + 32s = 63s,TCP才會把斷開這個連線。由於,SYN超時需要63秒,那麼就給攻擊者一個攻擊伺服器的機會,攻擊者在短時間內傳送大量的SYN包給Server(俗稱 SYN flood 攻擊),用於耗盡Server的SYN佇列。對於應對SYN 過多的問題,linux提供了幾個TCP引數:tcp_syncookies、tcp_synack_retries、tcp_max_syn_backlog、tcp_abort_on_overflow 來調整應對。

  • 什麼是 SYN 攻擊(SYN Flood)

    SYN 攻擊指的是,攻擊客戶端在短時間內偽造大量不存在的IP地址,向伺服器不斷地傳送SYN包,伺服器回覆確認包,並等待客戶的確認。由於源地址是不存在的,伺服器需要不斷的重發直至超時,這些偽造的SYN包將長時間佔用未連線佇列,正常的SYN請求被丟棄,導致目標系統執行緩慢,嚴重者會引起網路堵塞甚至系統癱瘓。

    SYN 攻擊是一種典型的 DoS(Denial of Service)/DDoS(:Distributed Denial of Service) 攻擊。

  • 如何檢測 SYN 攻擊?

    檢測 SYN 攻擊非常的方便,當你在伺服器上看到大量的半連線狀態時,特別是源IP地址是隨機的,基本上可以斷定這是一次SYN攻擊。在 Linux/Unix 上可以使用系統自帶的 netstats 命令來檢測 SYN 攻擊。

  • 如何防禦 SYN 攻擊?

    SYN攻擊不能完全被阻止,除非將TCP協議重新設計。我們所做的是儘可能的減輕SYN攻擊的危害,常見的防禦 SYN 攻擊的方法有如下幾種:

    • 縮短超時(SYN Timeout)時間

    • 增加最大半連線數

    • 過濾閘道器防護

    • SYN cookies技術

如果已經建立了連線,但是客戶端突然出現故障了怎麼辦?

TCP還設有一個保活計時器,顯然,客戶端如果出現故障,伺服器不能一直等下去,白白浪費資源。伺服器每收到一次客戶端的請求後都會重新復位這個計時器,時間通常是設定為2小時,若兩小時還沒有收到客戶端的任何資料,伺服器就會發送一個探測報文段,以後每隔75分鐘傳送一次。若一連發送10個探測報文仍然沒反應,伺服器就認為客戶端出了故障,接著就關閉連線。 

Four-way Handshake 四次揮手

  • FIN_WAIT_1 :這個狀態得好好解釋一下,其實FIN_WAIT_1 和FIN_WAIT_2 兩種狀態的真正含義都是表示等待對方的FIN報文。而這兩種狀態的區別是:- FIN_WAIT_1狀態實際上是當SOCKET在ESTABLISHED狀態時,它想主動關閉連線,向對方傳送了FIN報文,此時該SOCKET進入到FIN_WAIT_1 狀態。而當對方迴應ACK報文後,則進入到FIN_WAIT_2 狀態。當然在實際的正常情況下,無論對方處於任何種情況下,都應該馬上回應ACK報文,所以FIN_WAIT_1 狀態一般是比較難見到的,而FIN_WAIT_2 狀態有時仍可以用netstat看到。

  • FIN_WAIT_2 :上面已經解釋了這種狀態的由來,實際上FIN_WAIT_2狀態下的SOCKET表示半連線,即有一方呼叫close()主動要求關閉連線。注意:FIN_WAIT_2 是沒有超時的(不像TIME_WAIT 狀態),這種狀態下如果對方不關閉(不配合完成4次揮手過程),那這個 FIN_WAIT_2 狀態將一直保持到系統重啟,越來越多的FIN_WAIT_2 狀態會導致核心crash。

  • TIME_WAIT :表示收到了對方的FIN報文,併發送出了ACK報文。 TIME_WAIT狀態下的TCP連線會等待2*MSL(Max Segment Lifetime,最大分段生存期,指一個TCP報文在Internet上的最長生存時間。每個具體的TCP協議實現都必須選擇一個確定的MSL值,RFC 1122建議是2分鐘,但BSD傳統實現採用了30秒,Linux可以cat /proc/sys/net/ipv4/tcp_fin_timeout看到本機的這個值),然後即可回到CLOSED 可用狀態了。如果FIN_WAIT_1狀態下,收到了對方同時帶FIN標誌和ACK標誌的報文時,可以直接進入到TIME_WAIT狀態,而無須經過FIN_WAIT_2狀態。

  • CLOSING :這種狀態在實際情況中應該很少見,屬於一種比較罕見的例外狀態。正常情況下,當一方傳送FIN報文後,按理來說是應該先收到(或同時收到)對方的ACK報文,再收到對方的FIN報文。但是CLOSING 狀態表示一方傳送FIN報文後,並沒有收到對方的ACK報文,反而卻也收到了對方的FIN報文。什麼情況下會出現此種情況呢?那就是當雙方几乎在同時close()一個SOCKET的話,就出現了雙方同時傳送FIN報文的情況,這是就會出現CLOSING 狀態,表示雙方都正在關閉SOCKET連線。

  • CLOSE_WAIT :表示正在等待關閉。怎麼理解呢?當對方close()一個SOCKET後傳送FIN報文給自己,你的系統毫無疑問地將會迴應一個ACK報文給對方,此時TCP連線則進入到CLOSE_WAIT狀態。接下來呢,你需要檢查自己是否還有資料要傳送給對方,如果沒有的話,那你也就可以close()這個SOCKET併發送FIN報文給對方,即關閉自己到對方這個方向的連線。有資料的話則看程式的策略,繼續傳送或丟棄。簡單地說,當你處於CLOSE_WAIT 狀態下,需要完成的事情是等待你去關閉連線。

  • LAST_ACK :當被動關閉的一方在傳送FIN報文後,等待對方的ACK報文的時候,就處於LAST_ACK 狀態。當收到對方的ACK報文後,也就可以進入到CLOSED 可用狀態了。

Screen Shot 2018-11-05 at 20.55.01.jpg

TCP 的 Peer 兩端同時斷開連線

由上面的”TCP協議狀態機 “圖可以看出

  1. TCP的Peer端在收到對端的FIN包前 發出了FIN包,那麼該Peer的狀態就變成了FIN_WAIT1

  2. Peer在FIN_WAIT1狀態下收到對端Peer對自己FIN包的ACK包的話,那麼Peer狀態就變成FIN_WAIT2,

  3. Peer在FIN_WAIT2下收到對端Peer的FIN包,在確認已經收到了對端Peer全部的Data資料包後,就響應一個ACK給對端Peer,然後自己進入TIME_WAIT狀態。

但是如果Peer在FIN_WAIT1狀態下首先收到對端Peer的FIN包的話,那麼該Peer在確認已經收到了對端Peer全部的Data資料包後,就響應一個ACK給對端Peer,然後自己進入CLOSEING狀態,Peer在CLOSEING狀態下收到自己的FIN包的ACK包的話,那麼就進入TIME WAIT 狀態。於是

TCP的Peer兩端同時發起FIN包進行斷開連線,那麼兩端Peer可能出現完全一樣的狀態轉移 FIN_WAIT1——>CLOSEING——->TIME_WAIT,也就會Client和Server最後同時進入TIME_WAIT狀態

TCP 的 TIME_WAIT 狀態

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

    • Peer兩端,哪一端會進入TIME_WAIT呢?為什麼?

      相信大家都知道,TCP主動關閉連線的那一方會最後進入TIME_WAIT。

      那麼怎麼界定主動關閉方呢?

      是否主動關閉是由FIN包的先後決定的,就是在自己沒收到對端Peer的FIN包之前自己發出了FIN包,那麼自己就是主動關閉連線的那一方。對於TCP 的 Peer 兩端同時斷開連線 描述的情況那麼Peer兩邊都是主動關閉的一方,兩邊都會進入TIME_WAIT。為什麼是主動關閉的一方進行TIME_WAIT呢,被動關閉的進入TIME_WAIT可以不呢?

      我們來看看TCP四次揮手可以簡單分為下面三個過程

      • 過程一.主動關閉方 傳送FIN;

      • 過程二.被動關閉方 收到主動關閉方的FIN後 傳送該FIN的ACK,被動關閉方傳送FIN;

      • 過程三.主動關閉方 收到被動關閉方的FIN後傳送該FIN的ACK,被動關閉方等待自己FIN的ACK問題就在過程三中,據TCP協議規範,不對ACK進行ACK。

        如果主動關閉方不進入TIME_WAIT,那麼主動關閉方在傳送完ACK就走了的話:如果最後傳送的ACK在路由過程中丟掉了,最後沒能到被動關閉方,這個時候被動關閉方 沒收到自己FIN的ACK就不能關閉連線,接著被動關閉方 會超時重發FIN包,但是這個時候已經沒有對端會給該FIN回ACK,被動關閉方就無法正常關閉連線了,所以主動關閉方需要進入TIME_WAIT 以便能夠重發丟掉的被動關閉方FIN的ACK。

    • TIME_WAIT狀態為什麼需要經過2MSL的時間才關閉連線呢

  1. 為了保證A傳送的最後一個確認報文段能夠到達B。這個確認報文段可能會丟失,如果B收不到這個確認報文段,其會重傳第三次“揮手”傳送的FIN+ACK報文,而A則會在2MSL時間內收到這個重傳的報文段,每次A收到這個重傳報文段後,就會重啟2MSL計時器。這樣可以保證A和B都能正常關閉連線。

  2. 為了防止已失效的報文段出現在下一次連線中。A經過2MSL時間後,可以保證在本次連線中傳輸的報文段都在網路中消失,這樣一來就能保證在後面的連線中不會出現舊的連線產生的報文段了。

  • TIME_WAIT狀態是用來解決或避免什麼問題呢

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

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

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

    3. 防止鏈路上已經關閉的連線的殘餘資料包(a lost duplicate packet or a wandering duplicate packet) 干擾正常的資料包,造成資料流的不正常。這個問題和2)類似

  • TIME_WAIT會帶來哪些問題呢

    TIME_WAIT帶來的問題注意是源於:一個連線進入TIME_WAIT狀態後需要等待2*MSL(一般是1到4分鐘)那麼長的時間才能斷開連線釋放連線佔用的資源,會造成以下問題

    1. 作為伺服器,短時間內關閉了大量的Client連線,就會造成伺服器上出現大量的TIME_WAIT連線,佔據大量的tuple,嚴重消耗著伺服器的資源。

    2. 作為客戶端,短時間內大量的短連線,會大量消耗的Client機器的埠,畢竟埠只有65535個,埠被耗盡了,後續就無法在發起新的連線了。

    ( 由於上面兩個問題,作為客戶端需要連本機的一個服務的時候,首選UNIX域套接字而不是TCP )

    TIME_WAIT很令人頭疼,很多問題是由TIME_WAIT造成的,但是TIME_WAIT又不是多餘的不能簡單將TIME_WAIT去掉,那麼怎麼來解決或緩解TIME_WAIT問題呢?可以進行TIME_WAIT的快速回收和重用來緩解TIME_WAIT的問題。

    • 有沒一些清掉TIME_WAIT的技巧呢

    1. 修改tcp_max_tw_buckets:tcp_max_tw_buckets 控制併發的TIME_WAIT的數量,預設值是180000。如果超過預設值,核心會把多的TIME_WAIT連線清掉,然後在日誌裡打一個警告。官網文件說這個選項只是為了阻止一些簡單的DoS攻擊,平常不要人為的降低它。

    2. 利用RST包從外部清掉TIME_WAIT連結:根據TCP規範,收到任何的傳送到未偵聽埠、已經關閉的連線的資料包、連線處於任何非同步狀態(LISTEN, SYS-SENT, SYN-RECEIVED)並且收到的包的ACK在視窗外,或者安全層不匹配,都要回執以RST響應(而收到滑動視窗外的序列號的資料包,都要丟棄這個資料包,並回復一個ACK包),核心收到RST將會產生一個錯誤並終止該連線。我們可以利用RST包來終止掉處於TIME_WAIT狀態的連線,其實這就是所謂的RST攻擊了。為了描述方便:假設Client和Server有個連線Connect1,Server主動關閉連線並進入了TIME_WAIT狀態,我們來描述一下怎麼從外部使得Server的處於 TIME_WAIT狀態的連線Connect1提前終止掉。要實現這個RST攻擊,首先我們要知道Client在Connect1中的埠port1(一般這個埠是隨機的,比較難猜到,這也是RST攻擊較難的一個點),利用IP_TRANSPARENT這個socket選項,它可以bind不屬於本地的地址,因此可以從任意機器繫結Client地址以及埠port1,然後向Server發起一個連線,Server收到了視窗外的包於是響應一個ACK,這個ACK包會路由到Client處,這個時候99%的可能Client已經釋放連線connect1了,這個時候Client收到這個ACK包,會發送一個RST包,server收到RST包然後就釋放連線connect1提前終止TIME_WAIT狀態了。提前終止TIME_WAIT狀態是可能會帶來(問題二、)中說的三點危害,具體的危害情況可以看下RFC1337。RFC1337中建議,不要用RST過早的結束TIME_WAIT狀態。

    TCP的延遲確認機制

    TCP在何時傳送ACK的時候有如下規定:

    1. 當有響應資料傳送的時候,ACK會隨著資料一塊傳送

    2. 如果沒有響應資料,ACK就會有一個延遲,以等待是否有響應資料一塊傳送,但是這個延遲一般在40ms~500ms之間,一般情況下在40ms左右,如果在40ms內有資料傳送,那麼ACK會隨著資料一塊傳送,對於這個延遲的需要注意一下,這個延遲並不是指的是收到資料到傳送ACK的時間延遲,而是核心會啟動一個定時器,每隔200ms就會檢查一次,比如定時器在0ms啟動,200ms到期,180ms的時候data來到,那麼200ms的時候沒有響應資料,ACK仍然會被髮送,這個時候延遲了20ms.

      這樣做有兩個目的。

      1. 這樣做的目的是ACK是可以合併的,也就是指如果連續收到兩個TCP包,並不一定需要ACK兩次,只要回覆最終的ACK就可以了,可以降低網路流量。

      2. 如果接收方有資料要傳送,那麼就會在傳送資料的TCP資料包裡,帶上ACK資訊。這樣做,可以避免大量的ACK以一個單獨的TCP包傳送,減少了網路流量。

    3. 如果在等待發送ACK期間,第二個資料又到了,這時候就要立即傳送ACK!

    按照TCP協議,確認機制是累積的。也就是確認號X的確認指示的是所有X之前但不包括X的資料已經收到了。確認號(ACK)本身就是不含資料的分段,因此大量的確認號消耗了大量的頻寬,雖然大多數情況下,ACK還是可以和資料一起捎帶傳輸的,但是如果沒有捎帶傳輸,那麼就只能單獨回來一個ACK,如果這樣的分段太多,網路的利用率就會下降。為緩解這個問題,RFC建議了一種延遲的ACK,也就是說,ACK在收到資料後並不馬上回復,而是延遲一段可以接受的時間。延遲一段時間的目的是看能不能和接收方要發給傳送方的資料一起回去,因為TCP協議頭中總是包含確認號的,如果能的話,就將資料一起捎帶回去,這樣網路利用率就提高了。延遲ACK就算沒有資料捎帶,那麼如果收到了按序的兩個包,那麼只要對第二包做確認即可,這樣也能省去一個ACK消耗。由於TCP協議不對ACK進行ACK的,RFC建議最多等待2個包的積累確認,這樣能夠及時通知對端Peer,我這邊的接收情況。Linux實現中,有延遲ACK(Delay Ack)和快速ACK,並根據當前的包的收發情況來在這兩種ACK中切換:在收到資料包的時候需要傳送ACK,進行快速ACK;否則進行延遲ACK(在無法使用快速確認的條件下也是)。

    一般情況下,ACK並不會對網路效能有太大的影響,延遲ACK能減少傳送的分段從而節省了頻寬,而快速ACK能及時通知傳送方丟包,避免滑動視窗停等,提升吞吐率。

    關於ACK分段,有個細節需要說明一下:

    ACK的確認號,是確認按序收到的最後一個位元組序,對於亂序到來的TCP分段,接收端會回覆相同的ACK分段,只確認按序到達的最後一個TCP分段。TCP連線的延遲確認時間一般初始化為最小值40ms,隨後根據連線的重傳超時時間(RTO)、上次收到資料包與本次接收資料包的時間間隔等引數進行不斷調整。

    TCP的重傳機制以及重傳的超時計算

    前面說過,每一個數據包都帶有下一個資料包的編號。如果下一個資料包沒有收到,那麼 ACK 的編號就不會發生變化

    如果傳送方發現收到三個連續的重複 ACK,或者超時了還沒有收到任何 ACK,就會確認丟包,從而再次傳送這個包。

    TCP丟包機制確認

    TCP的重傳超時計算

    TCP互動過程中,如果傳送的包一直沒收到ACK確認,是要一直等下去嗎

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

    如何計算設定這個RTO?

    • 設長了,重發就慢,丟了老半天才重發,沒有效率,效能差;

    • 設短了,會導致可能並沒有丟就重發。於是重發的就快,會增加網路擁塞,導致更多的超時,更多的超時導致更多的重發。

    RFC793中定義了一個經典演算法——加權移動平均(Exponential weighted moving average),演算法如下:

    1. 首先採樣計算RTT(Round Trip Time)值——也就是一個數據包從發出去到回來的時間

    2. 然後計算平滑的RTT,稱為SRTT(Smoothed Round Trip Time),SRTT = ( ALPHA * SRTT ) + ((1-ALPHA) * RTT)——其中的 α 取值在0.8 到 0.9之間

    3. RTO = min[UpBOUND,max[LowBOUND,(BETA*SRTT)]]——BETA(延遲方差因子(BETA is a delay variance factor (e.g., 1.3 to 2.0))

    TCP的重傳機制

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

    1. 僅重傳4號包

    • 優點:按需重傳,能夠最大程度節省頻寬。

    • 缺點:重傳會比較慢,因為重傳4號包後,需要等下一個超時才會重傳6號包

  • 重傳3號後面所有的包,也就是重傳4~10號包

    • 優點:重傳較快,資料能夠較快交付給接收端。

    • 缺點:重傳了很多不必要重傳的包,浪費頻寬,在出現丟包的時候,一般是網路擁塞,大量的重傳又可能進一步加劇擁塞。

    上面的問題是由於單純以時間驅動來進行重傳的,都必須等待一個超時時間,不能快速對當前網路狀況做出響應,如果加入以資料驅動呢?

    TCP引入了一種叫Fast Retransmit(快速重傳 )的演算法,就是在連續收到3次相同確認號的ACK,那麼就進行重傳。這個演算法基於這麼一個假設,連續收到3個相同的ACK,那麼說明當前的網路狀況變好了,可以重傳丟失的包了。

    快速重傳解決了timeout的問題,但是沒解決重傳一個還是重傳多個的問題。出現難以決定是否重傳多個包問題的根源在於,傳送端不知道那些非連續序號的包已經到達接收端了,但是接收端是知道的,如果接收端告訴一下發送端不就可以解決這個問題嗎?於是,RFC2018提出了 SACK(Selective Acknowledgment)——選擇確認機制,SACK是TCP的擴充套件選項

    淺析TCP之SACK

    一個SACK的例子如下圖,紅框說明:接收端收到了0-5500,8000-8500,7000-7500,6000-6500的資料了,這樣傳送端就可以選擇重傳丟失的5500-6000,6500-7000,7500-8000的包。

    2.png

    SACK依靠接收端的接收情況反饋,解決了重傳風暴問題,這樣夠了嗎?接收端能不能反饋更多的資訊呢?顯然是可以的,於是,RFC2883對對SACK進行了擴充套件,提出了D-SACK,也就是利用第一塊SACK資料中描述重複接收的不連續資料塊的序列號引數,其他SACK資料則描述其他正常接收到的不連續資料。這樣傳送方利用第一塊SACK,可以發現數據段被網路複製、錯誤重傳、ACK丟失引起的重傳、重傳超時等異常的網路狀況,使得傳送端能更好調整自己的重傳策略。

    D-SACK,有幾個優點:

    1. 傳送端可以判斷出,是發包丟失了,還是接收端的ACK丟失了。(傳送方,重傳了一個包,發現並沒有D-SACK那個包,那麼就是傳送的資料包丟了;否則就是接收端的ACK丟了,或者是傳送的包延遲到達了)

    2. 傳送端可以判斷自己的RTO是不是有點小了,導致過早重傳(如果收到比較多的D-SACK就該懷疑是RTO小了)。

    3. 傳送端可以判斷自己的資料包是不是被複制了。(如果明明沒有重傳該資料包,但是收到該資料包的D-SACK)

    4. 傳送端可以判斷目前網路上是不是出現了有些包被delay了,也就是出現先發的包卻後到了。

    TCP的流量控制

    ACK攜帶兩個資訊。

    • 期待要收到下一個資料包的編號

    • 接收方的接收視窗的剩餘容量

    TCP的標準視窗最大為2^16-1=65535個位元組

    TCP的選項欄位中還包含了一個TCP視窗擴大因子,option-kind為3,option-length為3個位元組,option-data取值範圍0-14

    視窗擴大因子用來擴大TCP視窗,可把原來16bit的視窗,擴大為31bit。這個視窗是接收端告訴傳送端自己還有多少緩衝區可以接收資料。於是傳送端就可以根據這個接收端的處理能力來發送資料,而不會導致接收端處理不過來。也就是:

    傳送端是根據接收端通知的視窗大小來調整自己的傳送速率的,以達到端到端的流量控制——Sliding Window(滑動視窗)。

    TCP的視窗機制 

    TCP協議裡視窗機制有2種:一種是固定的視窗大小;一種是滑動的視窗。

    這個視窗大小就是我們一次傳輸幾個資料。對所有資料幀按順序賦予編號,傳送方在傳送過程中始終保持著一個傳送視窗,只有落在傳送視窗內的幀才允許被髮送;同時接收方也維持著一個接收視窗,只有落在接收視窗內的幀才允許接收。這樣通過調整發送方視窗和接收方視窗的大小可以實現流量控制。

    下面一張圖來分析一下固定視窗大小有什麼問題

    假設視窗的大小是1,也是就每次只能傳送一個數據只有接受方對這個資料進行確認了以後才能傳送第2個數據。我們可以看到傳送方每傳送一個數據接受方就要給傳送方一個ACK對這個資料進行確認。只有接受到了這個確認資料以後傳送方才能傳輸下個數據。 這樣我們考慮一下如果說視窗過小,那麼當傳輸比較大的資料的時候需要不停的對資料進行確認,這個時候就會造成很大的延遲。如果說視窗的大小定義的過大。我們假設傳送方一次傳送100個數據。但是接收方只能處理50個數據。這樣每次都會只對這50個數據進行確認。傳送方下一次還是傳送100個數據,但是接受方還是隻能處理50個數據。這樣就避免了不必要的資料來擁塞我們的鏈路。所以我們就引入了滑動視窗機制,視窗的大小並不是固定的而是根據我們之間的鏈路的頻寬的大小,這個時候鏈路是否擁護塞。接受方是否能處理這麼多資料了。  

    我們看看滑動視窗是如何工作的

    首先是第一次傳送資料這個時候的視窗大小是根據鏈路頻寬的大小來決定的。我們假設這個時候視窗的大小是3。這個時候接受方收到資料以後會對資料進行確認告訴傳送方我下次希望手到的是資料是多少。這裡我們看到接收方傳送的ACK=3(這是傳送方傳送序列2的回答確認,下一次接收方期望接收到的是3序列訊號)。這個時候傳送方收到這個資料以後就知道我第一次傳送的3個數據對方只收到了2個。就知道第3個數據對方沒有收到。下次在傳送的時候就從第3個數據開始發。這個時候視窗大小就變成了2 。 

    這個時候傳送方傳送2個數據。 

    看到接收方傳送的ACK是5就表示他下一次希望收到的資料是5,傳送方就知道我剛才傳送的2個數據對方收了這個時候開始傳送第5個數據。 

    這就是滑動視窗的工作機制,當鏈路變好了或者變差了這個視窗還會發生變話,並不是第一次協商好了以後就永遠不變了。   

    TCP滑動視窗剖析

    滑動視窗協議的基本原理就是在任意時刻,傳送方都維持了一個連續的允許傳送的幀的序號,稱為傳送視窗;同時,接收方也維持了一個連續的允許接收的幀的序號,稱為接收視窗。傳送視窗和接收視窗的序號的上下界不一定要一樣,甚至大小也可以不同。不同的滑動視窗協議視窗大小一般不同。

    視窗有3種動作:展開(右邊向右),合攏(左邊向右),收縮(右邊向左)這三種動作受接收端的控制。

    合攏:表示已經收到相應位元組的確認了

    展開:表示允許快取傳送更多的位元組

    收縮(非常不希望出現的,某些實現是禁止的):表示本來可以傳送的,現在不能傳送;但是如果收縮的是那些已經發出的,就會有問題;為了避免,收端會等待到快取中有更多快取空間時才進行通訊。

    滑動視窗機制

    位元滑動視窗協議

    當傳送視窗和接收視窗的大小固定為1時,滑動視窗協議退化為停等協議(stop-and-wait)。該協議規定傳送方每傳送一幀後就要停下來,等待接收方已正確接收的確認(acknowledgement)返回後才能繼續傳送下一幀。由於接收方需要判斷接收到的幀是新發的幀還是重新發送的幀,因此傳送方要為每一個幀加一個序號。由於停等協議規定只有一幀完全傳送成功後才能傳送新的幀,因而只用一位元來編號就夠了。其傳送方和接收方執行的流程圖如圖所示。

    後退n協議

    由於停等協議要為每一個幀進行確認後才繼續傳送下一幀,大大降低了通道利用率,因此又提出了後退n協議。後退n協議中,傳送方在發完一個數據幀後,不停下來等待應答幀,而是連續傳送若干個資料幀,即使在連續傳送過程中收到了接收方發來的應答幀,也可以繼續傳送。且傳送方在每傳送完一個數據幀時都要設定超時定時器。只要在所設定的超時時間內仍收到確認幀,就要重發相應的資料幀。如:當傳送方傳送了N個幀後,若發現該N幀的前一個幀在計時器超時後仍未返回其確認資訊,則該幀被判為出錯或丟失,此時傳送方就不得不重新發送出錯幀及其後的N幀。

    從這裡不難看出,後退n協議一方面因連續傳送資料幀而提高了效率,但另一方面,在重傳時又必須把原來已正確傳送過的資料幀進行重傳(僅因這些資料幀之前有一個數據幀出了錯),這種做法又使傳送效率降低。由此可見,若傳輸通道的傳輸質量很差因而誤位元速率較大時,連續測協議不一定優於停止等待協議。此協議中的傳送視窗的大小為k,接收視窗仍是1。

    選擇重傳協議

    在後退n協議中,接收方若發現錯誤幀就不再接收後續的幀,即使是正確到達的幀,這顯然是一種浪費。另一種效率更高的策略是當接收方發現某幀出錯後,其後繼續送來的正確的幀雖然不能立即遞交給接收方的高層,但接收方仍可收下來,存放在一個緩衝區中,同時要求傳送方重新傳送出錯的那一幀。一旦收到重新傳來的幀後,就可以原已存於緩衝區中的其餘幀一併按正確的順序遞交高層。這種方法稱為選擇重發(SELECTICE REPEAT),其工作過程如圖所示。顯然,選擇重發減少了浪費,但要求接收方有足夠大的緩衝區空間。

    流量控制

    所謂流量控制,主要是接收方傳遞資訊給傳送方,使其不要傳送資料太快,是一種端到端的控制。主要的方式就是返回的ACK中會包含自己的接收視窗的大小,並且利用大小來控制傳送方的資料傳送。

    上圖中,我們可以看到:

    • 接收端LastByteRead指向了TCP緩衝區中讀到的位置,NextByteExpected指向的地方是收到的連續包的最後一個位置,LastByteRcved指向的是收到的包的最後一個位置,我們可以看到中間有些資料還沒有到達,所以有資料空白區。

    • 傳送端的LastByteAcked指向了被接收端Ack過的位置(表示成功傳送確認),LastByteSent表示發出去了,但還沒有收到成功確認的Ack,LastByteWritten指向的是上層應用正在寫的地方。

    於是:

    • 接收端在給傳送端回ACK中會彙報自己的AdvertisedWindow = MaxRcvBuffer – LastByteRcvd – 1;

    • 而傳送方會根據這個視窗來控制傳送資料的大小,以保證接收方可以處理。

    下面我們來看一下發送方的滑動視窗示意圖:

    TCP視窗info

    傳送端是怎麼做到比較方便知道自己哪些包可以發,哪些包不能發呢?

    一個簡明的方案就是按照接收方的視窗通告,傳送方維護一個一樣大小的傳送視窗就可以了。在視窗內的可以發,視窗外的不可以發,視窗在傳送序列上不斷後移,這就是TCP中的滑動視窗。如下圖所示,對於TCP傳送端其傳送快取內的資料都可以分為4類

        [1]-已經發送並得到接收端ACK的; 

        [2]-已經發送但還未收到接收端ACK的; 

        [3]-未傳送但允許傳送的(接收方還有空間); 

        [4]-未傳送且不允許傳送(接收方沒空間了)。

    其中,[2]和[3]兩部分合起來稱之為傳送視窗。

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

    TCP下面是個滑動後的示意圖

    如果接收端通知一個零視窗給傳送端,這個時候傳送端還能不能傳送資料呢?如果不發資料,那一直等接收埠通知一個非0視窗嗎,如果接收端一直不通知呢?

    下圖,展示了一個傳送端是怎麼受接收端控制的。由上圖我們知道,當接收端通知一個zero視窗的時候,傳送端的傳送視窗也變成了0,也就是傳送端不能發資料了。如果傳送端一直等待,直到接收端通知一個非零視窗在發資料的話,這似乎太受限於接收端,如果接收端一直不通知新的視窗呢?顯然傳送端不能幹等,起碼有一個主動探測的機制。為解決0視窗的問題,TCP使用了ZWP(Zero Window Probe)。

    Zero Window

    傳送端在視窗變成0後,會發ZWP的包給接收方,來探測目前接收端的視窗大小,讓接收方來ack他的Window尺寸。一般這個值會設定成3次,每次大約30-60秒(不同的實現可能會不一樣)。如果3次過後還是0的話,有的TCP實現就會發RST掉這個連線。

    注意:只要有等待的地方都可能出現DDoS攻擊。攻擊者可以在和Server建立好連線後,就向Server通告一個0視窗,然後Server端就只能等待進行ZWP,於是攻擊者會併發大量的這樣的請求,把Server端的資源耗盡。

    Silly Window Syndrome

    點選看大圖

    如果接收端處理能力很慢,這樣接收端的視窗很快被填滿,然後接收處理完幾個位元組,騰出幾個位元組的視窗後,通知傳送端,這個時候傳送端馬上就傳送幾個位元組給接收端嗎?傳送的話會不會太浪費了,就像一艘萬噸油輪只裝上幾斤的油就開去目的地一樣。我們的TCP+IP頭有40個位元組,為了幾個位元組,要達上這麼大的開銷,這太不經濟了。

    對於傳送端產生資料的能力很弱也一樣,如果傳送端慢吞吞產生幾個位元組的資料要傳送,這個時候該不該立即傳送呢?還是累積多點在傳送?

    本質就是一個避免傳送大量小包的問題。造成這個問題原因有二:

      1. 接收端一直在通知一個小的視窗;

        在接收端解決這個問題,David D Clark’s 方案,如果收到的資料導致window size小於某個值,就ACK一個0視窗,這就阻止傳送端在發資料過來。等到接收端處理了一些資料後windows size 大於等於了MSS,或者buffer有一半為空,就可以通告一個非0視窗。

      2. 傳送端本身問題,一直在傳送小包。這個問題,TCP中有個術語叫Silly Window Syndrome(糊塗視窗綜合症)。解決這個問題的思路有兩:

        1. 接收端不通知小視窗,

        2. 傳送端積累一下資料在傳送。

    是在傳送端解決這個問題,有個著名的Nagle’s algorithm。Nagle 演算法的規則

        1. 如果包長度達到 MSS ,則允許傳送;

        2. 如果該包含有 FIN ,則允許傳送;

        3. 設定了 TCP_NODELAY 選項,則允許傳送;

        4. 設定 TCP_CORK 選項時,若所有發出去的小資料包(包長度小於 MSS )均被確認,則允許傳送;

        5. 上述條件都未滿足,但發生了超時(一般為 200ms ),則立即傳送。

    規則[4]指出TCP連線上最多隻能有一個未被確認的小資料包。從規則[4]可以看出Nagle演算法並不禁止傳送小的資料包(超時時間內),而是避免傳送大量小的資料包。由於Nagle演算法是依賴ACK的,如果ACK很快的話,也會出現一直髮小包的情況,造成網路利用率低。TCP_CORK選項則是禁止傳送小的資料包(超時時間內),設定該選項後,TCP會盡力把小資料包拼接成一個大的資料包(一個 MTU)再發送出去,當然也不會一直等,發生了超時(一般為 200ms ),也立即傳送。Nagle 演算法和CP_CORK 選項提高了網路的利用率,但是增加是延時。從規則[3]可以看出,設定TCP_NODELAY 選項,就是完全禁用Nagle 演算法了。

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

    客戶端在請求下載HTTP svr中的一個小檔案,一般情況下,HTTP svr都是先發送HTTP響應頭部,然後在傳送HTTP響應BODY(特別是比較多的實現在傳送檔案的實施採用的是sendfile系統呼叫,這就出現write-write-read模式了)。當傳送頭部的時候,由於頭部較小,於是形成一個小的TCP包傳送到客戶端,這個時候開始傳送body,由於body也較小,這樣還是形成一個小的TCP資料包,根據Nagle演算法,HTTP svr已經發送一個小的資料包了,在收到第一個小包的ACK後或等待200ms超時後才能在發小包,HTTP svr不能傳送這個body小TCP包;

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

    TCP的擁塞控制

    由於TCP看不到網路的狀況,那麼擁塞控制是必須的並且需要採用試探性的方式來控制擁塞,於是擁塞控制要完成兩個任務:[1]公平性;[2]擁塞過後的恢復。

    重介紹一下Reno演算法(RFC5681),其包含4個部分:

        [1]慢熱啟動演算法 – Slow Start 

        [2]擁塞避免演算法 – Congestion Avoidance; 

        [3]快速重傳 - Fast Retransimit; 

        [4]快速恢復演算法 – Fast Recovery。

    慢熱啟動演算法 – Slow Start

    我們怎麼知道,對方線路的理想速率是多少呢?答案就是慢慢試。

    開始的時候,傳送得較慢,然後根據丟包的情況,調整速率:如果不丟包,就加快傳送速度;如果丟包,就降低傳送速度。慢啟動的演算法如下(cwnd全稱Congestion Window):

    1. 連線建好的開始先初始化cwnd = N,表明可以傳N個MSS大小的資料。

    2. 每當收到一個ACK,++cwnd; 呈線性上升

    3. 每當過了一個RTT,cwnd = cwnd*2; 呈指數讓升

    4. 還有一個慢啟動門限ssthresh(slow start threshold),是一個上限,當cwnd >= ssthresh時,就會進入"擁塞避免演算法 - Congestion Avoidance"

    根據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後採用了這篇論文的建議(Linux 核心裡面設定了(常量TCP_INIT_CWND),剛開始通訊的時候,傳送方一次性發送10個數據包,即"傳送視窗"的大小為10。然後停下來,等待接收方的確認,再繼續傳送)

    擁塞避免演算法 – Congestion Avoidance

    慢啟動的時候說過,cwnd是指數快速增長的,但是增長是有個門限ssthresh(一般來說大多數的實現ssthresh的值是65535位元組)的,到達門限後進入擁塞避免階段。在進入擁塞避免階段後,cwnd值變化演算法如下:

    1. 每收到一個ACK,調整cwnd 為 (cwnd + 1/cwnd) * MSS個位元組

    2. 每經過一個RTT的時長,cwnd增加1個MSS大小。

    TCP是看不到網路的整體狀況的,那麼TCP認為網路擁塞的主要依據是它重傳了報文段。前面我們說過TCP的重傳分兩種情況:

    1. 出現RTO超時,重傳資料包。這種情況下,TCP就認為出現擁塞的可能性就很大,於是它反應非常'強烈'

      1. 調整門限ssthresh的值為當前cwnd值的1/2。

      2. reset自己的cwnd值為1

      3. 然後重新進入慢啟動過程。

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

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

    1.  調整門限ssthresh的值為當前cwnd值的1/2。

    2.  將cwnd值設定為新的ssthresh的值

    3.  重新進入擁塞避免階段。

    在快速重傳的時候,一般網路只是輕微擁堵,在進入擁塞避免後,cwnd恢復的比較慢。針對這個,“快速恢復”演算法被新增進來,當收到3個冗餘ACK時,TCP最後的[3]步驟進入的不是擁塞避免階段,而是快速恢復階段。

    快速恢復演算法 – Fast Recovery :

    快速恢復的思想是“資料包守恆”原則,即頻寬不變的情況下,在網路同一時刻能容納資料包數量是恆定的。當“老”資料包離開了網路後,就能向網路中傳送一個“新”的資料包。既然已經收到了3個冗餘ACK,說明有三個資料分段已經到達了接收端,既然三個分段已經離開了網路,那麼就是說可以在傳送3個分段了。於是只要傳送方收到一個冗餘的ACK,於是cwnd加1個MSS。快速恢復步驟如下(在進入快速恢復前,cwnd 和 sshthresh已被更新為:sshthresh = cwnd /2,cwnd = sshthresh):

    1. 把cwnd設定為ssthresh的值加3,重傳Duplicated ACKs指定的資料包

    2. 如果再收到 duplicated Acks,那麼cwnd = cwnd +1

    3. 如果收到新的ACK,而非duplicated Ack,那麼將cwnd重新設定為【3】中1)的sshthresh的值。然後進入擁塞避免狀態。

    細心的同學可能會發現快速恢復有個比較明顯的缺陷就是:它依賴於3個冗餘ACK,並假定很多情況下,3個冗餘的ACK只代表丟失一個包。但是3個冗餘ACK也很有可能是丟失了很多個包,快速恢復只是重傳了一個包,然後其他丟失的包就只能等待到RTO超時了。超時會導致ssthresh減半,並且退出了Fast Recovery階段,多個超時會導致TCP傳輸速率呈級數下降。出現這個問題的主要原因是過早退出了Fast Recovery階段。為解決這個問題,提出了New Reno演算法,該演算法是在沒有SACK的支援下改進Fast Recovery演算法(SACK改變TCP的確認機制,把亂序等資訊會全部告訴對方,SACK本身攜帶的資訊就可以使得傳送方有足夠的資訊來知道需要重傳哪些包,而不需要重傳哪些包),具體改進如下:

    1. 傳送端收到3個冗餘ACK後,重傳冗餘ACK指示可能丟失的那個包segment1,如果segment1的ACK通告接收端已經收到傳送端的全部已經發出的資料的話,那麼就是隻丟失一個包,如果沒有,那麼就是有多個包丟失了。

    2. 傳送端根據segment1的ACK判斷出有多個包丟失,那麼傳送端繼續重傳視窗內未被ACK的第一個包,直到sliding window內發出去的包全被ACK了,才真正退出Fast Recovery階段。

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

    總的來說TCP是一個有連線的、可靠的、帶流量控制和擁塞控制的端到端的協議。TCP的傳送端能發多少資料,由傳送端的傳送視窗決定(當然傳送視窗又被接收端的接收視窗、傳送端的擁塞視窗限制)的,那麼一個TCP連線的傳輸穩定狀態應該體現在傳送端的傳送視窗的穩定狀態上,這樣的話,TCP的傳送視窗有哪些穩定狀態呢?

    TCP的傳送視窗穩定狀態主要有上面三種穩定狀態:

    【1】接收端擁有大視窗的經典鋸齒狀

    大多數情況下都是處於這樣的穩定狀態,這是因為,一般情況下機器的處理速度就是比較快,這樣TCP的接收端都是擁有較大的視窗,這時傳送端的傳送視窗就完全由其擁塞視窗cwnd決定了;網路上擁有成千上萬的TCP連線,它們在相互爭用網路頻寬,TCP的流量控制使得它想要獨享整個網路,而擁塞控制又限制其必要時做出犧牲來體現公平性。於是在傳輸穩定的時候TCP傳送端呈現出下面過程的反覆

        [1]用慢啟動或者擁塞避免方式不斷增加其擁塞視窗,直到丟包的發生;

        [2]然後將傳送視窗將下降到1或者下降一半,進入慢啟動或者擁塞避免階段(要看是由於超時丟包還是由於冗餘ACK丟包);過程如下圖:

    【2】接收端擁有小視窗的直線狀態

    這種情況下是接收端非常慢速,接收視窗一直很小,這樣傳送視窗就完全有接收視窗決定了。由於傳送視窗小,傳送資料少,網路就不會出現擁塞了,於是傳送視窗就一直穩定的等於那個較小的接收視窗,呈直線狀態。

    【3】兩個直連網路端點間的滿載狀態下的直線狀態

    這種情況下,Peer兩端直連,並且只有位於一個TCP連線,那麼這個連線將獨享網路頻寬,這裡不存在擁塞問題,在他們處理能力足夠的情況下,TCP的流量控制使得他們能夠跑慢整個網路頻寬。

    通過上面我們知道,在TCP傳輸穩定的時候,各個TCP連線會均分網路頻寬的。相信大家學生時代經常會發生這樣的場景,自己在看視訊的時候突然出現視訊卡頓,於是就大叫起來,哪個開了迅雷,趕緊給我停了。其實簡單的下載加速就是開啟多個TCP連線來分段下載就達到加速的效果,假設宿舍的頻寬是1000K/s,一開始兩個在看視訊,每人平均網速是500k/s,這速度看起視訊來那叫一個順溜。突然其中一個同學打開啟迅雷