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。
- 計時器超時時,記錄srtt_prev = srtt+2(G)、rttvar_prev = rttvar。但直到發現有僞超時前,都不會使用它們。
- 執行某種僞超時檢測。
- 檢測到僞超時,設置僞恢復爲SPUR_TO(spurious timeout)
- 若僞恢復爲SPUR_TO,把下一個要發送的報文段的序列號改爲最新的未發送過的報文段。就可以避免GBN。
- 更新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算法:
算法要求:
- 當一個tcp連接中有在傳數據(已發送但未確認),小的報文段(長度小於SMSS)就不能被髮送,直到所有的在傳數據都收到ACK。
- 並且,在收到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是難點。
經典方法
同時只運行一個,可以互相切換。
基於包守恆。
- 建立tcp時執行慢啓動
- 直至有丟包時,執行擁塞避免算法
慢啓動
原因:由於未知網絡傳輸能力,需要緩慢探測可用傳輸資源,防止短時間內大量數據注入導致擁塞。
作用:
- 使TCP在用擁塞避免探尋更多可用帶寬之前得到cwnd值
- 幫助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 (或其他表明需要快速重傳的信號)肘,會執行以下行爲:
- ssthresh更新爲大於等式 ssthresh = max(在外數據值/2, 2*SMSS) 中的值
- 啓用快速重傳算法,將cwnd設爲(ssthresh + 3*SMSS)
- 每接收一個重複ACK, CWnd值暫時增加1 SMSS
- 當接收到一個好的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擁塞控制相關的攻擊
- ACK分裂攻擊:拆分ACK,從而讓發送端的cwnd加速增長。解決辦法:前面說到了。
- 重複ACK攻擊:就是本來沒重複ACK,強行發重複的,也是導致發送端在快速恢復階段增長得更快。解決辦法:限制發送端在恢復階段的在外數據值
- 樂觀響應攻擊:對還沒有收到的報文段產生ACK。導致發送端計算出來的RTT變小,發送端就會比正常情況下發得更快。解決辦法:發送報文大小加一點隨機,使得接收端難以猜出。
TCP保括機制
這一章很短,不是很重點。
keepalive是一種不怎麼納入標準的技術。因爲keepalive是有問題的:對於一個長連接,如果發送保活探測的時候,剛好這段時間中間網絡出了點問題(例如路由器重啓),就會導致好的連接被中斷掉。
保活功能是爲服務器提供的。服務器需要知道客戶端是否崩潰或離開,才能釋放連接資源。
一般,長時間交互式服務就不期望保活功能(ssh);短時間非交互式服務就期望保活功能(Web服務器)。
默認關閉。
非對稱,兩端都可以各自做keepalive。
組成:
- keepalive time:保活時間,即發送保活探測的計時器的timeout時間。一般爲無數據傳輸後2小時。
- keepalive interval:第一個探測發送後,如果沒收到回包,就需要緊跟着再次發送探測。一般爲75秒。
- keepalive probe:探測多少次後才認爲對端不可達,中斷連接。
探測報文:
一個空報文段或帶一個字節(垃圾數據)的報文段。序列號等於對端發送的ACK最大序列號減1,因爲這個序列號已經被成功接收,所以不會對對端造成影響。
探測報文返回的響應可以確定連接是否在工作。
響應報文也是不帶數據或者只帶垃圾數據。
2種報文丟失了都不會重傳。(所以需要重複探測)
探測前後,對端可能的四種狀態:
- 對端正常工作,正常響應了探測。探測定時器重置。
- 對端已崩潰或重啓中,此時不會響應探測。探測端會一直探測直到超過探測次數,就認爲對端已經關閉,斷開這個連接。
- 對端已崩潰但已重啓,會發送RST重置報文段作爲響應。探測端會斷開連接。
- 對端正常工作,但因其他原因探測報文不能到達。4和2相同,因爲tcp無法區分開。
Note:對端正常關閉然後重啓,是沒問題的。對端會發送FIN,正常斷開連接。
欺騙攻擊:因爲保活報文不包含用戶數據,加密強度有限。容易僞造。導致連接一直保持,浪費服務端資源。