淺析TCP協議中的疑難雜症

IT技術精選文摘發表於2018-05-27

前言


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

疑症 1 :TCP 的三次握手、四次揮手


下面兩圖大家再熟悉不過了,TCP的三次握手和四次揮手見下面左邊的”TCP建立連線”、”TCP資料傳送”、”TCP斷開連線”時序圖和右邊的”TCP協議狀態機”:

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

TCP協議狀態機:
要弄清TCP建立連線需要幾次互動才行,我們需要弄清建立連線進行初始化的目標是什麼。TCP進行握手初始化一個連線的目標是:分配資源、初始化序列號(通知peer對端我的初始序列號是多少),知道初始化連線的目標,那麼要達成這個目標的過程就簡單了。

握手過程可以簡化為下面的四次互動:

  • 1 ) clien 端首先傳送一個 SYN 包告訴 Server 端我的初始序列號是 X;

  • 2 ) Server 端收到 SYN 包後回覆給 client 一個 ACK 確認包,告訴 client 說我收到了;

  • 3 ) 接著 Server 端也需要告訴 client 端自己的初始序列號,於是 Server 也傳送一個 SYN 包告訴 client 我的初始序列號是Y;

  • 4 ) Client 收到後,回覆 Server 一個 ACK 確認包說我知道了。


整個過程4次互動即可完成初始化,但是,細心的同學會發現兩個問題:

  • 1)Server傳送SYN包是作為發起連線的SYN包,還是作為響應發起者的SYN包呢?怎麼區分?比較容易引起混淆;

  • 2)Server的ACK確認包和接下來的SYN包可以合成一個SYN ACK包一起傳送的,沒必要分別單獨傳送,這樣省了一次互動同時也解決了問題[1]. 這樣TCP建立一個連線,三次握手在進行最少次互動的情況下完成了Peer兩端的資源分配和初始化序列號的交換。


大部分情況下建立連線需要三次握手,也不一定都是三次,有可能出現四次握手來建立連線的。

如下圖,當Peer兩端同時發起SYN來建立連線的時候,就出現了四次握手來建立連線(對於有些TCP/IP的實現,可能不支援這種同時開啟的情況):


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

  • 1)初始化序列號X、Y是可以是寫死固定的嗎,為什麼不能呢?

  • 2)假如Client傳送一個SYN包給Server後就掛了或是不管了,這個時候這個連線處於什麼狀態呢?會超時嗎?為什麼呢?


TCP進行斷開連線的目標是:回收資源、終止資料傳輸。由於TCP是全雙工的,需要Peer兩端分別各自拆除自己通向Peer對端的方向的通訊通道。

這樣需要四次揮手來分別拆除通訊通道,就比較清晰明瞭了:

  • 1)Client 傳送一個FIN包來告訴 Server 我已經沒資料需要發給 Server了;

  • 2)Server 收到後回覆一個 ACK 確認包說我知道了;

  • 3)然後 server 在自己也沒資料傳送給client後,Server 也傳送一個 FIN 包給 Client 告訴 Client 我也已經沒資料發給client 了;

  • 4)Client 收到後,就會回覆一個 ACK 確認包說我知道了。


到此,四次揮手,這個TCP連線就可以完全拆除了。

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

  • 1)Client和Server同時發起斷開連線的FIN包會怎麼樣呢,TCP狀態是怎麼轉移的?

  • 2)左側圖中的四次揮手過程中,Server端的ACK確認包能不能和接下來的FIN包合併成一個包呢,這樣四次揮手就變成三次揮手了。

  • 3)四次揮手過程中,首先斷開連線的一端,在回覆最後一個ACK後,為什麼要進行TIME_WAIT呢(超時設定是 2*MSL,RFC793定義了MSL為2分鐘,Linux設定成了30s),在TIME_WAIT的時候又不能釋放資源,白白讓資源佔用那麼長時間,能不能省了TIME_WAIT呢,為什麼?

疑症 2 : TCP 連線的初始化序列號能否固定


如果初始化序列號(縮寫為ISN:Inital Sequence Number)可以固定,我們來看看會出現什麼問題:

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

疑症 3 : 初始化連線的 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 來調整應對。

疑症 4 : TCP 的 Peer 兩端同時斷開連線


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

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

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

  • 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狀態。

同時關閉連線的狀態轉移如下圖所示:

疑症 5 : 四次揮手能不能變成三次揮手呢?


答案是可能的。

TCP是全雙工通訊,Cliet在自己已經不會在有新的資料要傳送給Server後,可以傳送FIN訊號告知Server,這邊已經終止Client到對端Server那邊的資料傳輸。但是,這個時候對端Server可以繼續往Client這邊傳送資料包。於是,兩端資料傳輸的終止在時序上是獨立並且可能會相隔比較長的時間,這個時候就必須最少需要2+2 = 4 次揮手來完全終止這個連線。但是,如果Server在收到Client的FIN包後,在也沒資料需要傳送給Client了,那麼對Client的ACK包和Server自己的FIN包就可以合併成為一個包傳送過去,這樣四次揮手就可以變成三次了(似乎linux協議棧就是這樣實現的)。

疑症 6 : TCP 的頭號疼症 TIME_WAIT 狀態


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

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


相信大家都知道,TCP主動關閉連線的那一方會最後進入TIME_WAIT。那麼怎麼界定主動關閉方呢?是否主動關閉是由FIN包的先後決定的,就是在自己沒收到對端Peer的FIN包之前自己發出了FIN包,那麼自己就是主動關閉連線的那一方。對於疑症(4)中描述的情況,那麼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。

2TIME_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)類似。


3TIME_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的技巧呢?


TIME_WAIT的快速回收和重用


1TIME_WAIT快速回收


linux下開啟TIME_WAIT快速回收需要同時開啟tcp_tw_recycle和tcp_timestamps(預設開啟)兩選項。Linux下快速回收的時間為3.5 * RTO(Retransmission Timeout),而一個RTO時間為200ms至120s。開啟快速回收TIME_WAIT,可能會帶來(問題一、)中說的三點危險。

為了避免這些危險,要求同時滿足以下三種情況的新連線要被拒絕掉:

  • 1)來自同一個對端Peer的TCP包攜帶了時間戳;

  • 2)之前同一臺peer機器(僅僅識別IP地址,因為連線被快速釋放了,沒了埠資訊)的某個TCP資料在MSL秒之內到過本Server;

  • 3)Peer機器新連線的時間戳小於peer機器上次TCP到來時的時間戳,且差值大於重放視窗戳(TCP_PAWS_WINDOW)。初看起來正常的資料包同時滿足下面3條几乎不可能, 因為機器的時間戳不可能倒流的,出現上述的3點均滿足時,一定是老的重複資料包又回來了,丟棄老的SYN包是正常的。到此,似乎啟用快速回收就能很大程度緩解TIME_WAIT帶來的問題。但是,這裡忽略了一個東西就是NAT。。。在一個NAT後面的所有Peer機器在Server看來都是一個機器,NAT後面的那麼多Peer機器的系統時間戳很可能不一致,有些快,有些慢。這樣,在Server關閉了與系統時間戳快的Client的連線後,在這個連線進入快速回收的時候,同一NAT後面的系統時間戳慢的Client向Server發起連線,這就很有可能同時滿足上面的三種情況,造成該連線被Server拒絕掉。所以,在是否開啟tcp_tw_recycle需要慎重考慮了。


2TIME_WAIT重用


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

  • 1)新連線SYN告知的初始序列號比TIME_WAIT老連線的末序列號大;

  • 2)如果開啟了tcp_timestamps,並且新到來的連線的時間戳比老連線的時間戳大。


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

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

清掉TIME_WAIT的奇技怪巧


可以用下面兩種方式控制伺服器的TIME_WAIT數量:

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

  • 利用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狀態。


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

  • 1)TCP的可靠傳輸是確認號來實現的,那麼TCP的確認機制是怎樣的呢?是收到一個包就馬上確認,還是可以稍等一下在確認呢?

  • 2)假如傳送一個包,一直都沒收到確認呢?什麼時候重傳呢?超時機制的怎樣的?

  • 3)TCP兩端Peer的處理能力不對等的時候,比如傳送方處理能力很強,接收方處理能力很弱,這樣傳送方是否能夠不管接收方死活狂發資料呢?如果不能,流量控制機制的如何的?

  • 4)TCP是端到端的協議,也就是TCP對端Peer只看到對方,看不到網路上的其他點,那麼TCP的兩端怎麼對網路情況做出反映呢?發生擁塞的時候,擁塞控制機制是如何的?


疑症7:TCP的延遲確認機制


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

延遲ACK就算沒有資料捎帶,那麼如果收到了按序的兩個包,那麼只要對第二包做確認即可,這樣也能省去一個ACK消耗。由於TCP協議不對ACK進行ACK的,RFC建議最多等待2個包的積累確認,這樣能夠及時通知對端Peer,我這邊的接收情況。Linux實現中,有延遲ACK和快速ACK,並根據當前的包的收發情況來在這兩種ACK中切換。一般情況下,ACK並不會對網路效能有太大的影響,延遲ACK能減少傳送的分段從而節省了頻寬,而快速ACK能及時通知傳送方丟包,避免滑動視窗停等,提升吞吐率。

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

疑症8:TCP的重傳機制以及重傳的超時計算


1TCP的重傳超時計算


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

為了計算這個RTO,RFC793中定義了一個經典演算法,演算法如下:

  • 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樣本的時候,是用第一次發資料的時間和ack回來的時間做RTT樣本值,還是用重傳的時間和ACK回來的時間做RTT樣本值?不管是怎麼選擇,總會造成會要麼把RTT算過長了,要麼把RTT算過短了。如下圖:(a)就計算過長了,而(b)就是計算過短了。

針對上面經典演算法的缺陷,於是提出Karn / Partridge Algorithm對經典演算法進行了改進(演算法大特點是——忽略重傳,不把重傳的RTT做取樣),但是這個演算法有問題:如果在某一時間,網路閃動,突然變慢了,產生了比較大的延時,這個延時導致要重轉所有的包(因為之前的RTO很小)。於是,因為重轉的不算,所以,RTO就不會被更新,這是一個災難。

於是,為解決上面兩個演算法的問題,又有人推出來了一個新的演算法,這個演算法叫Jacobson / Karels Algorithm(參看RFC6289),這個演算法的核心是:除了考慮每兩次測量值的偏差之外,其變化率也應該考慮在內,如果變化率過大,則通過以變化率為自變數的函式為主計算RTT(如果陡然增大,則取值為比較大的正數,如果陡然減小,則取值為比較小的負數,然後和平均值加權求和),反之如果變化率很小,則取測量平均值。

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

SRTT = SRTT + α (RTT – SRTT) —— 計算平滑RTT;
DevRTT = (1-β)DevRTT + β(|RTT-SRTT|) ——計算平滑RTT和真實的差距(加權移動平均);
RTO= μ SRTT + ∂ DevRTT —— 神一樣的公式。

(其中:在Linux下,α = 0.125,β = 0.25, μ = 1,∂ = 4 ——這就是演算法中的“調得一手好引數”,nobody knows why, it just works…)


最後的這個演算法在被用在今天的TCP協議中並工作非常好。

知道超時怎麼計算後,很自然就想到定時器的設計問題。一個簡單直觀的方案就是為TCP中的每一個資料包維護一個定時器,在這個定時器到期前沒收到確認,則進行重傳。這種設計理論上是很合理的,但是實現上,這種方案將會有非常多的定時器,會帶來巨大記憶體開銷和排程開銷。既然不能每個包一個定時器,那麼多少個包一個定時器才好呢,這個似乎比較難確定。可以換個思路,不要以包量來確定定時器,以連線來確定定時器會不會比較合理呢?

目前,採取每一個TCP連線單一超時定時器的設計則成了一個預設的選擇,並且RFC2988給出了每連線單一定時器的設計建議演算法規則:

  • 1)每一次一個包含資料的包被髮送(包括重發),如果還沒開啟重傳定時器,則開啟它,使得它在RTO秒之後超時(按照當前的RTO值);

  • 2)當接收到一個ACK確認一個新的資料, 如果所有的發出資料都被確認了,關閉重傳定時器;

  • 3)當接收到一個ACK確認一個新的資料,還有資料在傳輸,也就是還有沒被確認的資料,重新啟動重傳定時器,使得它在RTO秒之後超時(按照當前的RTO值);

  • 4)當重傳定時器超時後,依次做下列3件事情:
       - 4.1)重傳最早的尚未被TCP接收方ACK的資料包
       - 4.2)重新設定RTO 為 RTO * 2(“還原定時器”),但是新RTO不應該超過RTO的上限(RTO有個上限值,這個上限值最少為60s)
       - 4.3)重啟重傳定時器。


上面的建議演算法體現了一個原則:沒被確認的包必須可以超時,並且超時的時間不能太長,同時也不要過早重傳。規則[1][3][4.3]共同說明了只要還有資料包沒被確認,那麼定時器一定會是開啟著的(這樣滿足 沒被確認的包必須可以超時的原則)。規則[4.2]說明定時器的超時值是有上限的(滿足 超時的時間不能太長 )。規則[3]說明,在一個ACK到來後重置定時器可以保護後發的資料不被過早重傳;因為一個ACK到來了,說明後續的ACK很可能會依次到來,也就是說丟失的可能性並不大。規則[4.2]也是在一定程度上避免過早重傳,因為,在出現定時器超時後,有可能是網路出現擁塞了,這個時候應該延長定時器,避免出現大量的重傳進一步加劇網路的擁塞。

2TCP的重傳機制


通過上面我們可以知道,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號包;

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


對於,上面兩個選擇的優缺點都比較明顯:

  • 方案[1]-優點:按需重傳,能夠最大程度節省頻寬。缺點:重傳會比較慢,因為重傳4號包後,需要等下一個超時才會重傳6號包;

  • 方案[2]-優點:重傳較快,資料能夠較快交付給接收端。缺點:重傳了很多不必要重傳的包,浪費頻寬,在出現丟包的時候,一般是網路擁塞,大量的重傳又可能進一步加劇擁塞。



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

快速重傳解決了timeout的問題,但是沒解決重傳一個還是重傳多個的問題。出現難以決定是否重傳多個包問題的根源在於,傳送端不知道那些非連續序號的包已經到達接收端了,但是接收端是知道的,如果接收端告訴一下傳送端不就可以解決這個問題嗎?

於是,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的資料了,這樣傳送端就可以選擇重傳丟失的5500-6000,6500-7000,7500-8000的包:

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了,也就是出現先發的包卻後到了。


疑症9:TCP的流量控制


我們知道TCP的視窗(window)是一個16bit位欄位,它代表的是視窗的位元組容量,也就是TCP的標準視窗最大為2^16-1=65535個位元組。另外在TCP的選項欄位中還包含了一個TCP視窗擴大因子,option-kind為3,option-length為3個位元組,option-data取值範圍0-14。視窗擴大因子用來擴大TCP視窗,可把原來16bit的視窗,擴大為31bit。這個視窗是接收端告訴傳送端自己還有多少緩衝區可以接收資料。於是傳送端就可以根據這個接收端的處理能力來傳送資料,而不會導致接收端處理不過來。也就是,傳送端是根據接收端通知的視窗大小來調整自己的傳送速率的,以達到端到端的流量控制。儘管流量控制看起來簡單明瞭,就是傳送端根據接收端的限制來控制自己的傳送就好了。

但是細心的同學還是會有些疑問的:

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

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

  • 3)如果接收端處理能力很慢,這樣接收端的視窗很快被填滿,然後接收處理完幾個位元組,騰出幾個位元組的視窗後,通知傳送端,這個時候傳送端馬上就傳送幾個位元組給接收端嗎?傳送的話會不會太浪費了,就像一艘萬噸油輪只裝上幾斤的油就開去目的地一樣。對於傳送端產生資料的能力很弱也一樣,如果傳送端慢吞吞產生幾個位元組的資料要傳送,這個時候該不該立即傳送呢?還是累積多點在傳送?


1疑問1)的解決


傳送方要知道那些可以發,哪些不可以發,一個簡明的方案就是按照接收方的視窗通告,傳送方維護一個一樣大小的傳送視窗就可以了,在視窗內的可以發,視窗外的不可以發,視窗在傳送序列上不斷後移,這就是TCP中的滑動視窗。

如下圖所示,對於TCP傳送端其傳送快取內的資料都可以分為4類:

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

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

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

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


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

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

2疑問2)的解決


由問題1)我們知道,傳送端的傳送視窗是由接收端控制的。下圖,展示了一個傳送端是怎麼受接收端控制的:

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

如果3次過後還是0的話,有的TCP實現就會發RST掉這個連線。正如有人的地方就會有商機,那麼有等待的地方就很有可能出現DDoS攻擊點。攻擊者可以在和Server建立好連線後,就向Server通告一個0視窗,然後Server端就只能等待進行ZWP,於是攻擊者會併發大量的這樣的請求,把Server端的資源耗盡。

3疑問點3)的解決


疑點3)本質就是一個避免傳送大量小包的問題。造成這個問題原因有二:1)接收端一直在通知一個小的視窗; 2)傳送端本身問題,一直在傳送小包。這個問題,TCP中有個術語叫Silly Window Syndrome(糊塗視窗綜合症)。解決這個問題的思路有兩,1)接收端不通知小視窗,2)傳送端積累一下資料在傳送。

思路1)是在接收端解決這個問題,David D Clark’s 方案,如果收到的資料導致window size小於某個值,就ACK一個0視窗,這就阻止傳送端在發資料過來。等到接收端處理了一些資料後windows size 大於等於了MSS,或者buffer有一半為空,就可以通告一個非0視窗。思路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的時候就要留意這個問題了。


疑症10:TCP的擁塞控制


談到擁塞控制,就要先談談擁塞的因素和本質。本質上,網路上擁塞的原因就是大家都想獨享整個網路資源,對於TCP,端到端的流量控制必然會導致網路擁堵。這是因為TCP只看到對端的接收空間的大小,而無法知道鏈路上的容量,只要雙方的處理能力很強,那麼就可以以很大的速率發包,於是鏈路很快出現擁堵,進而引起大量的丟包,丟包又引發傳送端的重傳風暴,進一步加劇鏈路的擁塞。另外一個擁塞的因素是鏈路上的轉發節點,例如路由器,再好的路由器只要接入網路,總是會拉低網路的總頻寬,如果在路由器節點上出現處理瓶頸,那麼就很容易出現擁塞。由於TCP看不到網路的狀況,那麼擁塞控制是必須的並且需要採用試探性的方式來控制擁塞,於是擁塞控制要完成兩個任務:[1]公平性;[2]擁塞過後的恢復。

TCP發展到現在,擁塞控制方面的演算法很多,其中Reno是目前應用最廣泛且較為成熟的演算法,下面著重介紹一下Reno演算法(RFC5681)。介紹該演算法前,首先介紹一個概念duplicate acknowledgment(冗餘ACK、重複ACK)。

一般情況下一個ACK被稱為冗餘ACK,要同時滿足下面幾個條件(對於SACK,那麼根據SACK的一些資訊來進一步判斷):

  • [1] 接收ACK的那端已經發出了一些還沒被ACK的資料包

  • [2] 該ACK沒有捎帶data

  • [3] 該ACK的SYN和FIN位都是off的,也就是既不是SYN包的ACK也不是FIN包的ACK。

  • [4] 該ACK的確認號等於接收ACK那端已經收到的ACK的最大確認號

  • [5] 該ACK通知的視窗等接收該ACK的那端上一個收到的ACK的視窗。


Reno演算法包含4個部分:

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

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

  • [3]快速重傳 - Fast Retransimit;

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


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

1慢熱啟動演算法 – 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後採用了這篇論文的建議。

2擁塞避免演算法 – 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,這一般是路由器故障或者輕度擁塞或者其它不太嚴重的原因引起的,因此此時擁塞視窗縮小的幅度就不能太大,此時進入快速重傳。


3快速重傳 - Fast Retransimit



快速重傳做的事情有:

  • 1) 調整門限ssthresh的值為當前cwnd值的1/2;

  • 2) 將cwnd值設定為新的ssthresh的值;

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


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

4快速恢復演算法 – 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的傳送視窗有哪些穩定狀態呢?

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加速。相信大家學生時代在下載愛情動作片的時候也遇到過這種情況,明明外網速度沒這麼快的,自己下載的愛情動作片的速度卻達到幾M/s,那是因為,你的左後或右後的宿友在幫你加速中。我們都知道P2P模式下載會快,並且越多人下載就越快,那麼問題來了,P2P下載加速理論上的加速比是多少呢?

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


傳統的C/S模式傳輸檔案,在跑滿Client頻寬的情況下傳輸一個檔案需要耗時FS/BW,如果有n個客戶端需要下載檔案,那麼總耗時是n(FS/BW),當然啦,這並不一定是序列傳輸,可以並行來傳輸的,這樣總耗時也就是FS/BW了,但是這需要伺服器的頻寬是n個client頻寬的總和nBW。C/S模式一個明顯的缺點是服務要傳輸一個檔案n次,這樣對伺服器的效能和頻寬帶來比較大的壓力,我可以換下思路,伺服器將檔案傳給其中一個Client後,讓這些互聯的Client自己來互動那個檔案,那伺服器的壓力就減少很多了。這就是P2P網路的好處,P2P利用各個節點間的互聯,提倡“人人為我,我為人人”。

知道P2P傳輸的好處後,我們來談下理論上的最大加速比,為了簡化討論,一個簡單的網路拓撲圖如下,有4個相互互聯的節點,並且每個節點間的網路頻寬是BW,傳輸一個大小為FS的檔案最快的時間是多少呢?假設節點N1有個大小為FS的檔案需要傳輸給N2,N3,N4節點,一種簡單的方式就是:節點N1同時將檔案傳輸給節點N2,N3,N4耗時FS/BW,這樣大家都擁有檔案FS了。大家可以看出,整個過程只有節點1在傳送檔案,其他節點都是在接收,完全違反了P2P的“人人為我,我為人人”的宗旨。那怎麼才能讓大家都做出貢獻了呢?

解決方案是切割檔案:

  • [1]首先:節點N1 檔案分成3個片段FS2,FS3,FS4 ,接著將FS2傳送給N2,FS3傳送給N3,FS4傳送給N4,耗時FS/(3BW);

  • [2]然後:N2,N3,N4執行“人人為我,我為人人”的精神,將自己擁有的F2,F3,F4分別發給沒有的其他的節點,這樣耗時FS/(3BW)完成交換。


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

於是下面就開始進行加速分發:

  • [1]節點N1將分段FS21,FS31,FS41分別傳送給N2,N3,N4節點。耗時,FS/(3KBW)

  • [2]節點N1將分段FS22,FS32,FS42分別傳送給N2,N3,N4節點,同時節點N2,N3,N4將階段[1]收到的分段相互發給沒有的節點。耗時,FS/(3KBW)

  • 。。。。。。

  • [K]節點N1將分段FS2K,FS3K,FS4K分別傳送給N2,N3,N4節點,同時節點N2,N3,N4將階段[K-1]收到的分段相互發給沒有的節點。耗時,FS/(3KBW)

  • [K+1]節點N2,N3,N4將階段[K]收到的分段相互發給沒有的節點。耗時,FS/(3KBW)。


於是總的耗時為(K+1) (FS/(3KBW)) = FS/(3BW) + FS/(3KBW),當K趨於無窮大的時候,檔案進行無限細分的時候,耗時變成了FS/(3*BW),也就是當節點是n+1的時候,加速比是n。這就是理論上的最大加速比了,最大加速比是P2P網路節點個數減1。

附加題2:系統呼叫listen() 的backlog引數指的是什麼


要說明backlog引數的含義,首先需要說一下Linux的協議棧維護的TCP連線的兩個連線佇列:

  • [1]SYN半連線佇列;

  • [2]accept連線佇列。


[1] SYN半連線佇列:
Server端收到Client的SYN包並回復SYN,ACK包後,該連線的資訊就會被移到一個佇列,這個佇列就是SYN半連線佇列(此時TCP連線處於 非同步狀態 )。

[2] accept連線佇列:
Server端收到SYN,ACK包的ACK包後,就會將連線資訊從[1]中的佇列移到另外一個佇列,這個佇列就是accept連線佇列(這個時候TCP連線已經建立,三次握手完成了)。

使用者程式呼叫accept()系統呼叫後,該連線資訊就會從[2]中的佇列中移走。

相信不少同學就backlog的具體含義進行爭論過,有些認為backlog指的是[1]和[2]兩個佇列的和。而有些則認為是backlog指的是[2]的大小。其實,這兩個說法都對,在linux kernel 2.2之前backlog指的是[1]和[2]兩個佇列的和。而2.2以後,就指的是[2]的大小,那麼在kernel 2.2以後,[1]的大小怎麼確定的呢?兩個佇列的作用分別是什麼呢?

SYN半連線佇列的作用:
對於SYN半連線佇列的大小是由(/proc/sys/net/ipv4/tcp_max_syn_backlog)這個核心引數控制的,有些核心似乎也受listen的backlog引數影響,取得是兩個值的最小值。當這個佇列滿了,Server會丟棄新來的SYN包,而Client端在多次重發SYN包得不到響應而返回(connection time out)錯誤。但是,當Server端開啟了syncookies,那麼SYN半連線佇列就沒有邏輯上的最大值了,並且/proc/sys/net/ipv4/tcp_max_syn_backlog設定的值也會被忽略。

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

公眾號推薦:

相關文章