如果你正在準備面試TCP,看這一篇就夠了

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"前言","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"TCP(Transmission Control Protocol,傳輸控制協議) 是計算機網絡的的重要組成部分,也是網絡編程的重要內容,還有我們平時接觸最多的 HTTP 也是基於 TCP 實現的。TCP 可以說是最重要的傳輸層協議,既然如此,作爲開發人員,就有必要把 TCP 的核心概念和原理搞清楚。除此之外,諸如三次握手、四次揮手、滑動窗口和擁塞控制這些概念更是高頻面試題,這就更有理由深入學習一下 TCP 了,本文就爲大家詳細梳理一下 TCP 的核心概念和原理。注:由於本文圖片較多,標註有 by HYN 的圖片爲作者自制,其它來自於網絡或參考資料,侵刪","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"一、TCP 簡介","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第一部分先爲大家介紹一下 TCP 的主要概念,並講解一下 TCP 的三個重要特性——1. 面向連接;2. 基於字節流;3. 可靠性。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"關於網絡分層的概念實在是老生常談了,下圖就是兩種經典的分層模型,可以看到 TCP 在網絡分層中的位置。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/a1/a1d7e5c414e056b5554cc0e7d52747e9.jpeg","alt":"如果你正在準備面試TCP,看這一篇就夠了","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"網絡分層模型","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文重點對 TCP 進行介紹,從圖中可以看到 TCP 位於傳輸層,而且構建於網絡層的 IP 協議之上,對於 TCP 最常見的介紹就是 “TCP 是一種面向連接的、可靠的、基於字節流的傳輸層通信協議”,那這三個形容詞究竟是什麼意思呢?","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"1.1 面向連接","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"面向連接意味着兩個使用 TCP 的應用 (通常是一個客戶端和一個服務器) 在彼此交換數據之前必須先建立一個 TCP 連接。這一過程與打電話很相似,先撥號響鈴,等對方應答後後再說明是誰。詳細的三次握手、四次揮手過程將在第二部分——連接管理部分進行介紹。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"1.2 基於字節流","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"TCP 連接雙方的數據交換格式是以字節 (byte,1byte = 8 bit)構成的有序但無結構的字節流。TCP 不在字節流中插入記錄標識符,這被稱爲字節流服務(byte stream service)。如果一方的應用程序先傳 10 字節,又傳 20 字節,再傳 50 字節 ,連接的另一方將無法瞭解發方每次發送了多少字節。收方可以分 4 次接收這 80 個字節,每次接收 20 字節。一端將字節流放到 TCP 連接上,同樣的字節流將出現在 TCP 連接的另一端。另外,TCP 對字節流的內容不作任何解釋,TCP 無法知道傳輸的數據字節流是二進制數據,還是 ASCI I 字符。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果覺得上面這段話比較抽象的話,可以拿 TCP 的字節流和 UDP 的報文 (message) 進行比較( ","attrs":{}},{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"UDP:User Datagram Protocol,用戶數據報協議,和 TCP 同爲傳輸層的協議,後面會提供兩者的全面對比","attrs":{}},{"type":"text","text":" )。TCP 的字節流類似於自來水,連接雙方都有緩衝區,可以類比成蓄水池,發送方的發送頻率和每次的發送量沒有固定要求,接收方也可以自由決定自己的接收頻率和每次的接收量,只要把所有的數據接收完畢即可。而 UDP 的數據報則類似於瓶裝水 (比如農夫山泉),發送方發送一瓶,接收方就要相應地接收一瓶。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下圖描述了 TCP 連接中數據的傳輸過程以及 TCP 在整個過程中所扮演的角色。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/84/8495c669796e8831b2c55389e571ac21.jpeg","alt":"如果你正在準備面試TCP,看這一篇就夠了","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"TCP 在網絡數據傳輸中的位置和角色","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"按照圖中的流程,比如我們在瀏覽B站,在 TCP 連接建立之後,客戶端的應用層協議可以向 TCP 發送無特殊格式的字節流,TCP 會將這些字節打包成報文段(segment),報文段大小視情況而定,這些報文段會被網絡層的 IP 封裝成 IP 數據報(IP Datagram),然後經過網絡傳輸給服務器,而接下來服務器的操作相當於客戶端的逆操作,先從 IP 數據報中拆分出 TCP 報文段,再把 TCP 報文段還原成字節流併發送給上層的應用層協議。服務器向客戶端發送數據的流程也是一樣的,發送方和接收方的角色互換即可。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"報文段簡介","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面多次提到了報文段的概念,其結構非常重要,後面的連接過程和擁塞控制等內容也要用到相關概念,先在這裏介紹一下。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/02/02cd210202342d253837cefab1cc1130.jpeg","alt":"如果你正在準備面試TCP,看這一篇就夠了","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"TCP 報文段結構","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"圖的上半部分顯示 TCP 報文段被封裝在 IP 數據報中,圖的下半部分則顯示了 TCP 報文段和 TCP 首部的結構,TCP 首部的固定數據有20字節,加上選項部分最大可達60字節,而有效數據部分則是被打包的應用層數據。下面介紹一下 TCP 首部的結構:","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"端口號 (Source Port and Destination Port):每個 TCP 報文段都包含源端和目的端的端口號,用於尋找發送端和接收端應用進程。這兩個值加上 IP 首部中的源端 IP 地址和目的端 IP 地址就可以確定一個唯一的 TCP 連接。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"序號 (Sequence Number):這個字段的主要作用是用於將失序的數據重新排列。TCP 會隱式地對字節流中的每個字節進行編號,而 TCP 報文段的序號被設置爲其數據部分的第一個字節的編號。序號是 32 bit 的無符號數,取值範圍是0到 2 32 - 1。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"確認序號 (Acknowledgment Number):接收方在接受到數據後,會回覆確認報文,其中包含確認序號,作用就是告訴發送方自己接收到了哪些數據,下一次數據從哪裏開始發,因此,確認序號應當是上次已成功收到數據字節序號加 1。只有 ACK 標誌爲 1 時確認序號字段纔有效。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首部長度 (Header Length):首部中的選項部分的長度是可變的,因此首部的長度也是可變的,所以需要這個字段來明確表示首部的長度,這個字段佔 4 bit,4 位的二進制數最大可以表示 15,而首部長度是以 4 個字節爲一個單位的,因此首部最大長度是 15 * 4 = 60 字節。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"保留字段 (Reserved):佔 6 位,未來可能有具體用途,目前默認值爲0.","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"控制位 (Control Bits):在三次握手和四次揮手中會經常看到 SYN、ACK 和 FIN 的身影,一共有 6 個標誌位,它們表示的意義如下:URG (Urgent Bit):值爲 1 時,緊急指針生效ACK (Acknowledgment Bit):值爲 1 時,確認序號生效PSH (Push Bit):接收方應儘快將這個報文段交給應用層RST (Reset Bit):發送端遇到問題,想要重建連接SYN (Synchronize Bit):同步序號,用於發起一個連接FIN (Finish Bit):發送端要求關閉連接","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"窗口大小 (Window): TCP的流量控制由連接的每一端通過聲明的窗口大小來提供。窗口大小爲字節數,起始於確認序號字段指明的值,這個值是接收端正期望接收的字節。窗口大小是一個 16 bit 字段,單位是字節, 因而窗口大小最大爲 65535 字節。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"檢驗和 (Checksum):功能類似於數字簽名,用於驗證數據完整性,也就是確保數據未被修改。檢驗和覆蓋了整個 TCP 報文段,包括 TCP 首部和 TCP 數據,發送端根據特定算法對整個報文段計算出一個檢驗和,接收端會進行計算並驗證。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"緊急指針 (Urgent Pointer):當 URG 控制位值爲 1 時,此字段生效,緊急指針是一個正的偏移量,和序號字段中的值相加表示緊急數據最後一個字節的序號。 TCP 的緊急方式是發送端向另一端發送緊急數據的一種方式。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"選項 (Options):這一部分是可選字段,也就是非必須字段,最常見的可選字段是“最長報文大小 (MSS,Maximum Segment Size)”。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有效數據部分 (Data):這部分也不是必須的,比如在建立和關閉 TCP 連接的階段,雙方交換的報文段就只包含 TCP 首部。","attrs":{}}]}]}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"1.3 可靠性","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們都知道 TCP 是具有可靠性的通信協議,它主要通過以下方式確保可靠性,這裏先了解一下可靠性的原理,其中細節部分後文會講:","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"合理的數據大小:TCP 發送的數據並不是固定的大小,而是會根據實際情況調整報文段的大小。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"檢驗和:發送端按照特定算法計算出 TCP 報文段的檢驗和並存儲在 TCP 首部中的對應字段上,接收端在接收時會以同樣的方式計算校驗和,如果不一致,說明報文段出現錯誤,會將其丟棄。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"序號與確認序號:對亂序的數據進行排序後發給應用層,並丟棄重複的數據。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"超時重傳機制:當 TCP 發出一個報文段後,它會啓動一個定時器,等待目的端確認收到這個報文段。如果不能及時收到一個確認,將重發這個報文段,後面會細講這個機制。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"連接管理:也就是三次握手和四次揮手,連接的可靠性是整體可靠性的前提,本文第二部分將會詳細介紹連接管理的內容。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"流量控制:TCP 雙方都有固定大小的緩衝區,流量控制的原理是利用滑動窗口控制數據發送速度,避免緩衝區溢出導致數據丟失。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"擁塞控制:TCP 利用慢啓動和擁塞避免等算法實現了擁塞控制。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面爲大家介紹了 TCP 最重要的三個特點,在本文第一部分的最後,再來看看 TCP 和 UDP 的對比吧。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"1.4 TCP 和 UDP 的區別","attrs":{}}]},{"type":"embedcomp","attrs":{"type":"table","data":{"content":"


UDP

TCP

是否連接

無連接

面向連接

是否可靠

不可靠,沒有確認機制、流量控制和擁塞控制

可靠,有確認機制、流量控制和擁塞控制

連接對象個數

支持一對一,一對多,多對一和多對多交互通信

只支持一對一通信

傳輸方式

面向報文

面向字節流

首部開銷

首部開銷小,固定8字節

首部開銷較大,最小20字節,最大60字節

適用場景

適用於實時應用(IP電話、視頻會議、直播等)

適用於要求可靠傳輸的應用,如文件傳輸等

"}}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"二、TCP 的連接控制","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"2.1 建立連接","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"2.1.1 三次握手","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個問題簡直太經典了,如果你在面試中只被問到了一個關於 TCP 的問題,那大概率就是關於三次握手的問題。TCP 的重要特性之一就是面向連接,連接雙方在發送數據之前必須經歷握手的階段,那具體的過程是怎樣的呢?先來看圖,大家最好可以動手簡單畫畫這個圖,當然還有後文四次揮手的圖,幫助加深記憶。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/03/03dc5be0cd1731765692c0347c1d6d14.jpeg","alt":"如果你正在準備面試TCP,看這一篇就夠了","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"三次握手過程","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如圖所示,雙方之間的三個藍色箭頭就表示了三次握手過程中所發生的數據交換:","attrs":{}}]},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"第一次握手:客戶端向服務器發送報文段1,其中的 SYN 標誌位 (前文已經介紹過各種標誌位的作用)的值爲 1,表示這是一個用於請求發起連接的報文段,其中的序號字段 (Sequence Number,圖中簡寫爲seq)被設置爲初始序號x (Initial Sequence Number,ISN),TCP 連接雙方均可隨機選擇初始序號。發送完報文段1之後,客戶端進入 SYN-SENT 狀態,等待服務器的確認。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"第二次握手:服務器在收到客戶端的連接請求後,向客戶端發送報文段2作爲應答,其中 ACK 標誌位設置爲 1,表示對客戶端做出應答,其確認序號字段 (Acknowledgment Number,圖中簡寫爲小寫 ack) 生效,該字段值爲 x + 1,也就是從客戶端收到的報文段的序號加一,代表服務器期望下次收到客戶端的數據的序號。此外,報文段2的 SYN 標誌位也設置爲1,代表這同時也是一個用於發起連接的報文段,序號 seq 設置爲服務器初始序號y。發送完報文段2後,服務器進入 SYN-RECEIVED 狀態。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"第三次握手:客戶端在收到報文段2後,向服務器發送報文段3,其 ACK 標誌位爲1,代表對服務器做出應答,確認序號字段 ack 爲 y + 1,序號字段 seq 爲 x + 1。此報文段發送完畢後,雙方都進入 ESTABLISHED 狀態,表示連接已建立。","attrs":{}}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"常見面試題 1:TCP 建立連接爲什麼要三次握手而不是兩次?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"答:網上大多數資料對這個問題的回答只有簡單的一句:防止已過期的連接請求報文突然又傳送到服務器,因而產生錯誤,這既不夠全面也不夠具體。下面給出比較詳細而全面的回答:","attrs":{}}]},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"防止已過期的連接請求報文突然又傳送到服務器,因而產生錯誤在雙方兩次握手即可建立連接的情況下,假設客戶端發送 A 報文段請求建立連接,由於網絡原因造成 A 暫時無法到達服務器,服務器接收不到請求報文段就不會返回確認報文段,客戶端在長時間得不到應答的情況下重新發送請求報文段 B,這次 B 順利到達服務器,服務器隨即返回確認報文並進入 ESTABLISHED 狀態,客戶端在收到 確認報文後已進入 ESTABLISHED 狀態,雙方建立連接並傳輸數據,之後正常斷開連接。此時姍姍來遲的 A 報文段纔到達服務器,服務器隨即返回確認報文並進入 ESTABLISHED 狀態,但是已經進入 CLOSED 狀態的客戶端無法再接受確認報文段,更無法進入 ESTABLISHED 狀態,這將導致服務器長時間單方面等待,造成資源浪費。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"三次握手才能讓雙方均確認自己和對方的發送和接收能力都正常第一次握手:客戶端只是發送處請求報文段,什麼都無法確認,而服務器可以確認自己的接收能力和對方的發送能力正常;第二次握手:客戶端可以確認自己發送能力和接收能力正常,對方發送能力和接收能力正常;第三次握手:服務器可以確認自己發送能力和接收能力正常,對方發送能力和接收能力正常;可見三次握手才能讓雙方都確認自己和對方的發送和接收能力全部正常,這樣就可以愉快地進行通信了。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"告知對方自己的初始序號值,並確認收到對方的初始序號值TCP 實現了可靠的數據傳輸,原因之一就是 TCP 報文段中維護了序號字段和確認序號字段,也就是圖中的 seq 和 ack,通過這兩個字段雙方都可以知道在自己發出的數據中,哪些是已經被對方確認接收的。這兩個字段的值會在初始序號值得基礎遞增,如果是兩次握手,只有發起方的初始序號可以得到確認,而另一方的初始序號則得不到確認。","attrs":{}}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"常見面試題2:TCP 建立連接爲什麼要三次握手而不是四次?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"答:相比上個問題而言,這個問題就簡單多了。因爲三次握手已經可以確認雙方的發送接收能力正常,雙方都知道彼此已經準備好,而且也可以完成對雙方初始序號值得確認,也就無需再第四次握手了。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"常見面試題3:有一種網絡攻擊是利用了 TCP 建立連接機制的漏洞,你瞭解嗎?這個問題怎麼解決?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"答:在三次握手過程中,服務器在收到了客戶端的 SYN 報文段後,會分配並初始化連接變量和緩存,並向客戶端發送 SYN + ACK 報文段,這相當於是打開了一個“半開連接 (half-open connection)”,會消耗服務器資源。如果客戶端正常返回了 ACK 報文段,那麼雙方可以正常建立連接,否則,服務器在等待一分鐘後會終止這個“半開連接”並回收資源。這樣的機制爲 SYN洪泛攻擊 (SYN flood attack)提供了機會,這是一種經典的 DoS攻擊 (Denial of Service,拒絕服務攻擊),所謂的拒絕服務攻擊就是通過進行攻擊,使受害主機或網絡不能提供良好的服務,從而間接達到攻擊的目的。在 SYN 洪泛攻擊中,攻擊者發送大量的 SYN 報文段到服務器請求建立連接,但是卻不進行第三次握手,這會導致服務器打開大量的半開連接,消耗大量的資源,最終無法進行正常的服務。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"解決方法:SYN Cookies,現在大多數主流操作系統都有這種防禦系統。SYN Cookies 是對 TCP 服務器端的三次握手做一些修改,專門用來防範 SYN 洪泛攻擊的一種手段。它的原理是,在服務器接收到 SYN 報文段並返回 SYN + ACK 報文段時,不再打開一個半開連接,也不分配資源,而是根據這個 SYN 報文段的重要信息 (包括源和目的 IP 地址,端口號可一個祕密數),利用特定散列函數計算出一個 cookie 值。這個 cookie 作爲將要返回的SYN + ACK 報文段的初始序列號(ISN)。當客戶端返回一個 ACK 報文段時,服務器根據首部字段信息計算 cookie,與返回的確認序號(初始序列號 + 1)進行對比,如果相同,則是一個正常連接,然後分配資源並建立連接,否則拒絕建立連接。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"2.2.2 同時打開","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這是 TCP 建立連接的特殊情況,有時會出現兩臺機器同時執行主動打開的情況,不過概率非常小,這種情況大家僅作了解即可。在這種情況下就無所謂發送方和接收方了,雙放都可以稱爲客戶端和服務器,同時打開的過程如下:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/78/78931b3ff5698f83d6b867b1d9c4c178.jpeg","alt":"如果你正在準備面試TCP,看這一篇就夠了","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"同時打開的過程","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如圖所示,雙方在同一時刻發送 SYN 報文段,並進入 SYN-SENT 狀態,在收到 SYN 後,狀態變爲 SYN-RECEIVED,同時它們都在發送一個 SYN + ACK 的報文段,狀態都變爲 ESTABLISHED,連接成功建立。在此過程中雙方一共交換了4個報文段,比三次握手多一個。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"2.2 關閉連接","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"2.2.1 四次揮手","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"建立一個連接需要三次握手,而終止一個連接要經過 4次握手。這由 TCP 的半關閉( half-close) 造成的。既然一個 TCP 連接是全雙工 (即數據在兩個方向上能同時傳遞), 因此每個方向必須單獨地進行關閉。這原則就是當一方完成它的數據發送任務後就能發送一個 FIN 來終止這個方向連接。當一端收到一個 FIN,它必須通知應用層另一端已經終止了數據傳送。理論上客戶端和服務器都可以發起主動關閉,但是更多的情況下是客戶端主動發起。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/e2/e25115874fe825d7cb0738906865522e.jpeg","alt":"如果你正在準備面試TCP,看這一篇就夠了","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"四次揮手過程","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"四次揮手詳細過程如下:","attrs":{}}]},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"客戶端發送關閉連接的報文段,FIN 標誌位1,請求關閉連接,並停止發送數據。序號字段 seq = x (等於之前發送的所有數據的最後一個字節的序號加一),然後客戶端會進入 FIN-WAIT-1 狀態,等待來自服務器的確認報文。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"服務器收到 FIN 報文後,發回確認報文,ACK = 1, ack = x + 1,並帶上自己的序號 seq = y,然後服務器就進入 CLOSE-WAIT 狀態。服務器還會通知上層的應用程序對方已經釋放連接,此時 TCP 處於半關閉狀態,也就是說客戶端已經沒有數據要發送了,但是服務器還可以發送數據,客戶端也還能夠接收。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"客戶端收到服務器的 ACK 報文段後隨即進入 FIN-WAIT-2 狀態,此時還能收到來自服務器的數據,直到收到 FIN 報文段。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"服務器發送完所有數據後,會向客戶端發送 FIN 報文段,各字段值如圖所示,隨後服務器進入 LAST-ACK 狀態,等待來自客戶端的確認報文段。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":5,"align":null,"origin":null},"content":[{"type":"text","text":"客戶端收到來自服務器的 FIN 報文段後,向服務器發送 ACK 報文,隨後進入 TIME-WAIT 狀態,等待 2MSL(2 * Maximum Segment Lifetime,兩倍的報文段最大存活時間) ,這是任何報文段在被丟棄前能在網絡中存在的最長時間,常用值有30秒、1分鐘和2分鐘。如無特殊情況,客戶端會進入 CLOSED 狀態。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":6,"align":null,"origin":null},"content":[{"type":"text","text":"服務器在接收到客戶端的 ACK 報文後會隨即進入 CLOSED 狀態,由於沒有等待時間,一般而言,服務器比客戶端更早進入 CLOSED 狀態。","attrs":{}}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"常見面試題1:爲什麼 TCP 關閉連接爲什麼要四次而不是三次?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"答:服務器在收到客戶端的 FIN 報文段後,可能還有一些數據要傳輸,所以不能馬上關閉連接,但是會做出應答,返回 ACK 報文段,接下來可能會繼續發送數據,在數據發送完後,服務器會向客戶單發送 FIN 報文,表示數據已經發送完畢,請求關閉連接,然後客戶端再做出應答,因此一共需要四次揮手。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"常見面試題2:客戶端爲什麼需要在 TIME-WAIT 狀態等待 2MSL 時間才能進入 CLOSED 狀態?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"答:按照常理,在網絡正常的情況下,四個報文段發送完後,雙方就可以關閉連接進入 CLOSED 狀態了,但是網絡並不總是可靠的,如果客戶端發送的 ACK 報文段丟失,服務器在接收不到 ACK 的情況下會一直重複 FIN 報文段,這顯然不是我們想要的。因此客戶端爲了確保服務器收到了 ACK,會設置一個定時器,並在 TIME-WAIT 狀態等待 2MSL 的時間,如果在此期間又收到了來自服務器的 FIN 報文段,那麼客戶端會重新設置計時器並再次等待 2MSL 的時間,如果在這段時間內沒有收到來自服務器的 FIN 報文,那就說明服務器已經成功收到了 ACK 報文,此時客戶端就可以進入 CLOSED 狀態了。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"2.2.2 同時關閉","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"之前在介紹 TCP 建立連接的時候會有一種特殊情況,那就是同時打開,與之對應地, TCP 關閉時也會有一種特殊情況,那就是同時關閉,這種情況僅作了解即可,流程圖如下:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/fd/fd083cd5adc3e829562c9fef87279df4.jpeg","alt":"如果你正在準備面試TCP,看這一篇就夠了","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"同時關閉過程","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這種情況下,雙方應用層同時發出關閉命令,這將導致雙方各發送一個 FIN,兩端均從 ESTABLISHED 變爲 FIN_WAIT_1,兩個 FIN 經過網絡傳送後分別到達另一端。收到 FIN 後,狀態由 FIN_WAIT_1 變遷到 CLOSING,併發送最後的 ACK,當收到最後的 ACK 時,爲確保對方也收到 ACK,狀態變化爲 TIME_WAIT,並等待 2MSL 時間,如果一切正常,隨後會進入 CLOSED 狀態。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"三、TCP 的流量控制與滑動窗口","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"3.1 什麼是流量控制?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"TCP 連接雙方的主機都爲該連接設置了發送緩存和接收緩存,這些緩存起到了蓄水池的作用,我們肯定不能把上層應用程序發來的數據一股腦兒發送到網絡中,而是利用發送緩存將其緩存起來,然後再按一定的速率通過網絡發送給對方,而接收緩存的作用是把對方傳來的數據先緩存起來,等到己方應用程序有空的時候再來取走數據。示意圖如下:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/25/259ada5c64db8f01eb94918977b345e1.jpeg","alt":"如果你正在準備面試TCP,看這一篇就夠了","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"TCP 緩存示意圖","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在此過程中,如果接收方應用程序讀取數據的速度小於發送方的數據發送速度,將導致接收方的接收緩存溢出,造成數據丟失,這顯然不是我們想看到的。因此 TCP 爲應用程序提供了流量控制服務 (flow-control service),以消除發送方使接收方的接收緩存溢出的可能性。簡單來說流量控制的目的就是協調發送方的數據發送速度,使其與接收方的數據處理速度相匹配,避免數據丟失,那麼如何實現流量控制呢?","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"3.2 早期的流量控制模式——停止-等待模式 (stop-wait)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"顧名思義就是發送方在發送一個數據包後就停止發送,等待對方響應 ACK,然後才能繼續發送數據。這種模式的具體實現爲 Positive Acknowledgment With Retransmission (PAR),意爲帶重傳的肯定確認協議,其實現方式如下圖所示:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/b3/b36c7a8a537ee8fa50ca1e6c3c35662e.png","alt":"如果你正在準備面試TCP,看這一篇就夠了","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"PAR 示意圖","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這種實現很簡單,發送方在發送數據包 (圖中的msg)時會設置一個計時器,然後等待接收方的 ACK,接收方在收到數據後會返回 ACK 作爲應答,發送方在收到 ACK 後會發送下一個數據包。如果由於網絡原因造成數據包或者 ACK 丟失時,計時器會超時,然後發送方會重新發送未被確認的數據包。可以看到,這種模式雖然可以確保數據傳輸的可靠性,但是有個致命的缺點,那就是效率太低?如果是你,你會怎麼對這個方案進行優化呢?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"既然每次發送只一個數據包效率太低,那就多發送幾個,然後給這些數據包編上號,接收端必須對每一個包進行確認,這樣設備 A 一次多發送幾個片段,而不必等候 ACK,同時接收端也要告知它能夠收多少,這樣發送端發起來也有個限制,當然還需要保證順序性,不要亂序,對於亂序的狀況,我們可以允許等待一定情況下的亂序,比如說先緩存提前到的數據,然後去等待需要的數據,如果一定時間沒來就丟掉亂序的數據,來保證順序性,這樣的話,數據傳輸效率就可以大大提高。不過 TCP 也沒有采用這種方案,而是在此基礎上實現更加複雜的滑動窗口。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"3.3 滑動窗口","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先給大家推薦一個視頻,講得很不錯https://www.bilibili.com/vide...","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們可以把發送方的發送緩存中的字節分爲以下四類,每個編號對應一個字節:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/e1/e1d4276786a79454d870531deec011c0.jpeg","alt":"如果你正在準備面試TCP,看這一篇就夠了","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"發送緩存中的字節分類","attrs":{}}]},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"第一類:已發送且已確認,這些數據已經發送成功並已經被確認的數據,比如圖中的前31個bytes,這些數據其實的位置是在窗口之外了,下一步將被移出發送緩存。窗口內順序最低的字節被確認之後,窗口左邊界會向右移動,稱爲窗口合攏。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"第二類:已發送但未收到確認,這部分數據已經被髮送出去,但是還沒有收到接收端的 ACK,認爲並沒有完成發送,這部分數據屬於窗口內的數據。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"第三類:未發送但是接收方已經準備好接收,這部分是儘快發送的數據,這部分數據已經被加載到緩存中,也在發送窗口中,正在等待發送,其實這個窗口是完全有接收方告知的,接收方告知當前可以接受這些數據,所以發送方需要儘快的發送。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"第四類:未發送且接收方未準備好接收,這些數據屬於未發送,同時接收端也不允許發送的,因爲這些數據已經超出了發送端所接收的範圍。","attrs":{}}]}]}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"3.3.1 發送窗口和接收窗口","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/e8/e876ad31d7964226d5a3488519b08f03.jpeg","alt":"如果你正在準備面試TCP,看這一篇就夠了","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"發送窗口","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"發送窗口:圖中的黑色框就是發送方的發送窗口,其大小由兩個因素決定:1、接收方的提供的窗口大小 (TCP 報文段首部中的 window 字段),發送方在三次握手階段首次得到這個值,之後的通信過程中接收方會根據自己的可用緩存對這個值進行動態調整;2、發送方會根據網絡情況維護一個擁塞窗口變量 (後文介紹)。發送窗口的大小取這兩個值的最小值。對於發送方來說,發送窗口分爲兩部分,分別是已經發送的部分(已經發送了,但是沒有收到ACK)和可用窗口,接收端允許發送但是沒有發送的那部分稱爲可用窗口。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接收窗口:對於接收端也是有一個接收窗口的,類似發送端,接收端的數據有3個分類,因爲接收端並不需要等待ACK所以它沒有類似的接收並確認了的分類,情況如下","attrs":{}}]},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"Received and ACK Not Send to Process:這部分數據屬於接收了數據但是還沒有被上層的應用程序接收;","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"Received Not ACK: 已經接收,但是還沒有回覆 ACK;","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"Not Received:有空位,還沒有被接收的數據。","attrs":{}}]}]}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"3.3.2 滑動窗口是如何滑動的?","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/47/47e1af8d30a334b487203690a8c38e2c.jpeg","alt":"如果你正在準備面試TCP,看這一篇就夠了","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"滑動窗口的滑動過程","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"累積確認概念:TCP 並不是每一個報文段都會回覆一個 ACK ,可能會對兩個報文段發送一個ACK,也可能會對多個報文段發送 1 個 ACK,這稱爲累積確認。比如說發送方有 1/2/3 3 個報文段,先發送了2,3 兩個報文段,但是接收方期望收到1報文段,這個時候 2/3 報文段就只能放在緩存中等待報文1的空洞被填上,如果報文段1一直不來,報文2/3也將被丟棄,如果報文1來了,那麼會發送一個 ACK 對第3個報文段進行確認,就代表對這三個報文段全部進行了確認。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面舉例說明一下窗口滑動的過程:","attrs":{}}]},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"在握手過程中,接收方通告的窗口大小爲20字節,所以發送方將發送窗口大小設置爲20字節。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"從圖中的\"上一個發送窗口的位置\"(灰色虛線框)說起, 32-51號字節恰好處於發送窗口中,恰好20個字節,假設 TCP 將其分爲 4 個報文段進行發送,每個報文段 5 個字節數據,分別記爲 seg1 32-36, seg2 37-41, seg3 42-46, seg4 47-51。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"TCP 將有序發送 seg1、seg2、seg3和seg4四個報文段,如果這四個報文段都順利到達接收方 (圖中並不是這樣),接收方將發回一個累積確認的 ACK 報文段,其中 ack = 52,代表希望收到下一個報文段的起始字節編號,報文段中也會繼續通告窗口大小,如果還是20字節,那麼發送方的窗口將整體向右移動20字節,如果通告的窗口值變小,比如變成15,那麼發送窗口左邊界移動20字節,右邊界移動15字節。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"如果在發送過程中 seg2 報文段丟失,而其他三個報文段正常到達接收方,那麼接收方會現接受這三個報文段,然後返回 ACK 報文段,ack = 37,表示希望收到的下一個報文段的起始字節號爲37,也就是seg2報文段。如果通告窗口值未發生變化,發送方在收到 ACK 後會將窗口整體右移5個字節,也就變成了圖中的位置。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":5,"align":null,"origin":null},"content":[{"type":"text","text":"由於 seg2 還未收到 ACK,當重傳計時器超時後,發送方會重新發送 seg2,此時52-56號字節又落到了發送窗口中,TCP 將其封裝成 報文段進行發送,如果接收方全部順利收到,會返回一個累積確認的 ACK,ack = 57,表示希望收到的喜愛個報文段的起始字節號爲57。","attrs":{}}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下來就是重複上述過程,直到 TCP 字節流的所有數據發送完畢。在這個過程中,接收方會根據自己接收緩存的剩餘空間動態調整窗口值,對發送方進行流量控制。文字描述可能不夠直觀,大家可以參考上文推薦的視頻。另外推薦一個動圖演示的網站 動畫地址 ,可以觀看滑動窗口的動態效果,如下圖 (此演示未考慮丟包的情況):","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/b9/b968acafbd6b608bd2dcee18c14112e3.gif","alt":"如果你正在準備面試TCP,看這一篇就夠了","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"滑動窗口動畫效果","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"四、TCP 的擁塞控制","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"4.1 什麼是擁塞控制?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當數據從一個大的管道 (比如一個快速局域網)向一個較小的管道 (比如較慢的廣域網)發送的時候就會發生擁塞,還有一種情況就是當多個輸入流到達一個路由器,而路由器的輸出流小於這些輸入流的總和時,也會發生擁塞。舉個例子就好理解了,第一種情況就好像源源不斷的車流從八車道進入四車道,如果不進行控制,必然造成道路擁堵;第二種情況類似於很多車輛匯入十字路口,如果進的速度大於出的速度,再不加以控制,必然也會造成擁堵。於是 TCP 提供了相應的機制來應對這種情況,也就是 TCP 的擁塞控制。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"4.2 如何實現擁塞控制?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"TCP 一共使用了四種算法來實現擁塞控制:1、慢開始 (slow-start);2、擁塞避免 (congestion avoidance);3、快速重傳 (fast retransmit);4、快速恢復 (fast recovery)。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏先介紹一下擁塞窗口 (congestion window,簡寫爲 cwnd)的概念:擁塞窗口是由發送方根據網絡狀況維護的一個變量,用於控制自己的數據發送速率。前文提到了發送方的發送窗口受兩個變量約束,一是接收方通告的窗口大小值,二就是發送方自身的擁塞窗口,實際的發送窗口大小取二者最小值。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/aa/aa63891c77fdaf1bcab0101d70770391.jpeg","alt":"如果你正在準備面試TCP,看這一篇就夠了","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"慢開始和擁塞避免","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"4.2.1 慢開始(慢啓動)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如圖所示,在剛開始,TCP 採用慢開始算法。慢開始不是指擁塞窗口的增長速度慢(增長速度是指數增長,非常快),而是指 TCP 開始發送設置 cwnd=1。思路就是不要一開始就發送大量的數據,先探測一下網絡的擁塞程度,也就是說由小到大 逐漸增加擁塞窗口的大小。這裏用報文段的個數的擁塞窗口大小舉例說明慢啓動算法,實時擁塞窗口大小是以字節爲單位的。爲了防止 cwnd 增長過大引起網絡擁塞,設置一個慢開始門限(slow start threshold,簡寫爲 ssthresh) ,","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當cnwd < ssthresh,使用慢開始算法","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當 cnwd = ssthresh,既可使用慢開始算法,也可以使用擁塞避免算法","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當 cnwd > ssthresh,使用擁塞避免算法","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"4.2.2 擁塞避免","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當擁塞窗口大小達到初始 ssthresh 值時,轉而採用擁塞避免算法。擁塞避免並非完全能夠避免擁塞,是說在擁塞避免階段將擁塞窗口控制爲按線性規律增長,使網絡比較不容易出現擁塞,思路:讓擁塞窗口 cwnd 緩慢地增大,即每經過一個往返時間 RTT 就把發送方的擁塞窗口加一。無論是在慢開始階段還是在擁塞避免階段,只要發送方判斷網絡出現擁塞(其根據就是沒有收到確認,雖然沒有收到確認可能是其他原因的分組丟失,但是因爲無法判定,所以都當做擁塞來處理),就把慢開始門限設置爲出現擁塞時的發送窗口大小的一半。然後把擁塞窗口設置爲 1,執行慢開始算法。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"4.2.3 快速重傳","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/06/0686b9df7fd6d46cb1b91c6a1691d50c.jpeg","alt":"如果你正在準備面試TCP,看這一篇就夠了","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"TCP Reno 應用了四種算法","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有時候的發送方未收到某個報文段的確認也並一定就說明一定是出現了網絡擁塞,也可能是其他原因,所以直接執行慢開始算法會影響整體效率,後來的 TCP Reno 版本解決了這一問題,那就是採用快速重傳和快速恢復算法。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"快速重傳要求接收方在收到一個失序的報文段後就立即發出重複確認(爲的是使發送方及早知道有報文段沒有到達對方),而不要等到自己發送數據時捎帶確認。快重傳算法規定,發送方只要一連收到三個重複確認就應當立即重傳對方尚未收到的報文段,而不必繼續等待設置的重傳計時器時間到期。由於不需要等待設置的重傳計時器到期,能儘早重傳未被確認的報文段,能提高整個網絡的吞吐量。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"4.2.4 快速恢復","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當發送方連續收到三個重複確認時,就執行“乘法減小”算法,把 ssthresh 門限減半。 但是接下去並不執行慢開始算法。考慮到如果網絡出現擁塞的話就不會收到好幾個重複的確認,所以發送方現在認爲網絡可能沒有出現擁塞。所以此時不執行慢開始算法,而是將 cwnd 設置爲 ssthresh 的大小, 然後執行擁塞避免算法。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"五、TCP 粘包與拆包","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"5.1 TCP 粘包和拆包的原因","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們知道 TCP 是以字節流的方式傳輸數據,傳輸的最小單位爲一個報文段(segment)。TCP 首部 中有個選項 (Options)的字段,常見的選項爲 MSS (Maximum Segment Size最大消息長度),它是收發雙方協商通信時每一個報文段所能承載的最大有效數據的長度。數據鏈路層每次傳輸的數據有個最大限制MTU (Maximum Transmission Unit),一般是1500比特,超過這個量要分成多個報文段,MSS 則是這個最大限制減去 TCP 的首部,光是要傳輸的數據的大小,一般爲1460比特。換算成字節,也就是180多字節。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"MSS = MTU - Header","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"TCP 爲提高性能,發送端會將需要發送的數據發送到發送緩存,等待緩存滿了之後,再將緩存中的數據發送到接收方。同理,接收方也有接收緩存這樣的機制,來接收數據。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面這些是發生 TCP 粘包和拆包的前提,下面是具體的原因:","attrs":{}}]},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"要發送的數據大於TCP發送緩衝區剩餘空間大小,將會發生拆包。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"待發送數據大於MSS(最大報文長度),TCP在傳輸前將進行拆包。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"應用程序寫入數據小於剩餘緩存大小,網卡將應用多次寫入的數據先緩存起來,然後一起發送到網絡上,這將會發生粘包。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"接收數據端的應用層沒有及時讀取接收緩存中的數據,將發生粘包。","attrs":{}}]}]}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"5.2 TCP 粘包和拆包的解決方案","attrs":{}}]},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"設置定長消息,服務端每次讀取既定長度的內容作爲一條完整消息。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"設置消息邊界,數據結尾尾增加特殊字符分割。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"使用帶消息頭的協議,消息頭存儲消息開始標識及消息長度信息,接收方獲取消息頭的時候解析出消息長度,然後向後讀取該長度的內容。","attrs":{}}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果您覺得文章對您有用的話 請轉發點贊支持一波","attrs":{}}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章