《TCP IP詳解卷1》12-17章TCP筆記

原文鏈接:https://www.qiujiawei.com/tcpip-1/

12章 TCP初步

  • tcp有差錯糾正。ip udp只有差錯檢測(CRC),出錯了就重發。
  • 差錯糾正一般是用差錯糾正碼,此外還有別的方法即ARQ協議(Automatic Repeat Request 自動重複請求)

12.1 入門介紹

12.1.1 ARQ和重傳

考慮多跳通信信道,有這些差錯種類:

  • 分組丟失
  • 比特差錯
  • 分組重新排序
  • 分組複製

最直接處理分組丟失、比特差錯(無法自動糾正的那種)的方法:重發分組直到正確接收

前提是發送方需要能夠判斷:

  • 接收方是否已收到分組
  • 接收方收到的分組是否與之前發送方發送的一樣

接收方因此需要給發送方一個信號來表明已接收到一個分組,這個方法就是ACK。但有幾個問題:

  • 發送方等待ACK應多長時間才重傳(簡單辦法,設爲RTT均值。具體看第14章)
  • ACK丟失怎麼辦(簡單辦法,等待超時後重傳)
  • 分組已接收,但有差錯怎麼辦(丟棄分組即可)

對於分組複製、分組亂序問題,解決方法則很簡單:每個分組加序列號。

目前爲止,上面這套簡單ACK機制,就可以實現可靠通信了,然而效率不高。

主要問題是停等問題:每次發送一個分組後都會停止繼續發分組並且等待這個分組的ACK,網絡的使用率很低。吞吐量約等於 M(分組大小)/R(RTT),若有丟包,只會更低。

解決停等問題關鍵是,允許往網絡注入更多的分組。但這又會引起大量的問題:

  • 發送方要決定什麼時間注入、注入多少個
  • 多分組時的定時器的維護變複雜
  • 要維持多個未ACK的分組副本以防重傳
  • ACK機制變複雜:ACK要支持區分,區分哪些分組已收到哪些沒有
  • 接收方要維護複雜的緩存機制,因爲分組會亂序到達或者部分丟失
  • 發送速率大於接收速率的問題
  • 中間路由器也有速率問題,一般遠低於兩端的速率

下面的機制就是爲了解決這些問題存在的。

12.1.2 分組窗口和窗口滑動

滑動窗口協議:

  • 窗口要分發送方和接收方(發送窗口和接收窗口),如果算上全雙工通訊,那麼有4個窗口
  • 窗口:窗口裏分組數量的大小限制了網絡利用率(吞吐量)
  • 窗口滑動:收到ACK時,窗口可能會向右滑動

12.1.3 流量控制和擁塞控制

流量控制:

  • 在接收方跟不上時會強迫發送方慢一來
  • 分類:
    • 基於速率的流控:給發送方指定某個速率,確保數據永遠不能超過這個速率發送
    • 基於窗口的流控:窗口大小不再固定,爲了讓接收方可以通知發送方應使用多大的窗口,出現了窗口更新概念。窗口更新信息是包含在ACK分組裏的,因此ACK使得窗口優化、同時窗口更新使得窗口變大變小。

擁塞控制:

  • 原因:中間網絡是低速網絡時,發送方速率可能超過某個路由器的能力,導致丟包
  • 分類:
    • 顯式發信:即上面說的窗口更新協議,由接收方顯式地告訴發送方正在發生什麼
    • 隱式發信:發送方根據其他證據減慢發送速率

12.2 TCP的引入

12.2.1 TCP服務模型

  • tcp是字節流模型
  • 沒有消息邊界
  • tcp不需要知道傳輸的字節流裏面是什麼東西

12.2.2 TCP中的可靠性

這一節正式談及TCP的可靠性,12.1講的只是一個概念,和真實的TCP區別很大。

組包(packetization):

  • 把字節流轉換成一組IP可以攜帶的TCP分組
  • TCP分組包含序列號,表示的是第一個字節相對於整個字節流的字節偏移
  • 序列號的機制使得分組在傳送中是可變大小的
  • TCP分組可以重新組合,稱爲重新組包(repacketization)
  • 應用程序數據被打散成TCP認爲的最佳大小的塊來發送

報文段:由TCP傳給IP的塊稱爲報文段

可靠性保障關鍵點:

  • 校驗和:校驗和覆蓋範圍是TCP、IP頭部+承載數據,但TCP校驗和可能會不夠強壯,所以最好要有自己的差錯保護機制
  • 重傳定時器:並不是每個報文段就對應一個定時器
  • ACK:可能會延遲發送;如果是雙向通訊,那麼ACK可能會由數據分組捎帶;ACK是累積的,收到N號ACK就表示<N的字節都成功接收了(而N是未接收!),累積性增強了ACK的robustness;

12.3 TCP頭部和封裝

tcp頭部:

 

 

頭部結構:

  • 2字節:源端口
  • 2字節:目的端口
  • 4字節:序列號seq
  • 4字節:ACK
  • 4位:頭部長度
  • 4位:保留
  • 8位:flags
  • 2字節:窗口大小
  • 2字節:校驗和
  • 2字節:緊急指針
  • 0~40字節:選項

關鍵詞:

  • 頭部長度範圍:至少20字節,帶選項的話最多60字節
  • 四元組:客戶端主機IP+socket端口號、服務端主機IP+socket端口號
  • Seq:序列號,32位無符號整數,到達232-1後回到0
  • ACK:確認號,確認號表示的是該確認號的發送方期待接收的下一個序列號
  • SACK:Seletive地ACK,要比普通ACK高效,因爲可以對次序雜亂的報文先ACK。前提是發送方有選擇重發能力
  • 頭部長度自表示:雖然只有4位,但是單位是word(32bits),所以可表示(24−1)⋅4(24−1)⋅4bytes = 60字節的頭部長度。
  • 窗口大小:佔2字節,單位是字節,所以最大窗口大小字節數爲216 - 1 = 65535字節
  • ISN:發送SYN的報文段裏的Seq就是ISN,隨機選定,第一個數據字節的Seq是ISN+1。因爲SYN消耗一個序列號,所以SYN是可靠傳輸的(FIN也是),而ACK不是
  • ISN是雙向的:雙方都要隨機一個ISN

要記住的:

  • ACK只有在flags的ACK位置1時纔有效,握手和斷開報文段之外的報文段都是置1的(待考證)
  • 默認窗口大小最大才65KB,需要用窗口縮放選項來放大,才能滿足高速和大延遲網絡
  • 通信速率SW/R (bits/s):S是分組比特總大小,W是窗口大小(分組數量),R是RTT
  • 不帶數據的包可能沒有可靠性保證,如Window Update ACK包

重要性能問題:重傳超時時間

  • 往返時間估計(RTT estimation):採樣最近的多個RTT並取平均
  • 超時並不能等於RTT均值,因爲可能有很多RTT是超過均值的,從而導致不斷超時重傳
  • 超時應略大於RTT均值,但也不能過大,否則網絡會變得空閒

13章 TCP連接管理

13.2 TCP連接的建立與終止

 

建立連接:

  • C發送ISN(c)(主動打開)
  • S收到ISN(c),發送ACK=ISN(c)+1 、 ISN(s) (被動打開)
  • C收到ACK和ISN(s)

斷開連接:

  • 一方發送FIN(主動關閉者),要發送當前序列號K,且帶有一個ACK(序列號L,代表最近一次收到的數據)
  • 另一方收到FIN後,發送ACK=k+1作爲響應(被動關閉者),同時通知上層應用程序
  • 另一方應用程序發FIN(轉變爲主動關閉者),序列號爲L
  • 一方收到FIN,發給另一方ACK確認

兩次半關閉:兩邊都要發FIN,才能變成完全關閉

半關閉:close、shutdown雙方都可以調用,close是全關閉,shutdown能實現半關閉

建立連接爲什麼要三次握手:

這個問題最好用逆向思維來思考。假設只做一次握手,那麼就是C發SYN給S後,就開始發數據了,根本不管S是否收到SYN甚至不管S是否存在,這肯定是不行的,都沒確定對方是否存在就發數據,既不能保證數據送到對方,也浪費了帶寬;接着假設只做二次握手,那麼就是C發SYN給S,S發ACK給C,S馬上就開始發送數據,這就有個問題,S發出的針對C的SYN的ACK,可能會丟失,導致客戶端沒收到SYN的ACK,就開始接收數據,因爲缺少ISN(s),所以C不能知道究竟有沒丟數據(並不能根據第一個收到的數據報文段來判斷)。

斷開連接爲什麼要四次握手:

因爲tcp是全雙工通信,斷開通信等於要關閉2個方向的數據流,即要做2次半關閉(2個FIN),一次半關閉就是2次握手。半關閉可能是自然發生的,例如客戶端發了FIN,服務端收到FIN時,可能還有一些數據在tcp發送緩衝區,於是只能發送ACK而不能發FIN,就進入了半關閉狀態;半關閉也可能是用戶用shutdown強制要求的,例如服務端收到FIN後,還是可以繼續往發送緩衝區繼續填入數據,無法知道服務端什麼時候會停止發送數據。綜上,半關閉的情況不可避免。不過有可能第二個FIN和第一個FIN的ACK是一起發送的(前提是本地的緩存隊列的數據都發送出去了不然不能發FIN),就變成了三次握手(本質上還是四次握手)。

13.2.3 初姑序列號ISN

  • 32位
  • 4微秒+1
  • 僞造包問題:只要四元組一樣,就能僞造發包。ISN需要設計成難以被猜出,方法是:隨機、散列、加密。

13.2.5 連接建立超時:

  • syn重發次數:cat /proc/sys/net/ipv4/tcp_syn_retries(或sysctl net.ipv4.tcp_syn_retries),一般等於6
  • synack重發次數:cat /proc/sys/net/ipv4/tcp_synack_retries,一般等於2
  • 指數回退:syn每次重發的間隔是上一次的一倍

13.2.6 連接與轉換器

NAT通過探查TCP的頭部來跟蹤連接的建立情況,主要就是通過flags的SYN ACK FIN位。NAT還可以修改報文段,但是有更復雜的問題。

13.3 TCP選項

 

題外話:tcp數據長度,是沒有在tcp頭顯式保存的,而是通過ip層的分組長度來算出,tcp數據長度 = ip分組長度 - tcp頭長度

MSS:

  • 最大段大小,記錄數據(不包括頭部)長度,佔2個字節。最小保證536字節(默認值),一般是1460字節
  • 最大段大小不是協商值,而是限定值,
  • MTU:最大傳輸單元。路徑MTU爲1500字節。ipv4:1460+40字節,ipv6:1440+60字節。
  • 65535的MSS是特殊值,ipv6網絡中超長數據報會用到。但實際MSS仍受路徑MTU限制,所以MSS值爲MTU-(40 IPv6頭+20 tcp頭)

(上面三個是最基本的)

SACK和SACK-Permitted:

  • 發SYN/SYNACK時就得發SACK-Permitted選項告訴對方支持SACK(雙向)
  • 當接收到亂序的數據時,可向發送方發送SACK選項
  • SACK選項長度爲8n+2,2個字節一個表示選項種類一個表示n,n等於SACK塊數量
  • SACK塊:已經成功接收的序列號範圍(序列號32位,需要start和end,所以總共要8個字節)
  • 最大n爲3,SACK最多佔8*3 + 2 = 26字節(因爲一般還有個時間戳)

WSOPT窗口縮放因子:

  • 字節:16bits
  • 範圍: 0-14,0表示沒有縮放。
  • 最大窗口大小:65535 x 2^14 ,約1GB。
  • tcp終端內部會維護這個真實的窗口大小
  • 主動打開者發WSCALE,被動打開者接收到WSCALE才能在SYNACK中發WSCALE
  • 如果主動打開者沒接收到被動打開者的WSCALE,就設爲0
  • 自己的爲S,發窗口大小給對方時,右移S位後將16數值填充到頭部
  • 對方的爲R,收到對方的窗口大小時,左移R位纔是真實大小
  • 縮放大小是根據接收緩存的大小自動選取的

TSOPT時間戳選項:

  • 作用1:估算RTT
  • 作用2:用來防止序列號迴繞導致的舊包無法去除問題
  • TSV:發送報文時放自己的當前時間戳 (Timestamp Value)
  • TSER:發ACK的時候把最新的TSV(TsRecent)複製進去(Timesatamp Echo Reply)
  • TsRecent並不是來自最新到達的報文段,而是來自最早的一個未經確認的報文段
  • RTT = current time - TSER

13.4 路徑MTU發現

  • 主要方案是:發大包,收到PTB(packet too big)消息後,調整自己MSS
  • 黑洞問題:防火牆導致無法收到PTB消息,無法知道是網絡不通還是包太大
  • 黑洞探測:報文重傳失敗多次後,嘗試改成較小報文段(分片)
  • 路由是動態變化的,所以每10分鐘要嘗試一個更大的MSS(接近初始的發送方MSS)
  • MSS大小影響吞吐量和窗口大小

13.5 TCP狀態轉換

 

 

TIME_WAIT:

  • MSL:最大段生存期,代表任何報文段在被丟棄前在網絡中被允許存在的最長時間
  • TIME_WAIT一般要持續2個MSL(60秒)(加倍等待)
  • 靜默時間:如果TIME_WAIT主機崩潰重啓,需要等待2MSL後才能建立新連接(但一般操作系統不會這樣做)
  • net.ipv4.tcp_fin_timeout:記錄了2MSL需要等待的超時時間(秒)(執行sysctl net.ipv4.tcp_fin_timeout輸出了60)
  • 對於服務器而言,如果當前有連接、且主動關閉了服務器,那麼正常是不能重新建立一個對同端口的監聽socket的,一樣要等2MSL,

TIME_WAIT作用:

  • 爲了可以重傳最終的ACK,或者叫可靠地實現TCP全雙工連接的終止。本質上是因爲對端會重傳FIN如果收不到ACK的話。重傳成功,服務器收到ACK,就可以從LAST_ACK進入CLOSED。但客戶端即使重傳成功也不能結束TIME_WAIT(或者說客戶端無法確定ACK是否重傳成功)。假如不能夠發ACK,那麼收到FIN時只能是發RST,這樣會導致服務器生成一個錯誤。
  • 讓老的重複報文可以在網絡中消失,即是說處於2MSL時,收到的數據報文都是要丟棄的。這也是爲了防止新連接錯誤接收舊報文引發數據異常。有的實現允許設一個大於所有舊的Seq的ISN,忽略2MSL狀態直接創建新連接,但有的實現是不允許,必須等到2MSL結束。

TIME_WAIT狀態其實不做什麼事情,就是限制1分鐘內這條連接的其中一端不能發起對另一端的新連接,這也是爲什麼只有一端進入TIME_WAIT而另一端可以直接CLOSED的原因,對端相信TIME_WAIT一端不會立即重新連接,

FIN_WAIT_2無限等待問題:

  • 主動關閉方發送fin並收到finack就進入了FIN_WAIT_2
  • 處於FIN_WAIT_2時需要等另一方發送fin(永久等待)
  • close系統調用會自動啓動定時器,60秒(net.ipv4.tcp_fin_timeout)後強制進入CLOSED狀態
  • 用shutdown的半關閉不會啓動定時器
  • FIN_WAIT_1不會無限等待,因爲不斷重發FIN直到收到ACK進入FIN_WAIT_2

同時打開同時關閉:

  • 是可以正確處理的,並且只會建立一條連接

13.6 RST重置報文段

當發現一個到達的報文段對於相關連接而言是不正確的時, TCP就會發送一個重置報文段。

重置報文段通常會導致TCP連接的快速拆卸。

發送時機:

  • 客戶端連接請求沒有對應的監聽socket時
  • 主動發RST強制終止連接時。發RST同時排隊報文會被丟棄。也被稱爲終止釋放。發FIN是有序釋放
  • 一端崩潰重啓,另一端就進入半開連接狀態。若收到半開連接(正常工作)的報文時,就會發RST。
  • TIME_WAIT錯誤時,TIME_WAIT端收到迷途的報文併發送ACK但被CLOSED的對端RST

最後一種情況TIME_WAIT錯誤是一種需要避免的錯誤,TIME_WAIT狀態需要忽略RST,否則就會過早地進入CLOSED。

強制終止方法:SO_LINGER設0(socket逗留選項),含義是,不會爲了確定數據是否到達另一端而逗留任何時間。

struct linger l;
l.l_onoff = 1;
l.l_linger = 0;
sock_setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &l, sizeof(l));

接收到的RST,必然要帶ACK,且序列號要在對端窗口範圍內,否則可能會遭到僞造RST攻擊。

發送RST,不需要一個RSTACK。對端收到RST就自然重置連接了(不過可能對端會丟失RST)。

UDP沒有RST,但是有ICMP目的地不可達消息。

13.7 TCP服務器選項

主要就是backlog。

13.8 攻擊

SYN flood

其實都寫在RFC裏了。

  • SYN cookies:把連接信息編碼到ISN,因爲第三次握手會發ISN+1給服務器,於是可以解碼出連接信息。不過tcp選項是編碼不進去的,需要關閉;另外SYN-ACK會無法重發,因爲服務器並不保存狀態。
  • SYN cache:把每個連接重要信息放到一個bucket(hashtable)(迷你TCB),cache entry可能比full TCB少好幾倍。就是從syn抽一些bits當做密鑰,然後和IP地址、port一起hash成一個哈希值,用這個哈希值來決定迷你TCB放在哈希表的哪裏。
  • 混合模式(SYN cookies + SYN cache):如果cache的哈希表滿了,就執行SYN cookies模式
  • 過濾:這個要依賴ISP,ISP過濾掉僞造了IP地址的包,抵禦SYN flood能力很強,然而這個機制並沒有稱爲ISP的標準。
  • increase backlog
  • 縮短SYN RECV 定時器
  • 頂掉最老的半開連接
  • 防火牆和代理:大意就是在客戶端和服務器之間架設一個代理機器,客戶端要先和代理機器三次握手成功後,纔會把三次握手“轉發”到服務器
  • monitor:監聽器能識別哪些IP地址是攻擊者,從而屏蔽掉來自攻擊者的syn。

超小MTU攻擊

攻擊者僞造一個ICMP PTB消息,消息包含一個非常小的MTU值(如68字節),就迫使受害者的TCP嘗試採用非常小的數據包來填充數據,大大降低了性能。禁用最大MTU發現功能就能實現這個攻擊。

序列號/劫持攻擊

就是對已建立的連接插入攻擊數據的攻擊。解決辦法應該是用校驗碼。

欺騙攻擊

例如僞造RST報文段:前提是序列號要在該連接的序列號範圍內

抵禦方式:認證每一個報文段(TCP-AO);要求RST報文段擁有一個特殊的序列號而不只是在範圍內的序列號、要求時間戳選項具有特定的數值。

14章 TCP超時和重傳

tcp_retries1:連接已經建立後,基本重傳次數。次數達到後,會先嚐試讓網絡層更新路由再繼續發包。一般爲3次.

tcp_retries2:連接已經建立後,最大重傳次數。次數達到後,會斷開連接。

重傳超時<RTT:對網絡引入不必要的重複數據

重傳超時>RTT:網絡利用率(吞吐量)下降

RTT是每個tcp連接獨立計算的。

超時重傳(RTO):在發送數據時設置一個定時器,若計時器超時仍未收到ACK,則會引發相應的超時/重傳操作。

快速重傳:若TCP累積確認無法返回新的ACK,或者當ACK包含的選擇確認信息(SACK)表明出現失序報文段時,快速重傳會推斷出現丟包。

RTO估算方法:

經典方法:

SRTT = a(SRTT) + (1-a)RTT,a一般爲0.8~0.9,稱爲指數加權移動平均/低通過濾器(EWMA)

RTO = min(ubound, max(lbound, (SRTT)b)),b爲時延離散因子,推薦值1.3 ~ 2.0

ubound:1分鐘,lbound:1秒。

適用於穩定RTT的網絡。

標準方法:

srtt = (1-g)srtt + (g)M rttvar = (1-h)rttvar + (h)(|M - srtt|) RTO = srtt + 4(rttvar)

M就是經典方法裏的RTT;srtt和SRTT等價;|M - srtt|爲平均偏差,所以rttvar爲平均偏差的EWMA。

g:新樣本M佔srtt估計值的權重,1/8

h:新平均偏差樣本|M-srtt|佔rttvar的權重,1/4

最終,RTO爲srtt加上4倍rttvar,4是一個研究出來的常量值。

當M變化時,|M-srtt|越大,rttvar也越大,RTO增長越快

時鐘粒度:

Linux計時器時鐘粒度爲1ms,設爲G。

RTO = srtt + max(G,4(rttvar))

Linux RTO默認RTO最小值200ms

收到第一個RTT測量值時再次初始化:

srtt = M

rttvar = M/2

RTO = srtt + 4(rttvar) = 3M

重傳二義性:

發生重傳時,若收到ACK,並不能知道是對第一次還是第二次發包的確認,所以無法計算RTT,需要跳過。(Karn算法第一部分)

若用了時間戳選項則可以處理二義性,因爲ACK包附帶了發包的時間戳,就可以知道ACK是對第一次還是第二次發包的確認。

退避係數(backoff factor):每當重傳計時器出現超時,退避係數加倍,直到接收到非重傳數據時重設爲1

基於定時器的重傳:

需要記錄被計時的報文段序列號,若及時收到該報文段的ACK,那麼計時器被取消。

沒丟包的話計時器不會超時。

若在設定的RTO內,沒收到ACK,就會觸發超時重傳。

此時需要降低發送速率:

  • 減小發送窗口大小
  • 重傳大於1次時,增大RTO退避因子,RTO = γRTO,γ = 1 2 4 8 ··· 。但RTO不會TCP_RTO_MAX
  • 一旦收到ACK,γ重置爲1

基於定時器的重傳不是好東西,會導致網絡利用率下降。

快速重傳

基於對端的反饋信息來引發重傳。(和基於計時器的重傳的本質區別)

更加及時。

tcp一般都實現了基於計時器的重傳和快速重傳。

重點:接收端收到失序報文段時,需要立即生成重複ACK立即發送,失序情況表明出現了丟段(接收緩存出現空洞)。

重複ACK:這個ACK可以表明是哪一個分組沒有收到。但因爲是用了tcp的seq段,所以一個RTT內只能填補一個空缺。

立即發送重複ACK是爲了讓發送端儘早得知有失序報文段,並告知空缺在哪。

當接收端收到當前期望序列號的後續分組時,當前期望的包可能丟失了,也可能是延遲到達,2者無法區分。

因此需要等待一定數目的重複ACK(重複ACK閾值 dupthresh,一般爲3,也有動態的),才能判斷數據是否丟失並快速重傳。

總之,發送端收到至少3個重複ACK後,馬上重傳可能丟失的分組,而不必等計時器超時。

帶選擇確認的重傳SACK

http://packetlife.net/blog/2010/jun/17/tcp-selective-acknowledgments-sack/

SACK是指已接收的失序報文段,並不是指丟失的報文段。

SACK會重複發,例如收到連續的2個報文段3和4,返回2個ACK,ACK都爲1(duplicate ACK),但第一個包帶了SACK=3,第二個包帶了SACK=3、4。發送端收到這2個ACK時,就會知道報文段2丟失,那麼重傳2。

一個tcp頭的SACK最多3塊。

收到SACK並重傳了包,也不能清空重傳緩存的該包(食言問題)。只有接收端發來的普通tcp ACK號大於發送端最大序列號值纔可清除。

RTT較大,丟包嚴重時,SACK的優勢就能體現出來。因爲一個RTT可以填補多個空缺很重要。

僞超時與重傳

GBN:連續發了n個報文段,如果網絡突然緩慢,最前面的那個超時了,會觸發重傳,此時後面的n-1個包也沒被確認,那麼n個報文段都重傳(回退)。

僞重傳:沒有丟包也發生了重傳,叫僞重傳。原因:網絡延遲、包失序、包重複、ACK丟失。

RTT增加,超過當前RTO時,就有可能出現僞超時(重傳)。

重要任務:檢測出僞重傳,幾種方法:D-SACK、F-RTO、Eifel檢測

D-SACK:重複SACK,一個操作系統的選項。

開啓後,可在第一個SACK塊中告知接收端收到的重複報文段序列號。

允許tcp一端開啓D-SACK而另一端沒有D-SACK,不需要對稱,沒有開啓D-SACK的一端不能使用D-SACK。

原理是,SACK接收端允許了包含seq<=累計ACK的SACK塊。

和通常的SACK的區別:DSACK只包含在單個ACK中,並且不會在多個SACK中重複。魯棒性比SACK低。

Eifel檢測算法:發生超時重傳時,Eifel算法等待接收下一個ACK,若爲針對第一次傳輸(即原始傳輸)的ACK(用時間戳判斷),則可以知道該重傳是僞重傳(利用TSOPT來檢測僞重傳)。

Forward RTO-recovery(路由轉發延遲導致的RTO的恢復機制,簡稱F-RTO):

  • 檢測僞重傳的標準算法
  • 不需要任何tcp選項
  • 是發送端自己的算法
  • 接收端不支持時間戳選項,也能工作
  • 只檢測由重傳計時器超時引發的僞重傳,別的無法判斷
  • F-RTO會修改TCP的行爲。其實就是GBN的問題。在超時重傳後收到第一個ACK時,改成發送新數據。等到下一個ACK到達時,如果2個ACK中至少有一個是重複ACK,則認爲此次重傳沒問題(確實發生了丟包,重複ACK指出了丟失的報文段)。如果2個都不是重複ACK,那麼就是僞重傳。

用上面的任意檢測算法檢測到僞重傳後,要應用Eifel響應算法:

說白了就是更新srtt和rttvar。因爲僞超時導致臨時修改了srtt和rttvar(RTO變了),檢測發現僞超時,那就得恢復到正常的srtt、rttvar。

  1. 計時器超時時,記錄srtt_prev = srtt+2(G)、rttvar_prev = rttvar。但直到發現有僞超時前,都不會使用它們。
  2. 執行某種僞超時檢測。
  3. 檢測到僞超時,設置僞恢復爲SPUR_TO(spurious timeout)
  4. 若僞恢復爲SPUR_TO,把下一個要發送的報文段的序列號改爲最新的未發送過的報文段。就可以避免GBN。
  5. 更新srtt、rttvar、RTO:srtt = max(srtt_prev,m),rttvar = max(rttvar_prev, m/2),RTO = srtt + max(G, 4(rttvar))。爲了拋棄RTT歷史值。另外,RTO更新方式不變。

包失序與包重複

包的問題有三種:丟失、失序、重複。tcp需要區分。

失序:

  • 如果發生在反向鏈路(ACK),發送端收到的ACK是亂序,那麼可能先收到後面的ACK,導致發送窗口快速前移(並且後面收到的ACK被丟棄)。快速前移會導致流量突發
  • 發生在正向鏈路,tcp可能無法正確識別失序or丟包。失序程度不大時,可以迅速得到處理;反之,tcp可能會誤認爲數據丟失,也就導致僞重傳(主要是指快速重傳)。

區分丟失和失序不是重要問題;互聯網中嚴重的失序並不常見,dupthresh設3就足夠了。

重複:

鏈路層重傳時會生成重複副本。會導致接收端生成一系列重複的ACK,觸發僞快速重傳。

但DSACK能處理這種情況,因爲重複ACK沒有包含失序信息,意味着ACK是重複數據。

目的度量

即操作系統在tcp斷開連接後依然緩存了該條路徑的rtt之類的信息。方便下次和該地址建立連接時,初始化srtt rttvar。

重新組包

當發生重傳,並不需要完全重傳相同的報文段,而可以重新組包,發送一個更大的報文段來提高性能。

重傳相關的攻擊

低速率DoS攻擊:

每次受害tcp重傳時,就發一堆數據給它並導致重傳超時,進而導致對方減小發送速率、退避發送,最終導致無法正常使用網絡帶寬。

預防方法是,隨機隨選RTO,使得攻擊者無法預知確切的重傳時間。

減慢受害tcp的發送並使RTT估計值過大(過分被動):

導致丟包後不會立即重傳。

僞造ACK使受害tcp的RTT估計值過小(過分積極):

導致過分發送,造成大量的無效傳輸。

15章 TCP數據流和窗口管理

Nagle算法:

算法要求:

  1. 當一個tcp連接中有在傳數據(已發送但未確認),小的報文段(長度小於SMSS)就不能被髮送,直到所有的在傳數據都收到ACK。
  2. 並且,在收到ACK後,tcp需要收集這些小數據,將其整合到一個報文段中發送。

特點:

  • 迫使tcp遵循停等規程(stop-and-wait)。發一個包就得等到收到ACK後再發下一個包,發包間隔等於RTT。
  • 因此每一時刻最多隻有一個包在傳
  • 因此減少了小包數,同時也增大了延遲。
  • 實現了自時鐘控制(self-clocking)。ACK返回越快,數據傳輸也越快。

在高延遲(擁塞)網絡中,需要減少報文數。算法使得單位時間內發送的報文段數目更少。

延時ACK和Nagle算法結合:

簡單來說就是會死鎖:

  • 服務端(Nagle)發一個包後開始等待ACK
  • 客戶端(延時ACK)收包後不馬上發送ACK而是等待一段時間再發

因爲客戶端最終會發出ACK,所以死鎖可解。但等待過程中,連接處於空閒狀態(明明有事忙),性能變差。

禁用Nagle:

  • 對socket設置TCP_NODELAY
  • 整個系統設置nodelay

流量控制和窗口管理

發送窗口:

  • 字節爲單位
  • SND.UNA,發送窗口左邊界
  • SND.WND,發送窗口大小
  • SND.UNA + SND.WND,發送窗口右邊界
  • SND.NXT,下個要發送的數據序列號
  • SND.UNA + SND.WND - SND.NXT,可用發送窗口

窗口運動術語:

  • close:左邊界右移(窗口縮小)。發生在已發送數據得到ACK確認。
  • open:右邊界右移(窗口變大)。可發送數據量變大。同樣發生在已發送數據得到ACK確認。
  • shrink:收縮,右邊界左移。

Note:左邊界不可能左移。

接收窗口:類似發送窗口。但窗口中間因爲SACK選項,可能會有被ACK的序列號(未確認的就是空洞)。但必須RCV.NXT接收了,窗口才能右移。

零窗口導致的問題:

左右邊界相等時,叫零窗口。此時發送端不能發數據。

接收端重新獲得可用空間時,會給發送端發送一個窗口更新(window update),當然還是用通告窗口。但這個發包不帶數據(因爲這是接收端,若發了數據就變成全雙工的發送端了),所以是一個典型的ACK消息。這個ACK是可能丟失的。

如果丟失了,就會進入死鎖狀態。發送端不知道接收端已經可以接收數據。

所以發送端會定時探測probe對方窗口,用來伺機增大發送窗口,接收端收到時必須返回ACK(包含了窗口大小字段)。

探測時機爲一個RTO超時後,然後指數間隔發送。

probe是帶有一個字節的數據的(用戶的數據),所以tcp會可靠傳輸它。不過如果接收端依然沒有可用緩存空間,就會丟掉這個包。

糊塗窗口綜合徵(silly window syndrome,SWS):

百度百科解釋:

糊塗窗口綜合症是指當發送端應用進程產生數據很慢、或接收端應用進程處理接收緩衝區數據很慢,或二者兼而有之;就會使應用進程間傳送的報文段很小,特別是有效載荷很小; 極端情況下,有效載荷可能只有1個字節;傳輸開銷有40字節(20字節的IP頭+20字節的TCP頭) 這種現象。

  • 接收端通告窗口較小
  • 發送端發送的數據段較小

解決方案:

  • 接收端:不應通告小的窗口值
  • 發送端:不應發送小的報文段

發送端需滿足以下條件才能發送:

  • 全長(MSS)的報文段
  • 數據段長度>=接收端通告過的最大窗口值的一半。(要記錄一個最大值,發送端纔可猜測接收緩存大小)
  • ACK不是目前期望的(或者說沒有未經確認的數據,那麼發送端立即發送新數據是合理的) 或 禁用了Nagle算法

第三點反過來說就是:如果啓用了Nagle或者有未確認的在傳數據,那麼不應該發送小包

大容量緩存與自動調優

  • 在相似環境下,使用較小接收緩存的tcp吞吐量會較差
  • 即使接收端指定了大容量緩存,但發送端指定了小緩存,性能還是差

這2個問題很關鍵,並且因爲第二個問題,很多tcp協議棧中應用層是不能指定緩存大小的。操作系統會自己定一個定值或弄成動態值。

窗口大小的自動調優:只能按類型選,沒有具體值,disabled、 highlyrestricted、 reStricted、 normal、experimental。

緩存大小的動態調整:通過估算髮送方的擁塞窗口的大小,來動態設置TCP接收緩存的大小。

緊急機制

雖然tcp的緊急機制調用用了一個叫MSG_OOB的flag,但是其實並不是帶外數據,這個緊急數據一樣是在用戶的數據流中傳輸,只是優先級更高。

發送的緊急數據只能是一個字節,並放進當前緩衝區末尾。因爲用了tcp的可靠傳輸,所以只需要插入一次。

因爲窗口大小原因,可能不能立即發出這個字節,但是tcp會知道已經進入緊急狀態,所有發出去的包都打開了URG標誌。

對接收端來說,收到URG的包頭,不等於該報文段裏就有緊急數據(還在收到)。要用緊急指針來判斷。在收到緊急數據前可能有多個包頭,包頭裏的緊急指針都一樣。

爲了讓用戶及時recv拿到緊急數據,需要用信號SIGURG的方式通知。SIGURG在第一次收到URG包頭時觸發一次。

帶外數據緩衝區:到達的緊急數據不能混在用戶數據緩衝區,所以另外用這個來存,等用戶來讀取。

send(sockfd, 'x', 1, MSG_OOB);

recv(sockfd, &ch, 1, MSG_OOB);

recv:在緊急狀態下,帶外數據仍未到達,函數返回EWOULDFBLOCK;非緊急狀態下,調用上述函數,返回EINVAL。

TCP擁塞控制

回顧:流量控制機制是基於通告窗口大小字段來實現,明確地告訴了發送端,接收端的緩存大小,避免了接收端緩存溢出。發送端降低了發送速率。

對路由器而言:超負荷時,降低發送速率或丟棄部分數據。原因是,即使路由器能緩存一部分數據,然後慢慢發出去,但源源不斷的數據到達,到達速率高於發出速率,任何容量都得溢出。(排隊理論!)

擁塞:路由器無法處理高速率到達的流量而被迫丟棄數據信息的現象

擁塞控制機制:是爲了緩解擁塞情況,tcp連接兩端都要進行

tcp擁塞檢測:

回顧:對於丟包,tcp採取首要機制是重傳:超時重傳和快速重傳。

但當網絡擁塞時,重傳會導致火上澆油。所以要避免這個情況。

當擁塞情況出現時的處理措施:

  • 減緩發送速率
  • 擁塞情況好轉時,檢測和使用新的可用帶寬

然而很難做到:因爲對於tcp發送方而言,沒有一個準確的方法去知曉路由器的狀態。

只能用一些信息來推斷:

  • 出現丟包
  • 時延測量
  • 顯式擁塞通知(ECN)

檢測出擁塞後,就是對擁塞的處理。其實就是when減速和how減速、how恢復速率。

減緩TcP發送

擁塞窗口:反映網絡傳輸能力的變量,cwnd

接收端通知窗口:awnd

發送端實際可用窗口:W = min(cwnd, awnd)

在外數據大小(flight size):發送端發送的數據中,未收到ACK的數據不能多於W(字節)。

cwnd無法拿到準確值:缺乏顯示擁塞的信號

W、cwnd、awnd需要根據經驗設定並動態調節。

所以,W的值不能過大或過小,應接近BDP(bandwidth-delay prodcut),帶寬延遲積,也稱作最佳窗口大小(optimal window size)。

W反映網絡中可存儲的待發送數據量大小。

實際計算方法:W = RTT * 鏈路中最小通行速率

W越接近BDP,網絡資源利用得越高效。

確定BDP是難點。

經典方法

同時只運行一個,可以互相切換。

基於包守恆。

  1. 建立tcp時執行慢啓動
  2. 直至有丟包時,執行擁塞避免算法

慢啓動

原因:由於未知網絡傳輸能力,需要緩慢探測可用傳輸資源,防止短時間內大量數據注入導致擁塞。

作用:

  1. 使TCP在用擁塞避免探尋更多可用帶寬之前得到cwnd值
  2. 幫助tcp建立ack時鐘

時機:

  • 新連接
  • 檢測到RTO導致的丟包時
  • 長時間處於空閒狀態

原理:

SMSS: 發送方的最大段大小 = min(接收方MSS,路徑MTU)

IW:初始窗口,一開始發送的數據段大小(SYN交換後)

  • IW = 2 * SMSS,if SMSS > 2190
  • IW = 3 * SMSS,if 2190 >= SMS > 1095
  • IW = 4 * SMSS,else

初始cwnd = IW = 1 * SMSS(簡單起見,設1)

每次收到ACK,cwnd會慢慢增加:cwnd += min(N, SMSS)

N:未ACK的數據,通過這一”好的ACK“能確認的數據大小

好的ACK:新收到ACK大於之前收到的ACK

這樣設計是因爲:如果每次收到ACK都直接+=SMSS,可能會遭到“ACK分裂”攻擊,通過發送小ACK導致發送方加速發送。

如果N大於SMSS,說明正在發送大量數據,那麼就只+=SMSS,cwnd = 2 * SMSS.

此時就可以發送2個數據段,如果繼續接收ACK成功(2個ACK),則2變4、4變8,指數增加。

k:k輪後,W = 2k,k = log2(W),需要k個RTT時間,窗口才能達到W。

指數增長看似快,但還是比一開始就以最大速率(接收方最大窗口)慢(W不會超過awnd)。

另外,如果接收端開啓了ACK時延,接收端就不會發2個ACK,而是合併1個,那麼增速變得更慢。

非交互式tcp流:其實就是指單方發送大數據的情況,不是短消息的一問一答(http)。

對於非交互式tcp流來說,delayed ACK是不好的,此時接收端是不會發數據的,所以沒可能在數據包裏帶上ACK。此時可以使用Quick ACK機制。

通過QuickACK,接收端recv後可以立即發送ACK,沒有delay。

但tcp本身很難知道是不是非交互式的流。可以這樣做:

  • 新tcp連接開啓quick ACK,直到檢測到該tcp有交互式特徵時關閉。(Linux默認如此)
  • 新tcp連接關閉quick ACK,當連接不是交互式時,開啓。

http://www.jauu.net/2010/10/02/tcp-quick-ack-versus-packet-overhead/

擁塞避免

當指數增長到一定程度,就會開始丟包。

此時設置一個慢啓動閾值(ssthresh),公式下面說;當前擁塞窗口大小(cwnd)減到一半(不一定,只是經典做法)。

只留一半是避免佔滿全部帶寬,導致路由器其他連接丟包。

擁塞避免:

當確立了慢啓動閾值,tcp就進入擁塞避免階段。cwnd不再翻倍,而是線性增大。這就可以得到更多的傳輸資源而不至於影響其他連接。

cwndt+1=cwndt+SMSS∗SMSS/cwndtcwndt+1=cwndt+SMSS∗SMSS/cwndt

其實就是每次收到ACK就增加(1/k)倍 (次線性)

慢啓動和擁塞避免的選擇

慢啓動和擁塞避免之間的區別:當新的ACK到達時,cwnd怎樣增長。

  • 當cwnd < ssthresh:慢啓動
  • 當cwnd > ssthresh:擁塞避免

ssthresh在整個連接中不是保持不變的。沒有丟包時,記住上一次最好的窗口估計值。有丟包時,按下面公式更新:

ssthresh=max (cwnd/2, 2*SMSS)

Tahoe、 Reno以及快速恢復算法

Tahoe:有丟包時,cwnd直接變1,重新慢啓動。會導致帶寬利用率低下。

解決辦法:

  • 超時丟包:cwnd = 1
  • 重複ACK引起的丟包:cwnd = 上一個ssthresh(快速重傳就會有這種丟包)

Reno快速恢復:

因爲丟包進入的慢啓動階段,可以快速恢復,方法是每接收到一個ACK(重複ACK),cwnd就增長1 SMSS(急速),直到接收到一個”好的ACK“。

標準TCP

可以小結出一套標準算法:

在TCP連接建立之初首先是慢啓動階段(cwnd = IW), ssthresh通常取一較大值(至少爲awnd)。當接收到一個好的ACK (表明新的數據傳輸成功), cwnd會相應更新。

  • cwnd += SMSS (若cwnd<ssthresh)慢啓動
  • cwnd += SMSS*SMSS/cwnd (若cwnd>ssthresh)擁塞避免

當收到三次重複ACK (或其他表明需要快速重傳的信號)肘,會執行以下行爲:

  1. ssthresh更新爲大於等式 ssthresh = max(在外數據值/2, 2*SMSS) 中的值
  2. 啓用快速重傳算法,將cwnd設爲(ssthresh + 3*SMSS)
  3. 每接收一個重複ACK, CWnd值暫時增加1 SMSS
  4. 當接收到一個好的ACK,將cwnd重設爲ssthresh(收縮)

以上第2步和第3步構成了快速恢復。

對標準算法的改進

NewReno:

因爲Reno需要收到重複ACK才能快速恢復,但如果先收到了好的ACK(局部ACK)(但還有別的包已發送未確認),導致了窗口膨脹過早結束,此時傳輸通道就會很空閒。且如果重複ACK不足三個(網絡中沒有足夠的數據表在傳輸就會這樣),不會進入快速重傳,而確實又有丟包,那麼最終就是RTO超時,進行超時重傳,並慢啓動。

Reno記錄了一個最高序列號來解決。效果就是避免過早結束膨脹。

(其他改進之後再學習)

共享擁塞狀態信息

其實就是操作系統的本地優化。新連接可以利用舊連接的信息,來優化。

net.ipv4.tcp_no_metrics_save = 0,默認開啓

每個tcp關閉前,保存信息:srtt、rttvar、重排估計值、cwnd、ssthresh

TCP友好性

就是多條tcp連接對資源的競爭問題。有一條很複雜的公式。篇幅很短,應該不是重點。

高速環境下的TCP

基於延遲的擁塞控制算法

緩衝區膨脹

網絡中的路由設備,其緩衝區的大小不是越大越好,過大的緩衝區反而會導致網絡擁塞。

緩衝區過小:

很容易就被寫滿,丟包率變高,導致傳輸效率差

緩衝區過大:

如果路由器接收速率大於發送速率,就會有大量數據在路由器排隊,延遲很大,此時還不算是丟包。丟包要等到發送端超時纔算,然後又往路由器塞入了重複報文段。

擁塞信號反饋過慢。

https://blog.csdn.net/zerooffdate/article/details/77688460

積極隊列管理和ECN:

一般,路由器沒有義務把擁塞信息發給tcp發送端。就不利於擁塞控制。

路由器默認只有FIFO和尾部丟棄機制,先到的包會發出去,後到的包如果塞不下了,就丟棄。

應用FIFO和尾部丟棄以外的調度算法和緩存管理策略被認爲是積極的。

AQM:積極隊列管理

ECN:非常依賴路由器、交換機的擁塞通知機制。

路由器會給發送中的報文打上ECN標識,報文送到接收端後,接收端再通過ACK包告訴發送端ECN。發送端就可以降低發送速率。

與TCP擁塞控制相關的攻擊

  1. ACK分裂攻擊:拆分ACK,從而讓發送端的cwnd加速增長。解決辦法:前面說到了。
  2. 重複ACK攻擊:就是本來沒重複ACK,強行發重複的,也是導致發送端在快速恢復階段增長得更快。解決辦法:限制發送端在恢復階段的在外數據值
  3. 樂觀響應攻擊:對還沒有收到的報文段產生ACK。導致發送端計算出來的RTT變小,發送端就會比正常情況下發得更快。解決辦法:發送報文大小加一點隨機,使得接收端難以猜出。

TCP保括機制

這一章很短,不是很重點。

keepalive是一種不怎麼納入標準的技術。因爲keepalive是有問題的:對於一個長連接,如果發送保活探測的時候,剛好這段時間中間網絡出了點問題(例如路由器重啓),就會導致好的連接被中斷掉。

保活功能是爲服務器提供的。服務器需要知道客戶端是否崩潰或離開,才能釋放連接資源。

一般,長時間交互式服務就不期望保活功能(ssh);短時間非交互式服務就期望保活功能(Web服務器)。

默認關閉。

非對稱,兩端都可以各自做keepalive。

組成:

  • keepalive time:保活時間,即發送保活探測的計時器的timeout時間。一般爲無數據傳輸後2小時。
  • keepalive interval:第一個探測發送後,如果沒收到回包,就需要緊跟着再次發送探測。一般爲75秒。
  • keepalive probe:探測多少次後才認爲對端不可達,中斷連接。

探測報文:

一個空報文段或帶一個字節(垃圾數據)的報文段。序列號等於對端發送的ACK最大序列號減1,因爲這個序列號已經被成功接收,所以不會對對端造成影響。

探測報文返回的響應可以確定連接是否在工作。

響應報文也是不帶數據或者只帶垃圾數據。

2種報文丟失了都不會重傳。(所以需要重複探測)

探測前後,對端可能的四種狀態:

  1. 對端正常工作,正常響應了探測。探測定時器重置。
  2. 對端已崩潰或重啓中,此時不會響應探測。探測端會一直探測直到超過探測次數,就認爲對端已經關閉,斷開這個連接。
  3. 對端已崩潰但已重啓,會發送RST重置報文段作爲響應。探測端會斷開連接。
  4. 對端正常工作,但因其他原因探測報文不能到達。4和2相同,因爲tcp無法區分開。

Note:對端正常關閉然後重啓,是沒問題的。對端會發送FIN,正常斷開連接。

欺騙攻擊:因爲保活報文不包含用戶數據,加密強度有限。容易僞造。導致連接一直保持,浪費服務端資源。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章