前言
小到基於應用層做網路開發,大到生活中無處不在的網路。我們在享受這個便利的時候,沒有人會關心它如此牢固的底層基石是如何搭建的。而這些基石中很重要的一環就是tcp協議。翻看一下“三次握手”和“四次揮手”,本以為這就是tcp了,其實不然。它僅僅解決了連線和關閉的問題,傳輸的問題才是tcp協議更重要,更難,更復雜的問題。回頭看tcp協議的原理,會發現它為了承諾上層資料傳輸的“可靠”,不知要應對多少網路中複雜多變的情況。簡單直白列舉一下:
- 怎麼保證資料都是可靠呢?---連線確認!關閉確認!收到資料確認!各種確認!!
- 因為網路或其他原因,對方收不到資料怎麼辦?--超時重試
- 網路情況千變萬化,超時時間怎麼確定?--根據RTT動態計算
- 反反覆覆,不厭其煩的重試,導致網路擁塞怎麼辦?---慢啟動,擁塞避免,快速重傳,快速恢復
- 傳送速度和接收速度不匹配怎麼辦?--滑動視窗
- 滑動視窗滑的過程中,他一直告訴我處理不過來了,不讓傳資料了怎麼辦?--ZWP
- 滑動視窗滑的過程中,他處理得慢,就理所當然的每次讓我發很少的資料,導致網路利用率很低怎麼辦?---Nagle
其中任何一個小環節,都凝聚了無數的演算法,我們沒有能力理解各個演算法的實現,但是需要了解下tcp實現者的思路歷程。
梳理完所有內容,大概可以知道:
tcp提供哪些機制保證了資料傳輸的可靠性?
tcp連線的“三次握手”和關閉的“四次揮手”流程是怎麼樣的?
tcp連線和關閉過程中,狀態是如何變化的?
tcp頭部有哪些欄位,分別用來做什麼的?
tcp的滑動視窗協議是什麼?
超時重傳的機制是什麼?
如何避免傳輸擁塞?
一. 概述
1. tcp連線的特點
- 提供面向連線的,可靠的位元組流服務
- 為上層應用層提供服務,不關心具體傳輸的內容是什麼,也不知道是二進位制流,還是ascii字元。
2. tcp的可靠性如何保證
- 分塊傳送:資料被分割成最合適的資料塊(UDP的資料包長度不變)
- 等待確認:通過定時器等待接收端傳送確認請求,收不到確認則重發
- 確認回覆:收到確認後傳送確認回覆(不是立即傳送,通常推遲幾分之一秒)
- 資料校驗:保持首部和資料的校驗和,檢測資料傳輸過程有無變化
- 亂序排序:接收端能重排序資料,以正確的順序交給應用端
- 重複丟棄:接收端能丟棄重複的資料包
- 流量緩衝:兩端有固定大小的緩衝區(滑動視窗),防止速度不匹配丟資料
3. tcp的首部格式
3.1 巨集觀位置
- 從應用層->傳輸層->網路層->鏈路層,每經過一次都會在報文中增加相應的首部。參考之前的文章http協議
- TCP資料被封裝在IP資料包中
3.2 首部格式
- tcp首部資料通常包含20個位元組(不包括任選欄位)
- 第1-2兩個位元組:源埠號
- 第3-4兩個位元組:目的埠號
源埠號+ip首部中的源ip地址+目的埠號+ip首部中的目的ip地址,唯一的確定了一個tcp連線。對應編碼級別的socket。
- 第5-8四個位元組:32位序號。tcp提供全雙工服務,兩端都有各自的序號。編號:解決網路包亂序的問題
序號如何生成:不能是固定寫死的,否則斷網重連時序號重複使用會亂套。tcp基於時鐘生成一個序號,每4微秒加一,到2^32-1時又從0開始
- 第9-12四個位元組:32位確認序列號。上次成功收到資料位元組序號加1,ack為1才有效。確認號:解決丟包的問題
- 第13位位元組:首部長度。因為任選欄位長度可變
- 後面6bite:保留
- 隨後6bite:標識位。控制各種狀態
- 第15-16兩個位元組:視窗大小。接收端期望接收的位元組數。解決流量控制的問題
- 第17-18兩個位元組:校驗和。由傳送端計算和儲存,由接收端校驗。解決資料正確性問題
- 第19-20兩個位元組:緊急指標
3.3 標識位說明
- URG:為1時,表示緊急指標有效
- ACK:確認標識,連線建立成功後,總為1。為1時確認號有效
- PSH:接收方應儘快把這個報文交給應用層
- RST:復位標識,重建連線
- SYN:建立新連線時,該位為0
- FIN:關閉連線標識
3.4 tcp選項格式
- 每個選項開始是1位元組kind欄位,說明選項的型別
- kind為0和1的選項,只佔一個位元組
- 其他kind後有一位元組len,表示該選項總長度(包括kind和len)
- kind為11,12,13表示tcp事務
3.5 MSS 最長報文大小
- 最常見的可選欄位
- MSS只能出現在SYN時傳過來(第一次握手和第二次握手時)
- 指明本端能接收的最大長度的報文段
- 建立連線時,雙方都要傳送MSS
- 如果不傳送,預設為536位元組
二. 連線的建立與釋放
1. 連線建立的“三次握手”
1.1 三次握手流程
- 客戶端傳送SYN,表明要向伺服器建立連線。同時帶上序列號ISN
- 伺服器返回ACK(序號為客戶端序列號+1)作為確認。同時傳送SYN作為應答(SYN的序列號為服務端唯一的序號)
- 客戶端傳送ACK確認收到回覆(序列號為服務端序列號+1)
1.2 為什麼是三次握手
- tcp連線是全雙工的,資料在兩個方向上能同時傳遞。
- 所以要確保雙方,同時能發資料和收資料
- 第一次握手:證明了傳送方能發資料
- 第二次握手:ack確保了接收方能收資料,syn確保了接收方能發資料
- 第三次握手:確保了傳送方能收資料
- 實際上是四個維度的資訊交換,不過中間兩步合併為一次握手了。
- 四次握手浪費,兩次握手不能保證“雙方同時具備收發功能”
2. 連線關閉的“四次揮手”
2.1 為什麼是四次揮手
- 因為tcp連線是全雙工的,資料在兩個方向上能同時傳遞。
- 同時tcp支援半關閉(傳送一方結束髮送還能接收資料的功能)。
- 因此每個方向都要單獨關閉,且收到關係通知需要傳送確認回覆
2.2 為什麼要支援半關閉
- 客戶端需要通知服務端,它的資料已經傳輸完畢
- 同時仍要接收來自服務端的資料
- 使用半關閉的單連線效率要比使用兩個tcp連線更好
2.3 四次握手流程
- 主動關閉的一方傳送FIN,表示要單方面關閉資料的傳輸
- 服務端收到FIN後,傳送一個ACK作為確認(序列號為收到的序列號+1)
- 等伺服器資料傳輸完畢,也傳送一個FIN標識,表示關閉這個方向的資料傳輸
- 客戶端回覆ACK以確認回覆
3. 連線和關閉對應的狀態
3.1 狀態說明
- 服務端等待客戶端連線時,處於Listen監聽狀態
- 客戶端主動開啟請求,傳送SYN時處於SYN_SENT傳送狀態
- 客戶端收到syn和ack,並回復ack時,處與Established狀態等待傳送報文
- 服務端收到ack確認後,也處於Established狀態等待傳送報文
- 客戶端傳送fin後,處於fin_wait_1狀態
- 服務端收到fin併傳送ack時,處於close_wait狀態
- 客戶端收到ack確認後,處於fin_wait_2狀態
- 服務端傳送fin後,處於last_ack狀態
- 客戶端收到fin後傳送ack,處於time_wait狀態
- 服務端收到ack後,處於closed狀態
3.2 time_wait狀態
- 也稱為2MSL等待狀態,MSL=Maximum Segment LifetIme,報文段最大生存時間,根據不同的tcp實現自行設定。常用值為30s,1min,2min。linux一般為30s。
- 主動關閉的一方傳送最後一個ack所處的狀態
- 這個狀態必須維持2MSL等待時間
3.2.1 為什麼需要這麼做?
- 設想一個場景,最後這個ack丟失了,接收方沒有收到
- 這時候接收方會重新傳送fin給傳送方
- 這個等待時間就是為了防止這種情況發生,讓傳送方重新傳送ack
- 總結:預留足夠的時間給接收端收ack。同時保證,這個連線不會和後續的連線亂套(有些路由器會快取資料包)
3.2.2 這麼做的後果?
- 在這2MSL等待時間內,該連線(socket,ip+port)將不能被使用
- 很多時候linux上報too many open files,說埠不夠用了,就需要檢查一些程式碼裡面是不是建立大量的socket連線,而這些socket連線並不是關閉後就立馬釋放的
- 客戶端連線伺服器的時候,一般不指定客戶端的埠。因為客戶端關閉然後立馬啟動,按照理論來說是會提示埠被佔用。同樣的道理,主動關閉伺服器,2MSL時間內立馬啟動是會報埠被佔用的錯誤
- 多併發的短連線情況下,會出現大量的Time_wait狀態。這兩個引數可以解決問題,但是它違背了tcp協議,是有風險的。引數為:tcp_tw_reuse和tcp_tw_recycle
- 如果是服務端開發,可設定keep-alive,讓客戶端主動關閉連線解決這個問題
4. 復位報文段
一個報文段從源地址發往目的地址,只要出現錯誤,都會發出復位的報文段,首部欄位的RST是用於“復位”的。這些錯誤包括以下情況
- 埠沒有在監聽
- 異常中止:通過傳送RST而不是fin來中止連線
5. 同時開啟
- 兩個應用程式同時執行主動開啟,稱為“同時開啟“
- 這種情況極少發生
- 兩端同時傳送SYN,同時進入SYN_SENT狀態
- 開啟一條連線而不是兩條
- 要進行四次報文交換過程,“四次握手”
6. 同時關閉
- 雙方同時執行主動關閉
- 進行四次報文交換
- 狀態和正常關閉不一樣
7. 伺服器對於併發請求的處理
- 正等待連線的一端有一個固定長度的佇列(長度叫做“積壓值”,大多數情況長度為5)
- 該佇列中的連線為:已經完成了三次握手,但還沒有被應用層接收(應用層需要等待最後一個ack收到後才知道這個連線)
- 應用層接收請求的連線,將從該佇列中移除
- 當新的請求到來時,先判斷佇列情況來決定是否接收這個連線
- 積壓值的含義:tcp監聽的端點已經被tcp接收,但是等待應用層接收的最大值。與系統允許的最大連線數,伺服器接收的最大併發數無關
三. 資料的傳輸
1. tcp傳輸的資料分類
- 成塊資料傳輸:量大,報文段常常滿
- 互動資料傳輸:量小,報文段為微小分組,大量微小分組,在廣域網傳輸會增加擁堵的出現
- tcp處理的資料包括兩類,有不同的特點,需要不同的傳輸技術
2. 互動資料的傳輸技術
2.1 經受時延的確認
- 概念:tcp收到資料時,並不立馬傳送ack確認,而是稍後傳送
- 目的:將ack與需要沿該方向傳送的資料一起傳送,以減少開銷
- 特點:接收方不必確認每一個收到的分組,ACk是累計的,它表示接收方已經正確收到了一直到確認序號-1的所有位元組
- 延時時間:絕大多數為200ms。不能超過500ms
2.2 Nagle演算法
- 解決什麼問題:微小分組導致在廣域網出現的擁堵問題
- 核心:減少了通過廣域網傳輸的小分組數目
- 原理:要求一個tcp連線上最多隻能有一個未被確認的未完成的分組,該分組的確認到達之前,不能傳送其他分組。tcp收集這些分組,確認到來之前以一個分組的形式發出去
- 優點:自適應。確認到達的快,資料傳送越快。確認慢,傳送更少的組。
- 使用注意:區域網很少使用該演算法。且有些特殊場景需要禁用該演算法
3. 成塊資料的傳輸
- 主要使用滑動視窗協議
四. 滑動視窗協議
1. 概述
- 解決了什麼問題:傳送方和接收方速率不匹配時,保證可靠傳輸和包亂序的問題
- 機制:接收方根據目前緩衝區大小,通知傳送方目前能接收的最大值。傳送方根據接收方的處理能力來傳送資料。通過這種協調機制,防止接收端處理不過來。
- 視窗大小:接收方發給傳送端的這個值稱為視窗大小
2. tcp緩衝區的資料結構
- 接收端:
- LastByteRead: 緩衝區讀取到的位置
- NextByteExpected:收到的連續包的最後一個位置
- LastByteRcvd:收到的包的最後一個位置
- 中間空白區:資料沒有到達
- 傳送端:
- LastByteAcked: 被接收端ack的位置,表示成功傳送確認
- LastByteSent:發出去了,還沒有收到成功確認的Ack
- LastByteWritten:上層應用正在寫的地方
3. 滑動視窗示意圖
3.1 初始時示意圖
- 黑框表示滑動視窗
- #1表示收到ack確認的資料
- #2表示還沒收到ack的資料
- #3表示在視窗中還沒有發出的(接收方還有空間)
- #4視窗以外的資料(接收方沒空間)
3.2 滑動過程示意圖
- 收到36的ack,併發出46-51的位元組
4. 擁塞視窗
- 解決什麼問題:傳送方傳送速度過快,導致中轉路由器擁堵的問題
- 機制:傳送方增加一個擁塞視窗(cwnd),每次受到ack,視窗值加1。傳送時,取擁塞視窗和接收方發來的視窗大小取最小值傳送
- 起到傳送方流量控制的作用
5. 滑動視窗會引發的問題
5.1 零視窗
- 如何發生: 接收端處理速度慢,傳送端傳送速度快。視窗大小慢慢被調為0
- 如何解決:ZWP技術。傳送zwp包給接收方,讓接收方ack他的視窗大小。
5.2 糊塗視窗綜合徵
- 如何發生:接收方太忙,取不完資料,導致傳送方越來越小。最後只讓傳送方傳幾位元組的資料。
- 缺點:資料比tcp和ip頭小太多,網路利用率太低。
- 如何解決:避免對小的視窗大小做響應。
- 傳送端:前面說到的Nagle演算法。
- 接收端:視窗大小小於某個值,直接ack(0),阻止傳送資料。視窗變大後再發。
五. 超時與重傳
1. 概述
- tcp提供可靠的運輸層,使用的方法是確認機制。
- 但是資料和確認都有可能丟失
- tcp通過在傳送時設定定時器解決這種問題
- 定時器時間到了還沒收到確認,就重傳該資料
2. tcp管理的定時器型別
- 重傳定時器:等待收到確認
- 堅持定時器:使視窗大小資訊保持不斷流動
- 保活定時器:檢測空閒連線崩潰或重啟
- 2MSL定時器:檢測time_wait狀態
3. 超時重傳機制
3.1 背景
- 接收端給傳送端的Ack確認只會確認最後一個連續的包
- 比如傳送1,2,3,4,5共五份資料,接收端收到1,2,於是回ack3,然後收到4(還沒收到3),此時tcp不會跳過3直接確認4,否則傳送端以為3也收到了。這時你能想到的方法是什麼呢?tcp又是怎麼處理的呢?
3.1 被動等待的超時重傳策略
- 直觀的方法是:接收方不做任何處理,等待傳送方超時,然後重傳。
- 缺點:傳送端不知道該重發3,還是重發3,4,5
- 如果傳送方如果只傳送3:節省寬度,但是慢
- 如果傳送方如果傳送3,4,5:快,但是浪費寬頻
- 總之,都在被動等待超時,超時可能很長。所以tcp不採用此方法
3.2 主動的快速重傳機制
3.2.1 概述
- 名稱為:Fast Retransmit
- 不以實際驅動,而以資料驅動重傳
3.2.2 實現原理
- 如果包沒有送達,就一直ack最後那個可能被丟的包
- 傳送方連續收到3相同的ack,就重傳。不用等待超時
- 圖中發生1,2,3,4,5資料
- 資料1到達,發生ack2
- 資料2因為某些原因沒有送到
- 後續收到3的時候,接收端並不是ack4,也不是等待。而是主動ack2
- 收到4,5同理,一直主動ack2
- 客戶端收到三次ack2,就重傳2
- 2收到後,結合之前收到的3,4,5,直接ack6
3.2.3 快速重傳的利弊
- 解決了被動等待timeout的問題
- 無法解決重傳之前的一個,還是所有的問題。
- 上面的例子中是重傳2,還是重傳2,3,4,5。因為並不清楚ack2是誰傳回來的
3.3 SACK方法
3.3.1 概述
- 為了解決快速重傳的缺點,一種更好的SACK重傳策略被提出
- 基於快速重傳,同時在tcp頭裡加了一個SACK的東西
- 解決了什麼問題:客戶端應該傳送哪些超時包的問題
3.3.2 實現原理
- SACK記錄一個數值範圍,表示哪些資料收到了
- linux2.4後預設開啟該功能,之前版本需要配置tcp-sack引數
- SACK只是一種輔助的方式,傳送方不能完全依賴SACK。主要還是依賴ACK和timout
3.3.3 Duplicate SACK(D-SACK)
- 使用SACK標識的範圍,還可以知道告知傳送方,有哪些資料被重複接收了
- 可以讓傳送方知道:是發出去的包丟了,還是回來的ack包丟了
4. 超時時間的確定
4.1 背景
- 路由器和網路流量均會變化
- 所以超時時間肯定不能設定為一個固定值
- 超時長:重發慢,效率低,效能差
- 超時短:並沒有丟就重發,導致網路擁塞,導致更多超時和更多重發
- tcp會追蹤這些變化,並相應的動態改變超時時間(RTO)
4.2 如何動態改變
- 每次重傳的時間間隔為上次的一倍,直到最大間隔為64s,稱為“指數退避”
- 首次重傳到最後放棄重傳的時間間隔一般為9min
- 依賴以往的往返時間計算(RTT)動態的計算
4.3 往返時間(RTT)的計算方法
- 並不是簡單的ack時間和傳送時間的差值。因為有重傳,網路阻塞等各種變化的因素。
- 而是通過取樣多次數值,然後做估算
- tcp使用的方法有:
- 被平滑的RTT估計器
- 被平滑的均值偏差估計器
4.4. 重傳時間的具體計算
- 計算往返時間(RTT),儲存測量結果
- 通過測量結果維護一個被平滑的RTT估計器和被平滑的均值偏差估計器
- 根據這兩個估計器計算下一次重傳時間
5. 超時重傳引發的問題-擁塞
5.1 為什麼重傳會引發擁塞
- 當網路延遲突然增加時,tcp會重傳資料
- 但是過多的重傳會導致網路負擔加重,從而導致更大的延時和丟包,進入惡性迴圈
- 也就是tcp的擁塞問題
5.2 解決擁塞-擁塞控制的演算法
- 慢啟動:降低分組進入網路的傳輸速率
- 擁塞避免:處理丟失分組的演算法
- 快速重傳
- 快速恢復
六. 其他定時器
1. 堅持定時器
1.1 堅持定時器存在的意義
- 當視窗大小為0時,接收方會傳送一個沒有資料,只有視窗大小的ack
- 但是,如果這個ack丟失了會出現什麼問題?雙方可能因為等待而中止連線
- 堅持定時器週期性的向接收方查詢視窗是否被增大。這些發出的報文段稱為視窗探查
1.2 堅持定時器啟動時機
- 傳送方被通告接收方視窗大小為0時
1.3 與超時重傳的相同和不同
- 相同:同樣的重傳時間間隔
- 不同:視窗探查從不放棄傳送,直到視窗被開啟或者程式被關閉。而超時重傳到一定時間就放棄傳送
2. 保活定時器
2.1 保活定時器存在的意義
- 當tcp上沒有資料傳輸時,伺服器如何檢測到客戶端是否還存活
參考
- 《tcp/ip詳解 卷1:協議》
- coolshell.cn/articles/11…
- coolshell.cn/articles/11…