首頁 > theory > network > > 正文

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

發布人:[email protected]    點擊:

長文是對TCP IP的街剖析歸類總結,就自己的經驗再次回顧IP協議而寫的歸納性筆記,助力初學者掌握。文有不妥之處,請查看原文并留言告知,謝謝!

如果對網絡工程基礎不牢,建議通讀《細說OSI七層協議模型及OSI參考模型中的數據封裝過程?

下面就是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 連接的初始化序列號能否固定

單個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,現在的實現大多是在一個基準值的基礎上進行隨機的。

注:這些內容引用自《從 TCP 三次握手說起:淺析TCP協議中的疑難雜癥 》,推薦查看。

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 四次揮手

現來看下TCP各種狀態含義解析(節選改編自《TCP、UDP 的區別,三次握手、四次揮手

  • 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-IP詳解:Delay ACK

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中RTT的測量和RTO的計算》 

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號包

  2. 重傳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協議

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


選擇重傳協議

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

選擇重傳協議

推薦閱讀《計算機網絡 TCP 滑動窗口協議 詳解


流量控制

所謂流量控制,主要是接收方傳遞信息給發送方,使其不要發送數據太快,是一種端到端的控制。主要的方式就是返回的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

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/IP之TCP協議:流量控制(滑動窗口協議)

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發展到現在,擁塞控制方面的算法很多,請查看《wiki-具體實現算法》,《斐訊面試記錄—TCP滑動窗口及擁塞控制

總的來說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,這速度看起視頻來那叫一個順溜。突然其中一個同學打打開迅雷開著99個TCP連接在下載愛情動作片,這個時候平均下來你能分到的帶寬就剩下10k/s,這網速下你的視頻還不卡成幻燈片。在通信鏈路帶寬固定(假設為W),多人公用一個網絡帶寬的情況下,利用TCP協議的擁塞控制的公平性,多開幾個TCP連接就能多分到一些帶寬(當然要忽略有些用UDP協議帶來的影響),然而不管怎么最多也就能把整個帶寬搶到,于是在占滿整個帶寬的情況下,下載一個大小為FS的文件,那么最快需要的時間是FS/W,難道就沒辦法加速了嗎?


答案是有的,這樣因為網絡是網狀的,一個節點是要和很多幾點互聯的,這就存在多個帶寬為W的通信鏈路,如果我們能夠將要下載的文件,一半從A通信鏈路下載,另外一半從B通信鏈路下載,這樣整個下載時間就減半了為FS/(2W),這就是p2p加速。


其實《不為人知的網絡編程:淺析TCP協議中的疑難雜癥》講的非常細,而且一遍文章根本總結不了(我也只是搬運工而已,因為所知的太少,都不像筆記了

基礎科普類:https://hit-alibaba.github.io/interview/basic/network/HTTP.html

推薦文章:

跟著動畫學習 TCP 三次握手和四次揮手

滑動窗口控制流量的原理

TCP 協議簡介》(阮一峰)


推薦書本:

圖解HTTP,圖解TCP/IP 系列書(日本人出的)或者head first 系列的,嗑書的話:清華大學出版社的TCP/IP原理(美版教程翻譯版)。不要讀啥  十天學會網絡工程、tcp ip 這樣學的 一些水貨文章,基本上是浪費時間。