淺談TCP(1):狀態機與重傳機制

TCP協議比較複雜,接下來分兩篇文章淺要介紹TCP中的一些要點。html

本文介紹TCP的狀態機與重傳機制,下文講解流量控制與擁塞控制。git

本文大部份內容基於TCP 的那些事兒(上)修改而來,部分觀點與原文不一樣,重要地方增長了解釋。github

前置知識

一些網絡基礎

TCP在網絡OSI的七層模型中的第四層——Transport層,IP在第三層——Network層,ARP在第二層——Data Link層,在第二層上的數據,咱們叫Frame,在第三層上的數據叫Packet,第四層的數據叫Segment。算法

應用層的數據首先會打到TCP的Segment中,而後TCP的Segment會打到IP的Packet中,而後再打到以太網Ethernet的Frame中,傳到對端後,各個層解析本身的協議,而後把數據交給更高層的協議處理。shell

TCP頭格式

在正式討論以前,先來看一下TCP頭的格式: [圖片上傳中...(image.png-eed30f-1522722733998-0)]瀏覽器

image.png

注意:安全

  • TCP的包是沒有IP地址的,那是IP層上的事。可是有源端口和目標端口。
  • 一個TCP鏈接須要四個元組來表示是同一個鏈接(src_ip, src_port, dst_ip, dst_port)(準確說是五元組,還有一個是協議,但由於這裏只是說TCP協議,因此,這裏我只說四元組)。
  • 注意上圖中的四個很是重要的東西:
    • Sequence Number,包的序號Seq,用於解決網絡包亂序(reordering)。
    • Acknowledgement Number,Ack用於確認收到Seq(Ack = Seq + 1,表示收到了Seq及Seq以前的數據包,期待Seq + 1),用於解決丟包
    • Window,又叫Advertised Window,能夠近似理解爲滑動窗口(Sliding Window)的大小,用於流控
    • TCP Flag ,區分包的類型,如SYN包、FIN包、RST包等,主要_用於操控TCP狀態機_。

其餘字段參考下圖:服務器

image.png

TCP的狀態機

其實,網絡傳輸是沒有鏈接的——TCP所謂的「鏈接」,其實只不過是在通信的雙方維護一個「鏈接狀態」,讓它看上去好像有鏈接同樣。因此,TCP的狀態轉換很是重要。cookie

下面是簡化的「TCP協議狀態機」 和 「TCP三次握手建鏈接 + 傳數據 + 四次揮手斷鏈接」 的對照圖,兩張圖本質上都描述了TCP協議狀態機,但場景略有不一樣。這兩個圖很是重要,必定要記牢網絡

TCP協議狀態機,不區分client、server:

image.png

下圖是經典的「TCP三次握手建鏈接 + 傳數據 + 四次揮手斷鏈接」,client發起握手,向server傳輸數據(server不向client傳),最後發起揮手:

image.png

三次握手與四次揮手

不少人會問,爲何建鏈接要三次握手,斷鏈接須要四次揮手?

三次握手建鏈接

主要是要_初始化Sequence Number 的初始值_。

通訊的雙方要同步對方ISN(初始化序列號,Inital Sequence Number)——因此叫SYN(全稱Synchronize Sequence Numbers)。也就是上圖中的 x 和 y。這個號在之後的數據通訊中,在client端按發送順序遞增,在server端按遞增順序從新組織,以保證應用層接收到的數據不會由於網絡問題亂序。

四次揮手斷鏈接

實際上是_雙方各自進行2次揮手_。

由於TCP是全雙工的,client與server都佔用各自的資源發送segment(同一通道,同時雙向傳輸seq和ack),因此,雙方都須要關閉本身的資源(向對方發送FIN)並確認對方資源已關閉(回覆對方Ack);而雙方能夠同時主動關閉,也能夠由一方主動關閉帶動另外一方被動關閉。只不過,一般以一方主動另外一方被動舉例(如圖,client主動server被動),因此看上去是所謂的4次揮手。

若是兩邊同時主動斷鏈接,那麼雙方都會進入CLOSING狀態,而後到達TIME_WAIT狀態,最後超時轉到CLOSED狀態。下圖是雙方同時主動斷鏈接的示意圖(對應TCP狀態機中的Simultaneous Close分支):

image.png

握手過程當中的其餘問題

建鏈接時SYN超時

server收到client發的SYN並回復Ack(SYN)(此處稱爲Ack1)後,若是client掉線了(或網絡超時),那麼server將沒法收到client回覆的Ack(Ack(SYN))(此處稱爲Ack2),鏈接處於一個中間狀態(非成功非失敗)。

爲了解決中間狀態的問題,server若是在必定時間內沒有收到Ack2,會重發Ack1(不一樣於數據傳輸過程當中的重傳機制)。Linux下,默認重試5次,加上第一次最多共發送6次;重試間隔從1s開始翻倍增加(一種指數回退策略,Exponential Backoff),5次的重試時間分別爲1s, 2s, 4s, 8s, 16s,第5次發出後還要等待32s才能判斷第5次也超時。因此,至多共發送6次,通過1s + 2s + 4s+ 8s+ 16s + 32s = 2^6 -1 = 63s,TCP纔會認爲SYN超時斷開這個鏈接

SYN Flood攻擊

能夠利用建鏈接時的SYN超時機制發起SYN Flood攻擊——給server發一個SYN就當即下線,因而服務器默認須要佔用資源63s纔會斷開鏈接。發SYN的速度是很快的,這樣,攻擊者很容易將server的SYN隊列資源耗盡,使server沒法處理正常的新鏈接。

針對該問題,Linux提供了一個tcp_syncookies參數解決這個問題——當SYN隊列滿了後,TCP會經過源地址端口、目標地址端口和時間戳構造一個特別的Sequence Number發回去,稱爲SYN Cookie,若是是攻擊者則不會有響應,若是是正常鏈接,則會把這個SYN Cookie發回來,而後server端能夠經過SYN Cookie建鏈接(即便你不在SYN隊列中)。至於SYN隊列中的鏈接,則不作處理直至超時關閉。請注意,不要用tcp_syncookies參數來處理正常的大負載鏈接狀況,由於SYN Cookie本質上也破壞了建鏈接的SYN超時機制,是妥協版的TCP協議。

對於正常的鏈接請求,有另外三個參數可供選擇:

  • tcp_synack_retries參數設置SYN超時重試次數
  • tcp_max_syn_backlog參數設置最大SYN鏈接數(SYN隊列容量)
  • tcp_abort_on_overflow參數使SYN請求處理不過來的時候拒絕鏈接

ISN的同步

  • 首先,不能選擇靜態的ISN。例如,若是鏈接建好後始終用1來作ISN,若是client發了30個segment(假設一個字節一個segment)過去,可是網絡斷了,因而 client重連,又用了1作ISN,可是舊鏈接的那些segment(稱爲「迷途的重複分組」)到了,因爲區分鏈接的五元組相同(稱該新鏈接爲舊鏈接的「化身」),server會把它們當作新鏈接中的segment。
  • 而後,從上例還可以得知,須要使ISN隨時鐘動態增加,以保證新鏈接的ISN大於舊鏈接。
  • 最後,從安全等角度考慮,也不能使ISN的增加呈現規律性(如簡單隨時鐘正比例增加)。這很容易理解,若是增加規律過於簡單,則很容僞造ISN對網絡兩端發起攻擊。

最終,設計了多種ISN增加算法,廣泛_使ISN隨時鐘動態增加,並具備必定的隨機性_。RFC793中描述了一種簡單的ISN增加算法:ISN會和一個假的時鐘綁在一塊兒,這個時鐘會在每4微秒對ISN作加一操做,直到超過2^32,又從0開始。這樣,一個ISN的週期大約是4.55(我算的4.77???)個小時。定義segment在網絡上的最大存活時間爲MSL(Maximum Segment Lifetime),網絡中存活時間超過MSL的分組將被丟棄。所以,若是使用RFC793中的ISN增加算法,則MSL的值必須小於4.55小時,以保證不會在相鄰的鏈接中重用ISN(TIME_WAIT也有該做用)。同時,這間接限制了網絡的大小(固然,4.55小時的MSL已經能構造很是大的網絡了)。

MSL應大於IP協議TTL換算的時間,RFC793建議MSL設置爲2分鐘,Linux遵循伯克利習慣設置爲30s。

揮手過程當中的其餘問題

關於TIME_WAIT

爲何須要TIME_WAIT

在TCP狀態機中,從TIME_WAIT狀態到CLOSED狀態,有一個超時時間 2 * MSL。爲何須要TIME_WAIT狀態,且超時時間爲2 * MSL?主要有兩個緣由:

  • 2 * MSL確保有足夠的時間讓被動方收到了ACK或主動方收到了被動發超時重傳的FIN。即,若是被動方沒有收到Ack,就會觸發被動方重傳FIN,發送Ack+接收FIN正好2個MSL,TIME_WAIT狀態的鏈接收到重傳的FIN後,重傳Ack,再等待2 * MSL時間。
  • 確保有足夠的時間讓「迷途的重複分組」過時丟棄。這隻須要1 * MSL便可,超過MSL的分組將被丟棄,不然很容易同新鏈接的數據混在一塊兒(僅僅依靠ISN是不行的)。

大規模出現TIME_WAIT

一個常見問題是大規模出現TIME_WAIT,一般是在高併發短鏈接的場景中,會消耗不少資源。

網上大部分文章都是教你打開兩個參數,tcp_tw_reusetcp_tw_recycle。這兩個參數默認都是關閉的,tcp_tw_recycletcp_tw_reuse更爲激進;要想使用兩者,還須要打開tcp_timestamps(默認打開),不然無效。不過,打開這兩個參數可能會讓TCP鏈接出現詭異的問題:如上所述,若是不等待超時就重用鏈接的話,新舊鏈接的數據可能會混在一塊兒,好比新鏈接握手期間收到了舊鏈接的FIN,則新鏈接會被重置。所以,使用這兩個參數時應格外當心

各參數詳細以下:

  • tcp_tw_reuse:官方文檔上說tcp_tw_reuse加上tcp_timestamps能夠保證客戶端(僅客戶端)在協議角度的安全,可是須要在兩端都打開tcp_timestamps
  • tcp_tw_recycle:若是是tcp_tw_recycle被打開了話,會假設對端開啓了tcp_timestamps,而後會去比較時間戳,若是時間戳變大了,就能夠重用鏈接(NAT網絡有可能建鏈接失敗,出現"connection time out"的錯誤)。

補充一個參數:

  • tcp_max_tw_buckets:控制併發的TIME_WAIT的數量(默認180000),若是超限,系統會把多餘的TIME_WAIT鏈接destory掉,而後在日誌裏打一個警告(如「time wait bucket table overflow」)。官網文檔說這個參數是用來對抗DDoS攻擊的,須要根據實際狀況考慮。

關於TIME_WAIT的建議

總之,TIME_WAIT出如今主動發起揮手的一方,即,誰發起揮手誰就要犧牲資源維護那些等待從TIME_WAIT轉換到CLOSED狀態的鏈接。TIME_WAIT的存在是必要的,所以,與其經過上述參數破協議來逃避TIME_WAIT,不如好好優化業務(如改用長鏈接等),針對不一樣業務優化TIME_WAIT問題。

對於HTTP服務器,能夠設置HTTP的KeepAlive參數,在應用層重用TCP鏈接來處理多個HTTP請求(須要瀏覽器配合),讓client端(即瀏覽器)發起揮手,這樣TIME_WAIT只會出如今client端。

示例

下圖是我從Wireshark中截了個我在訪問coolshell.cn時的有數據傳輸的圖,能夠參照理解Seq與Ack是怎麼變的(使用Wireshark菜單中的Statistics ->Flow Graph… ):

image.png

能夠看到,Seq與Ack的增長和傳輸的字節數相關。上圖中,三次握手後,來了兩個Len:1440的包,所以第一個包爲Seq(1),第二個包爲Seq(1441)。而後收到第一個Ack(1441),表示1~1440的數據已經收到了,期待Seq(1441)。另外,能夠看到一個包能夠同時充當Ack與Seq,在一次傳輸中攜帶數據與響應。

若是你用Wireshark抓包程序看3次握手,你會發現ISN老是爲0。不是這樣的,Wireshark爲了顯示更友好,使用了Relative Seq——相對序號。你只要在右鍵菜單中的protocol preference中取消掉就能夠看到「Absolute Seq」了。

TCP重傳機制

TCP協議經過重傳機制保證全部的segment均可以到達對端,經過滑動窗口容許必定程度的亂序和丟包(滑動窗口還具備流量控制等做用,暫不討論)。注意,此處重傳機制特指數據傳輸階段,握手、揮手階段的傳輸機制與此不一樣。

TCP是面向字節流的,Seq與Ack的增加均以字節爲單位。在最樸素的實現中,爲了減小網絡傳輸,接收端只回復最後一個連續包的Ack,並相應移動窗口。好比,發送端發送1,2,3,4,5一共五份數據(假設一份數據一個字節),接收端快速收到了Seq 1, Seq 2,因而回Ack 3,並移動窗口;而後收到了Seq 4,因爲在此以前未收到過Seq 3(亂序),若是仍在窗口內,則只填充窗口,但不發送Ack 5,不然丟棄Seq 3(與丟包的效果類似);假設在窗口內,則等之後收到Seq 3時,發現Seq 4及之前的數據包都收到了,則回Ack 5,並移動窗口。

超時重傳機制

當發送方發現等待Seq 3的Ack(即Ack 4)超時後,會認爲Seq 3發送「失敗」,重傳Seq 3。一旦接收方收到Seq 3,會當即回Ack 4。

發送方沒法區分是Seq 3丟包、接收方故障、仍是Ack 4丟包,本文統一表述爲Seq發送「失敗」。

這種方式有些問題:假設目前已收到了Seq 4;因爲未收到Seq 3,致使發送方重傳Seq 3,在收到重傳的Seq 3以前,包括新收到的Seq 5和剛纔收到的Seq 4都不能回覆Ack,很容易引起發送方重傳Seq 四、Seq5。接收方以前已經將Seq 四、Seq 5保存到窗口中,此時重傳Seq 四、Seq 5明顯形成浪費。

也就是說,超時重傳機制面臨「重傳一個仍是重傳全部」的問題,即:

  • 重傳一個:僅重傳timeout的包(即Seq 3),後續包等超時後再重傳。節省資源,但效率略低。
  • 重傳全部:每次都重傳timeout包及以後全部的數據(即Seq 三、四、5)。效率更高(若是帶寬未打滿),但浪費資源。

可知,兩種方法都屬於超時重傳機制,各有利弊,但兩者都須要等待timeout,是基於時間驅動的,性能與timeout的長度密切相關。若是timeout很長(廣泛狀況),則兩種方法的性能都會受到較大影響。

快速重傳機制

最理想的方案是:在超時以前,經過某種機制要求發送方儘快重傳timeout的包(即Seq 3),如快速重傳機制(Fast Retransmit)。這種方案浪費資源(浪費多少取決於「重傳一個仍是重傳全部」,見下),但效率很是高(由於不須要等待timeout了)。

快速重傳機制不基於時間驅動,而基於數據驅動若是包沒有連續到達,就Ack最後那個可能被丟了的包;若是發送方連續收到3次相同的Ack,就重傳對應的Seq

好比:假設發送方仍然發送1,2,3,4,5共5份數據;接收方先收到Seq 1,回Ack 2;而後Seq 2因網絡緣由丟失了,正常收到Seq 3,繼續回Ack 2;後面Seq 4和Seq 5都到了,最後一個可能被丟了的包仍是Seq 2,繼續回Ack 2;如今,發送方已經連續收到4次(大於等於3次)相同的Ack(即Ack 2),知道最大序號的未收到包是Seq 2,因而重傳Seq 2,並清空Ack 2的計數器;最後,接收方收到了Seq 2,查看窗口發現Seq 三、四、5都收到了,回Ack 6。示意圖以下:

image.png

快速重傳解決了timeout的問題,但依然面臨「重傳一個仍是重傳全部」的問題。對於上面的示例來講,是隻重傳Seq 2呢仍是重傳Seq 二、三、四、5呢?

若是隻使用快速重傳,則必須重傳全部:由於發送方並不清楚上述連續的4次Ack 2是由於哪些Seq傳回來的。假設發送方發出了Seq 1到Seq 20供20份數據,只有Seq 一、六、十、20到達了接收方,觸發重傳Ack 2;而後發送方重傳Seq 2,接收方收到,回覆Ack 3;接下來,發送方與接收方都不會再發送任何數據,兩端陷入等待。所以,發送方只能選擇「重傳全部」,這也是某些TCP協議的實際實現,對於帶寬未滿時重傳效率的提高很是明顯。

一個更完美的設計是:將超時重傳與快速重傳結合起來,觸發快速重傳時,只重傳局部的一小段Seq(局部性原理,甚至只重傳一個Seq),其餘Seq超時後重傳


參考:


本文連接:淺談TCP(1):狀態機與重傳機制
做者:猴子007
出處:monkeysayhi.github.io
本文基於 知識共享署名-相同方式共享 4.0 國際許可協議發佈,歡迎轉載,演繹或用於商業目的,可是必須保留本文的署名及連接。

相關文章
相關標籤/搜索