【網絡】tcp/ip詳解

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

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

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

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。每一個標誌位的意思以下:nginx

    • 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分段中第一個字節的序列編號。git

      在鏈接創建時用來同步序號。當SYN=1而ACK=0時,代表這是一個鏈接請求報文。對方若贊成創建鏈接,則應在響應報文中使SYN=1和ACK=1. 所以, SYN置1就表示這是一個鏈接請求或鏈接接受報文。github

    • 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必須爲1chrome

      網絡上有不少錯誤說法,好比:ACK是可能與SYN,FIN等同時使用的,好比SYN和ACK可能同時爲1,它表示的就是創建鏈接以後的響應,若是隻是單個的一個SYN,它表示的只是創建鏈接。TCP的幾回握手就是經過這樣的ACK表現出來的。其實:ACK&SYN是標誌位,shell

    • 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鏈接中出現嚴重差錯,必須釋放鏈接,而後再從新創建鏈接

  • 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握手揮手過程
image.png

Three-way Handshake 三次握手 

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

固然,若是那邊同時打開,就有多是四次握手
image.png

在此推薦閱讀《面試題·TCP 爲何要三次握手,四次揮手?

TCP 鏈接的雙方會經過三次握手肯定 TCP 鏈接的初始序列號、窗口大小以及最大數據段,這樣通訊雙方就能利用鏈接中的初始序列號保證雙方數據段的不重不漏、經過窗口大小控制流量並使用最大數據段避免 IP 協議對數據包的分片。

2014 年提出的 TCP 快啓(TCP Fast Open,TFO)卻能夠在某些場景下經過一次通訊創建 TCP 鏈接。目前TFO被植入了Linux 2.6.34內核,所以RHEL7/CentOS7是支持的,但默認沒有開啓,須要手動開啓:echo 3 > /proc/sys/net/ipv4/tcp_fastopen。

TCP 快啓TCP Fast Open

TCP 快啓策略使用存儲在客戶端的 TFO Cookie 與服務端快速創建鏈接。
image.png

TCP 鏈接的客戶端向服務端發送 SYN 消息時會攜帶快啓選項,服務端會生成一個 Cookie 並將其發送至客戶端,客戶端會緩存該 Cookie,當其與服務端從新創建鏈接時,它會使用存儲的 Cookie 直接創建 TCP 鏈接,服務端驗證 Cookie 後會向客戶端發送 SYN 和 ACK 並開始傳輸數據,這也就能減小通訊的次數。

  1. 客戶端發送SYN包,包尾加一個FOC請求,只有4個字節。
  2. 服務端受到FOC請求,驗證後根據來源ip地址聲稱cookie(8個字節),將這個COOKIE加載SYN+ACK包的末尾發送回去。
  3. 客戶端緩存住獲取到的Cookie 能夠給下一次使用。
  4. 下一次請求開始,客戶端發送SYN包,這時候後面帶上緩存的COOKIE,而後就是正式發送的數據。
  5. 服務器端驗證COOKIE正確,將數據交給上層應用處理獲得相應結果,而後在發送SYN+ACK時,再也不等待客戶端的ACK確認,即開始發送相應數據。

TFO是GOOGLE發佈的。目前chrome已經支持TFO,老版的默認關閉。

TFO存在的問題

  1. 客戶端的TFOcookie多長時間後刪除,誰來維護和刪除?
  2. nginx的TFO隊列具體是什麼意思?隊列滿了會怎樣?數值設定多少合適?
  3. 隊列是RFC7413中的一種對服務器的安全保護機制,超出隊列的數據包,會降級到普通的無cookie鏈接方式,即TFO功能失效。但這個數值具體設置多少不太好定。

因此,這裏也很少討論,本人只是對純展現內容開啓TFO。

 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狀態是可能會帶來(問題2、)中說的三點危害,具體的危害狀況能夠看下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.

    這樣作有兩個目的。

  3. 這樣作的目的是ACK是能夠合併的,也就是指若是連續收到兩個TCP包,並不必定須要ACK兩次,只要回覆最終的ACK就能夠了,能夠下降網絡流量。
  4. 若是接收方有數據要發送,那麼就會在發送數據的TCP數據包裏,帶上ACK信息。這樣作,能夠避免大量的ACK以一個單獨的TCP包發送,減小了網絡流量。
  5. 若是在等待發送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發送端連續發了一、二、三、四、五、六、七、八、九、10共10包,其中四、六、8這3個包全丟失了,因爲TCP的ACK是確認最後連續收到序號,這樣發送端只能收到3號包的ACK,這樣在TIME_OUT的時候,發送端就面臨下面兩個重傳選擇:

  1. 僅重傳4號包
  • 優勢:按需重傳,可以最大程度節省帶寬。
  • 缺點:重傳會比較慢,由於重傳4號包後,須要等下一個超時纔會重傳6號包
  1. 重傳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(糊塗窗口綜合症)。解決這個問題的思路有兩:
  3. 接收端不通知小窗口,
  4. 發送端積累一下數據在發送。

是在發送端解決這個問題,有個著名的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就認爲出現擁塞的可能性就很大,因而它反應很是'強烈'
  2. 調整門限ssthresh的值爲當前cwnd值的1/2。
  3. reset本身的cwnd值爲1
  4. 而後從新進入慢啓動過程。
  5. 在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 協議簡介》(阮一峯)

相關文章
相關標籤/搜索