前言
小到基於應用層作網絡開發,大到生活中無處不在的網絡。咱們在享受這個便利的時候,沒有人會關心它如此牢固的底層基石是如何搭建的。而這些基石中很重要的一環就是tcp協議。翻看一下「三次握手」和「四次揮手」,本覺得這就是tcp了,其實否則。它僅僅解決了鏈接和關閉的問題,傳輸的問題纔是tcp協議更重要,更難,更復雜的問題。回頭看tcp協議的原理,會發現它爲了承諾上層數據傳輸的「可靠」,不知要應對多少網絡中複雜多變的狀況。簡單直白列舉一下:html
- 怎麼保證數據都是可靠呢?---鏈接確認!關閉確認!收到數據確認!各類確認!!
- 由於網絡或其餘緣由,對方收不到數據怎麼辦?--超時重試
- 網絡狀況變幻無窮,超時時間怎麼肯定?--根據RTT動態計算
- 反反覆覆,不厭其煩的重試,致使網絡擁塞怎麼辦?---慢啓動,擁塞避免,快速重傳,快速恢復
- 發送速度和接收速度不匹配怎麼辦?--滑動窗口
- 滑動窗口滑的過程當中,他一直告訴我處理不過來了,不讓傳數據了怎麼辦?--ZWP
- 滑動窗口滑的過程當中,他處理得慢,就理所固然的每次讓我發不多的數據,致使網絡利用率很低怎麼辦?---Nagle
其中任何一個小環節,都凝聚了無數的算法,咱們沒有能力理解各個算法的實現,可是須要了解下tcp實現者的思路歷程。linux
梳理完全部內容,大概能夠知道:算法
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。shell
- 第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 滑動過程示意圖
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:快,可是浪費寬帶
- 總之,都在被動等待超時,超時可能很長。因此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使用的方法有:
4.4. 重傳時間的具體計算
- 計算往返時間(RTT),保存測量結果
- 經過測量結果維護一個被平滑的RTT估計器和被平滑的均值誤差估計器
- 根據這兩個估計器計算下一次重傳時間
5. 超時重傳引起的問題-擁塞
5.1 爲何重傳會引起擁塞
- 當網絡延遲忽然增長時,tcp會重傳數據
- 可是過多的重傳會致使網絡負擔加劇,從而致使更大的延時和丟包,進入惡性循環
- 也就是tcp的擁塞問題
5.2 解決擁塞-擁塞控制的算法
- 慢啓動:下降分組進入網絡的傳輸速率
- 擁塞避免:處理丟失分組的算法
- 快速重傳
- 快速恢復
六. 其餘定時器
1. 堅持定時器
1.1 堅持定時器存在的意義
- 當窗口大小爲0時,接收方會發送一個沒有數據,只有窗口大小的ack
- 可是,若是這個ack丟失了會出現什麼問題?雙方可能由於等待而停止鏈接
- 堅持定時器週期性的向接收方查詢窗口是否被增大。這些發出的報文段稱爲窗口探查
1.2 堅持定時器啓動時機
1.3 與超時重傳的相同和不一樣
- 相同:一樣的重傳時間間隔
- 不一樣:窗口探查從不放棄發送,直到窗口被打開或者進程被關閉。而超時重傳到必定時間就放棄發送
2. 保活定時器
2.1 保活定時器存在的意義
- 當tcp上沒有數據傳輸時,服務器如何檢測到客戶端是否還存活
參考