一、互聯網和傳輸層協議
01 | 互聯網的構成
1.1 | 網絡的組成
整個世界可以看作一張巨大的、立體的網。可以想象,用無數個節點構成一個個小型網絡,再用小型網絡組成中型網絡,再組成大型網絡,以此類推,最后組成完整的一個如星河般的世界。
1.2 | 公司內網
公司網絡從本地網絡服務提供商 (Internet Service Provider) 接入,然后內部再分成一個個子網。上圖中看到的線路,也被稱作通信鏈路(Communication Link),用於傳輸網絡信號。你可以觀察到,有的網絡節點,同時接入了 2 條以上的鏈路,這個時候因為路徑發生了分叉,數據傳輸到這些節點需要選擇方向,因此我們在這些節點需要進行交換(Switch)。數據發生交換的時候,會先從一條鏈路進入交換設備,然后緩存下來,再轉發(切換)到另一條路徑,如下圖所示:
交換技術的本質,就是讓數據切換路徑。因為,網絡中的數據是以分組或封包(Packet)的形式傳輸,因此這個技術也稱作封包交換技術(Packet Switch)。
比如要傳遞一首 2Mb 的 MP3 的歌曲,歌曲本身不是一次傳輸完成的,而是被拆分成很多個封包。每個封包只有歌曲中的一部分數據,而封包一旦遇到岔路口,就需要封包交換技術幫助每個封包選擇最合理的路徑。
網絡中,常見的具有交換能力的設備是路由器(Router)和鏈路層交換機(Link-Layer Switch)。通常情況下,兩個網絡可以通過路由器進行連接,多台設備可以通過交換機進行連接。但是路由器通常也具有交換機的功能。
1.3 | 移動網絡
網絡傳輸需要通信鏈路(Communication Link),而通信鏈路是一個抽象概念。
這里說的抽象,就是面向對象中抽象類和繼承類的關系,比如同軸電纜是通信鏈路,無線信號的發送接收器可以構成通信鏈路,藍牙信道也可以構成通信鏈路。
移動網絡中,無線信號構成了通信鏈路。在移動網絡的設計中,通信的核心被稱作蜂窩塔(Cellular Tower),有時候也稱作基站(BaseStation)。之所以有這樣的名稱,是因為每個蜂窩塔只覆蓋一個六邊形的范圍,如果要覆蓋一個很大的區域就需要很多的蜂窩塔(六邊形)排列在一起,像極了蜜蜂的巢穴。這種六邊形的結構,可以讓信號無死角地覆蓋。想象一下,如果是圓形結構,那么圓和圓之間就會有間隙,造成一部分無法覆蓋的信號死角,而六邊形就完美地解決了這個問題。
對於構成移動網絡最小的網絡結構——蜂窩網絡來說
國家或全球網絡提供商將網絡供給處於蜂窩網絡邊緣的路由器,路由器連接蜂窩塔,再通過蜂窩塔(基站)提供給處於六邊形地區中的設備。通常是國家級別的網絡服務提供商負責部署基站,比如中國電信、中國聯通。將網絡提供給一個子網的行為,通常稱為網絡提供(Network Provider),反過來,對一個子網連接提供商的網絡,稱為網絡接入(Network Access)。
隨着移動網絡的發展,一個蜂窩網格中的設備越來越多,也出現了基站覆蓋有重疊關系的網格
這樣設計的好處是,當一個基站過載、出現故障,或者用戶設備周邊信號出現不穩定,就可以切換到另一個基站的網絡,不影響用戶繼續使用網絡服務。
另一方面,在一定范圍內的區域,離用戶較近的地方還可以部署服務器,幫助用戶完成計算。這相當於計算資源的下沉,稱為邊緣計算。相比中心化的計算,邊緣計算延遲低、鏈路短,能夠將更好的體驗帶給距離邊緣計算集群最近的節點。從而讓用戶享受到更優質、延遲更低、算力更強的服務。
1.4 | 家用網絡
家用網絡現在已經發展成一種網格狀的連接。一方面家用網絡會通過路由器接入本地 ISP 提供的網絡服務。另一方面,一些設備,比如電腦、筆記本、手機、冰箱等都可以直接和路由器連接。路由器也承擔了鏈路層網關的作用,作為家用電器之間信息的交換設備。
還有一些家用設備,比如說 10 多塊錢的燈泡,不太適合內部再嵌入一個幾十塊錢可以接收 WI-FI 的芯片,這個時候就可以考慮用藍牙控制電燈。路由器提供藍牙不現實,因此一些家用電器也承擔了藍牙設備的控制器——比如說智能音箱。上圖中的智能音箱把家用網絡帶向了一個網格狀,有的設備會同時連接路由器(WI-FI)和智能音箱,這樣手機和音箱都可以直接控制這些設備。這樣的設計,即便網絡斷開,仍然可以控制這些家用設備。
1.5 | 整體關系
最頂部的全球或國家大型的 ISP 之間聯網,構成了網絡的主干。然后區域性的 ISP 承接主干網絡,在這個基礎之上再向家庭和公司提供接入服務。移動蜂窩網絡因為部署復雜,往往也是由大型 ISP 直接提供。
1.6 | 數據的傳輸
為了傳遞數據,在網絡中有幾個特別重要的抽象。
- 最終提供服務或者享受服務的設備,稱為終端(Terminal),或者端系統(End System),有時候簡單稱為主機(Host)
- 可以把網絡傳輸分成兩類,一類是端到端(Host-to-Host)的能力,由 TCP/IP 協議群提供。還有一類是廣播的能力,是一對多、多對多的能力,可以看作是端到端(Host-to-Host)能力的延伸。
比如說:電腦、手機、冰箱、汽車等,我們都可以看作是一個主機(Host)。
可以思考一下,一個北京的主機(Host)向一個深圳的主機(Host)發送消息。那么,中間會穿越大量的網絡節點,這些節點可以是路由器、交換機、基站等。在發送消息的過程中,可能跨越很多網絡、通過很多邊緣,也可能會通過不同的網絡提供商提供的網絡……而且,傳輸過程中,可能會使用不同材質的通信鏈路(Communication Link),比如同軸電纜、雙絞線、光纖,或者通過無線傳輸的 WI-FI、衛星等。
網絡基礎設施往往不能一次性傳輸太大的數據量,因此通常會將數據分片傳輸。
比如傳輸一個 MP3,我們會將 MP3 內容切分成很多個組,每個組也稱作一個封包,英文都是 Packet。這樣,如果一個封包損壞,只需要重發損壞的封包,而不需要重發所有數據。
另一方面,網絡中兩點間的路徑非常多,如果一條路徑阻塞了,部分封包可以考慮走其他路徑。發送端將數據拆分成封包(Packet),封包在網絡中遇到岔路,由交換器和路由器節點決定走向.下圖是對封包交換技術的一個演示。

[========]
02 | 傳輸層協議TCP
TCP 和 UDP 是今天應用最廣泛的傳輸層協議,擁有最核心的壟斷地位。今天互聯網的整個傳輸層,幾乎都是基於這兩個協議打造的。無論是應用開發、框架設計選型、做底層和優化,還是定位線上問題,只要碰到網絡,就逃不開 TCP 協議相關的知識。
2.1 | TCP協議
TCP(Transport Control Protocol)是一個傳輸層協議,提供 Host-To-Host 數據的可靠傳輸,支持全雙工,是一個連接導向的協議
2.2 | 主機到主機(Host-To-Host)
TCP 提供的是 Host-To-Host 傳輸,一台主機通過 TCP 發送數據給另一台主機。
這里的主機(Host)是一個抽象的概念,可以是手機、平板、手表等。收發數據的設備都是主機,所以雙方是平等的。
TCP 協議往上是應用到應用(Application-To-Application)的協議。
什么是應用到應用的協議呢?比如你用微信發信息給張三,你的微信客戶端、微信聊天服務都是應用。微信有自己的聊天協議,微信的聊天協議是應用到應用的協議;如果微信的聊天協議想要工作,就需要一個主機到主機的協議幫助它實現通信。
而 TCP 上層有太多的應用,不僅僅有微信,還有原神、抖音、網易雲音樂……因此 TCP 上層的應用層協議使用 TCP 能力的時候,需要告知 TCP 是哪個應用——這就是端口號。端口號用於區分應用.
TCP 要實現主機到主機通信,就需要知道主機們的網絡地址(IP 地址),但是 TCP 不負責實際地址到地址(Address-To-Address)的傳輸,因此 TCP 協議把 IP 地址給底層的互聯網層處理。
互聯網層,也叫網絡層(Network Layer),提供地址到地址的通信,IP 協議就在這一層工作。互聯網層解決地址到地址的通信,但是不負責信號在具體兩個設備間傳遞。因此,網絡層會調用下方的鏈路層在兩個相鄰設備間傳遞信息。當信號在兩個設備間傳遞的時候,科學家又設計出了物理層封裝最底層的物理設備、傳輸介質等,由最下方的物理層提供最底層的傳輸能力。
以上的 5 層架構,我們稱為互聯網協議群,也稱作 TCP/IP 協議群。主機到主機(Host-To-Host)為應用提供應用間通信的能力。
2.3 | 連接和會話
連接(Connection)——連接是數據傳輸雙方的契約。
連接是通信雙方的一個約定,目標是讓兩個在通信的程序之間產生一個默契,保證兩個程序都在線,而且盡快地響應對方的請求,這就是連接(Connection)。
設計上,連接是一種傳輸數據的行為。傳輸之前,建立一個連接。具體來說,數據收發雙方的內存中都建立一個用於維護數據傳輸狀態的對象,比如雙方 IP 和端口是多少?現在發送了多少數據了?狀態健康嗎?傳輸速度如何?
所以,連接是網絡行為狀態的記錄。
與連接相關的是會話(Session),會話是應用的行為。
比如微信里張三和你聊天,那么張三和你建立一個會話。你要和張三聊天,你們創建一個聊天窗口,這個就是會話。你開始 Typing,開始傳輸數據,你和微信服務器間建立一個連接。如果你們聊一段時間,各自休息了,約定先不要關微信,1 個小時后再回來。那么連接會斷開,因為聊天窗口沒關,所以會話還在。
有些系統設計中,會話會自動重連(也就是重新創建連接),或者幫助創建連接。 此外,會話也負責在多次連接中保存狀態,比如 HTTP Session 在多次 HTTP 請求(連接)間保持狀態(如用戶信息)。
總結下,會話是應用層的概念,連接是傳輸層的概念。
2.4 | 雙工/單工問題
在任何一個時刻,如果數據只能單向發送,就是單工,所以單工需要至少一條線路。如果在某個時刻數據可以向一個方向傳輸,也可以向另一個方向反方向傳輸,而且交替進行,叫作半雙工;半雙工需要至少 1 條線路。最后,如果任何時刻數據都可以雙向收發,這就是全雙工,全雙工需要大於 1 條線路。當然這里的線路,是一個抽象概念,你可以並發地處理信號,達到模擬雙工的目的。
TCP 是一個雙工協議,數據任何時候都可以雙向傳輸。這就意味着客戶端和服務端可以平等地發送、接收信息。正因為如此,客戶端和服務端在 TCP 協議中有一個平等的名詞——Host(主機)。
2.5 | 可靠性
可靠性指數據保證無損傳輸。如果發送方按照順序發送,然后數據無序地在網絡間傳遞,就必須有一種算法在接收方將數據恢復原有的順序。另外,如果發送方同時要把消息發送給多個接收方,這種情況叫作多播,可靠性要求每個接收方都無損收到相同的副本。多播情況還有強可靠性,就是如果有一個消息到達任何一個接收者,那么所有接受者都必須收到這個消息。
2.6 | TCP的握手和揮手
TCP 是一個連接導向的協議,設計有建立連接(握手)和斷開連接(揮手)的過程。TCP 沒有設計會話(Session),因為會話通常是一個應用的行為。
2.7 | TCP協議的基本操作
TCP 協議有這樣幾個基本操作:
- 如果一個 Host 主動向另一個 Host 發起連接,稱為 SYN(Synchronization),請求同步;
- 如果一個 Host 主動斷開請求,稱為 FIN(Finish),請求完成;
- 如果一個 Host 給另一個 Host 發送數據,稱為 PSH(Push),數據推送。
以上 3 種情況,接收方收到數據后,都需要給發送方一個 ACK(Acknowledgement)響應。
請求/響應的模型是可靠性的要求,如果一個請求沒有響應,發送方可能會認為自己需要重發這個請求。
2.8 | 建立連接的過程(3次握手)
因為要保持連接和可靠性約束,TCP 協議要保證每一條發出的數據必須給返回,返回數據叫作 ACK(也就是響應)。
- 客戶端發消息給服務端(SYN)
- 服務端准備好進行連接
- 服務端針對客戶端的 SYN 給一個 ACK
你可能覺得,2 次握手就足夠了。但其實不是,因為服務端還沒有確定客戶端是否准備好了。比如步驟 3 之后,服務端馬上給客戶端發送數據,這個時候客戶端可能還沒有准備好接收數據。因此還需要增加一個過程。
接下來還會發生以下操作:
- 服務端發送一個 SYN 給客戶端
- 客戶端准備就緒
- 客戶端給服務端發送一個 ACK
雖然是6步操作,但其實是三次握手:
- 步驟 1 是 1 次握手;
- 步驟 2 是服務端的准備,不是數據傳輸,因此不算握手;
- 步驟 3 和步驟 4,因為是同時發生的,可以合並成一個 SYN-ACK 響應,作為一條數據傳遞給客戶端,因此是第 2 次握手;
- 步驟 5 不算握手;
- 步驟 6 是第 3 次握手。
為了方便理解步驟 3 和步驟 4,這里我畫了一張圖。可以看到下圖中 SYN 和 ACK 被合並了,因此建立連接一共需要 3 次握手
2.9 | 斷開連接的過程(4次揮手)
- 客戶端要求斷開連接,發送一個斷開的請求,這個叫作(FIN)。
- 服務端收到請求,然后給客戶端一個 ACK,作為 FIN 的響應。
- 這里你需要思考一個問題,可不可以像握手那樣馬上傳 FIN 回去?
其實這個時候服務端不能馬上傳 FIN,因為斷開連接要處理的問題比較多,比如說服務端可能還有發送出去的消息沒有得到 ACK;也有可能服務端自己有資源要釋放。因此斷開連接不能像握手那樣操作——將兩條消息合並。所以,服務端經過一個等待,確定可以關閉連接了,再發一條 FIN 給客戶端。 - 客戶端收到服務端的 FIN,同時客戶端也可能有自己的事情需要處理完,比如客戶端有發送給服務端沒有收到 ACK 的請求,客戶端自己處理完成后,再給服務端發送一個 ACK。
2.10 | 總結
在學習 3 次握手、4 次揮手時,一定要理解為什么這么設計。
- TCP 提供連接(Connection),讓雙方的傳輸更加穩定、安全。
- TCP 沒有直接提供會話,因為應用對會話的需求多種多樣,比如聊天程序會話在保持雙方的聊天記錄,電商程序會話在保持購物車、訂單一致,所以會話通常在 TCP 連接上進一步封裝,在應用層提供。
- TCP 是一個面向連接的協議(Connection -oriented Protocol),說的就是 TCP 協議參與的雙方(Host)在收發數據之前會先建立連接。后面我們還會學習 UDP 協議,UDP 是一個面向報文(Datagram-oriented)的協議——協議雙方不需要建立連接,直接傳送報文(數據)。
- 最后,連接需要消耗更多的資源。比如說,在傳輸數據前,必須先協商建立連接。因此,不是每種場景都應該用連接導向的協議。像視頻播放的場景,如果使用連接導向的協議,服務端每向客戶端推送一幀視頻,客戶端都要給服務端一次響應,這是不合理的。
最后:TCP 為什么是 3 次握手,4 次揮手?
【解析】TCP 是一個雙工協議,為了讓雙方都保證,建立連接的時候,連接雙方都需要向對方發送 SYC(同步請求)和 ACK(響應)。
握手階段雙方都沒有煩瑣的工作,因此一方向另一方發起同步(SYN)之后,另一方可以將自己的 ACK 和 SYN 打包作為一條消息回復,因此是 3 次握手——需要 3 次數據傳輸。
到了揮手階段,雙方都可能有未完成的工作。收到揮手請求的一方,必須馬上響應(ACK),表示接收到了揮手請求。類比現實世界中,你收到一個 Offer,出於禮貌你先回復考慮一下,然后思考一段時間再回復 HR 最后的結果。最后等所有工作結束,再發送請求中斷連接(FIN),因此是 4 次揮手。
[========]
03 | TCP封包格式
本節從穩定性角度深挖 TCP 協議的運作機制。
簡單來說,可靠性就是讓數據無損送達。但若是考慮到成本,就會變得非常復雜——因為還需要盡可能地提升吞吐量、降低延遲、減少丟包率。
TCP 協議具有很強的實用性,而可靠性又是 TCP 最核心的能力,所以理所當然成為面試官們津津樂道的問題。具體來說,從一個終端有序地發出多個數據包,經過一個復雜的網絡環境,到達目的地的時候會變得無序,而可靠性要求數據恢復到原始的順序。這里先提出兩個問題:
- TCP 協議是如何恢復數據的順序的?
- 拆包和粘包的作用是什么?
3.1 | TCP的拆包和粘包
TCP 發送數據的時候,往往不會將數據一次性發送;而是將數據拆分成很多個部分,然后再逐個發送。像下圖這樣:
同樣的,在目的地,TCP 協議又需要逐個接收數據。
TCP之所以不一次性發送完所有數據,有很多原因:
- 為了穩定性,一次發送的數據越多,出錯的概率越大。
- 為了效率,網絡中有時候存在着並行的路徑,拆分數據包就能更好地利用這些並行的路徑。
- 發送和接收數據的時候,都存在着緩沖區。
- 如果數據的大小超過一個頁表,可能會存在頁面置換問題,造成性能的損失。
緩沖區是在內存中開辟的一塊區域,目的是緩沖。因為大量的應用頻繁地通過網卡收發數據,這個時候,網卡只能一個一個處理應用的請求。當網卡忙不過來的時候,數據就需要排隊,也就是將數據放入緩沖區。如果每個應用都隨意發送很大的數據,可能導致其他應用實時性遭到破壞。
總之,方方面面的原因:在傳輸層封包不能太大。這種限制,往往是以緩沖區大小為單位的。
也就是 TCP 協議,會將數據拆分成不超過緩沖區大小的一個個部分。每個部分有一個獨特的名詞,叫作 TCP 段(TCP Segment)。
在接收數據的時候,一個個 TCP 段又被重組成原來的數據。
- 拆包:數據經過拆分,然后傳輸,然后在目的地重組。拆包是將數據拆分成多個 TCP 段傳輸。
- 粘包:將多個數據合並成一個 TCP 段發送。如果發往一個目的地的多個數據太小了,為了防止多次發送占用資源,TCP 協議有可能將它們合並成一個 TCP 段發送,在目的地再還原成多個數據。
3.2 | TCP Segment
TCP段格式如下:
如圖所示,TCP 的很多配置選項和數據粘在了一起,作為一個 TCP 段。
TCP 協議就是依靠每一個 TCP 段工作的,所以每認識一個 TCP 的能力,幾乎都會找到在 TCP Segment 中與之對應的字段。
- Source Port/Destination Port 描述的是發送端口號和目標端口號,代表發送數據的應用程序和接收數據的應用程序。比如 80 往往代表 HTTP 服務,22 往往是 SSH 服務……
- Sequence Number 和 Achnowledgment Number 是保證可靠性的兩個關鍵。具體見下文的討論。
- Data Offset 是一個偏移量。這個量存在的原因是 TCP Header 部分的長度是可變的,因此需要一個數值來描述數據從哪個字節開始。
- Reserved 是很多協議設計會保留的一個區域,用於日后擴展能力。
- URG/ACK/PSH/RST/SYN/FIN 是幾個標志位,用於描述 TCP 段的行為。也就是一個 TCP 封包到底是做什么用的
1)URG 代表這是一個緊急數據,比如遠程操作的時候,用戶按下了 Ctrl+C,要求終止程序,這種請求需要緊急處理。
2)ACK 代表響應,所有的消息都必須有 ACK,這是 TCP 協議確保穩定性的一環。
3)PSH 代表數據推送,也就是在傳輸數據的意思。
4)SYN 同步請求,也就是申請握手。
5)FIN 終止請求,也就是揮手。
特別說明:以上這 5 個標志位,每個占了一個比特,可以混合使用。比如 ACK 和 SYN 同時為 1,代表同步請求和響應被合並了。這也是 TCP 協議,為什么是三次握手的原因之一。
6) Window 也是 TCP 保證穩定性並進行流量控制的工具.
7)Checksum 是校驗和,用於校驗 TCP 段有沒有損壞。
8)Urgent Pointer 指向最后一個緊急數據的序號(Sequence Number)。它存在的原因是:有時候緊急數據是連續的很多個段,所以需要提前告訴接收方進行准備。
9)Options 中存儲了一些可選字段,比如接下來要討論的 MSS(Maximun Segment Size)。
10)Padding 存在的意義是因為 Options 的長度不固定,需要 Pading 進行對齊。
3.3 | Sequence Number 和 Acknowledge Number
這里思考一個問題:穩定性要求數據無損地傳輸,也就是說拆包獲得數據,又需要恢復到原來的樣子。而在復雜的網絡環境當中,即便所有的段是順序發出的,也不能保證它們順序到達,因此,發出的每一個 TCP 段都需要有序號。這個序號,就是 Sequence Number(Seq)。
- 發送數據的時候,為每一個 TCP 段分配一個自增的 Sequence Number。
- 接收數據的時候,雖然得到的是亂序的 TCP 段,但是可以通過 Seq 進行排序。
但是這樣又會產生一個新的問題——接收方如果要回復發送方,也需要這個 Seq。而網絡的兩個終端,去同步一個自增的序號是非常困難的。因為任何兩個網絡主體間,時間都不能做到完全同步,又沒有公共的存儲空間,無法共享數據,更別說實現一個分布式的自增序號了。
其實這個問題的本質就好像兩個人在說話一樣,我們要確保他們說出去的話,和回答之間的順序。因為 TCP 是一個雙工的協議,兩邊可能會同時說話。所以聰明的科學家想到了確定一句話的順序,需要兩個值去描述——也就是發送的字節數和接收的字節數。
於是重新定義Seq:對於任何一個接收方,如果知道了發送者發送某個 TCP 段時,已經發送了多少字節的數據,那么就可以確定發送者發送數據的順序。
但是這里有一個問題。如果接收方也向發送者發送了數據請求(或者說雙方在對話),接收方就不知道發送者發送的數據到底對應哪一條自己發送的數據了。
舉個例子:下面 A 和 B 的對話中,我們可以確定他們彼此之間接收數據的順序。但是無法確定數據之間的關聯關系,所以只有 Sequence Number 是不夠的。
A:今天天氣好嗎?
A:今天你開心嗎?
B:開心
B:天氣不好
人類很容易理解這幾句話的順序,但是對於機器來說就需要特別的標注。因此我們還需要另一個數據,就是每個 TCP 段發送時,發送方已經接收了多少數據。用 Acknowledgement Number 表示,簡寫為 ACK。
舉例:終端發送了三條數據,並且接收到四條數據,通過觀察,根據接收到的數據中的 Seq 和 ACK,將發送和接收的數據進行排序。
上圖中,發送方發送了 100 字節的數據,而接收到的(Seq = 0 和 Seq =100)的兩個封包,都是針對發送方(Seq = 0)這個封包的。發送 100 個字節,所以接收到的 ACK 剛好是 100。說明(Seq= 0 和 Seq= 100)這兩個封包是針對接收到第 100 個字節數據后,發送回來的。這樣就確定了整體的順序。
注:無論 Seq 還是 ACK,都是針對“對方”而言的。是對方發送的數據和對方接收到的數據。
3.4 | MSS(Maximum Segment Size)
MSS,也是面試經常會問到的一個 TCP Header 中的可選項(Options),這個可選項控制了 TCP 段的大小,它是一個協商字段(Negotiate)。協議是雙方都要遵循的標准,因此配置往往不能由單方決定,需要雙方協商。
TCP 段的大小(MSS)涉及發送、接收緩沖區的大小設置,雙方實際發送接收封包的大小,對拆包和粘包的過程有指導作用,因此需要雙方去協商。
如果MSS設置得非常大,就會帶來一些影響。
- 對方可能會拒絕。作為服務的提供方,你可能不會願意接收太大的 TCP 段。因為大的 TCP 段,會降低性能,比如內存使用的性能。另外,一個用戶占用服務器太多的資源,意味着其他的用戶就需要等待或者降低他們的服務質量。
- 支持 TCP 協議工作的 IP 協議,工作效率會下降。TCP 協議不肯拆包,IP 協議就需要拆出大量的包。那么 IP 協議為什么需要拆包呢?這是因為在網絡中,每次能夠傳輸的數據不可能太大,這受限於具體的網絡傳輸設備,也就是物理特性。但是 IP 協議,拆分太多的封包並沒有意義。因為可能會導致屬於同個 TCP 段的封包被不同的網絡路線傳輸,這會加大延遲。同時,拆包,還需要消耗硬件和計算資源。
MSS太小的情況下,會浪費傳輸資源(降低吞吐量)。因為數據被拆分之后,每一份數據都要增加一個頭部。如果 MSS 太小,那頭部的數據占比會上升,這讓吞吐量成為一個災難。所以在使用的過程當中,MSS 的配置,往往都是一個折中的方案。
最后,TCP 協議是如何恢復數據的順序的,TCP 拆包和粘包的作用是什么?
【解析】TCP 拆包的作用是將任務拆分處理,降低整體任務出錯的概率,以及減小底層網絡處理的壓力。拆包過程需要保證數據經過網絡的傳輸,又能恢復到原始的順序。這中間,需要數學提供保證順序的理論依據。TCP 利用(發送字節數、接收字節數)的唯一性來確定封包之間的順序關系。
[========]
04 | TCP穩定性:滑動窗口與流量控制
TCP 作為一個傳輸層協議,最核心的能力是傳輸。傳輸需要保證可靠性,還需要控制流速,這兩個核心能力均由滑動窗口提供。
4.1 | 請求/響應模型
TCP 中每個發送的請求都需要響應。如果一個請求沒有收到響應,發送方就會認為這次發送出現了故障,會觸發重發。
大體的模型,和下圖很像。但是如果完全和下圖一樣,每一個請求收到響應之后,再發送下一個請求,吞吐量會很低。因為這樣的設計,會產生網絡的空閑時間,說白了,就是浪費帶寬。帶寬沒有用滿,意味着可以同時發送更多的請求,接收更多的響應。
一種改進的方式,就是讓發送方有請求就發送出去,而不是等待響應。通過這樣的處理方式,發送的數據連在了一起,響應的數據也連在了一起,吞吐量就提升了。
但是如果可以同時發送的數據真的非常多呢?比如成百上千個 TCP 段都需要發送,這個時候帶寬可能會不足。像下圖這樣,很多個數據封包都需要發送,該如何處理呢?
4.3 | 排隊模型
此種情況下,通常我們會考慮排隊(Queuing)機制。
考慮這樣一個模型,如上圖所示,在 TCP 層實現一個隊列。新元素從隊列的一端(左側)排隊,作為一個未發送的數據封包。開始發送的數據封包,從隊列的右側離開。
這樣做就需要多個隊列,我們要將未發送的數據從隊列中取出,加入發送中的隊列。然后再將發送中的數據,收到 ACK 的部分取出,放入已接收的隊列。而發送中的封包,何時收到 ACK 是一件不確定的事情,這樣使用隊列似乎也有一定的問題。
4.4 | 滑動窗口(Sliding Window)
這里其實應該用一種叫作滑動窗口的數據結構去實現
如上圖所示:
- 深綠色代表已經收到 ACK 的段
- 淺綠色代表發送了,但是沒有收到 ACK 的段
- 白色代表沒有發送的段
- 紫色代表暫時不能發送的段
下面重新設計一不同類型封包的順序,將已發送的數據放到最左邊,發送中的數據放到中間,未發送的數據放到右邊。假設我們最多同時發送 5 個封包,也就是窗口大小 = 5。窗口中的數據被同時發送出去,然后等待 ACK。如果一個封包 ACK 到達,我們就將它標記為已接收(深綠色)。
如下圖所示,有兩個封包的 ACK 到達,因此標記為綠色。
這個時候滑動窗口可以向右滑動
4.5 | 重傳
發送過程中,部分數據沒能收到 ACK 時可能發生重傳。
如果發生下圖這樣的情況,段 4 遲遲沒有收到 ACK。
這個時候滑動窗口只能右移一個位置,
這個過程中,如果后來段 4 重傳成功(接收到 ACK),那么窗口就會繼續右移。如果段 4 發送失敗,還是沒能收到 ACK,那么接收方也會拋棄段 5、段 6、段 7。這樣從段 4 開始之后的數據都需要重發。
4.6 | 快速重傳
在 TCP 協議中,如果接收方想丟棄某個段,可以選擇不發 ACK。發送端超時后,會重發這個 TCP 段。而有時候,接收方希望催促發送方盡快補發某個 TCP 段,這個時候可以使用快速重傳能力。
例如段 1、段 2、段 4 到了,但是段 3 沒有到。 接收方可以發送多次段 3 的 ACK。如果發送方收到多個段 3 的 ACK,就會重發段 3。這個機制稱為快速重傳。這和超時重發不同,是一種催促的機制。
為了不讓發送方誤以為段 3 已經收到了,在快速重傳的情況下,接收方即便收到發來的段 4,依然會發段 3 的 ACK(不發段 4 的 ACK),直到發送方把段 3 重傳。
窗口大小的單位是多少呢?在上面所有的圖片中,窗口大小是 TCP 段的數量。實際操作中,每個 TCP 段的大小不同,限制數量會讓接收方的緩沖區不好操作,因此實際操作中窗口大小單位是字節數。
4.7 | 流速控制
發送、接收窗口的大小可以用來控制 TCP 協議的流速。
窗口越大,同時可以發送、接收的數據就越多,支持的吞吐量也就越大。當然,窗口越大,如果數據發生錯誤,損失也就越大,因為需要重傳越多的數據。
舉個例子:我們用 RTT 表示 Round Trip Time,就是消息一去一回的時間。
假設 RTT = 1ms,帶寬是 1mb/s。如果窗口大小為 1kb,那么 1ms 可以發送一個 1kb 的數據(含 TCP 頭),1s 就可以發送 1mb 的數據,剛好可以將帶寬用滿。如果 RTT 再慢一些,比如 RTT = 10ms,那么這樣的設計就只能用完 1/10 的帶寬。 當然你可以提高窗口大小提高吞吐量,但是實際的模型會比這個復雜,因為還存在重傳、快速重傳、丟包等因素。
而實際操作中,也不可以真的把帶寬用完,所以最終我們會使用折中的方案,在延遲、丟包率、吞吐量中進行選擇,畢竟魚和熊掌不可兼得。
4.8 | 總結
為了提高傳輸速率,TCP 協議選擇將多個段同時發送,為了讓這些段不至於被接收方拒絕服務,在發送前,雙方要協商好發送的速率。但是我們不可能完全確定網速,所以協商的方式,就變成確定窗口大小。
有了窗口,發送方利用滑動窗口算法發送消息;接收方構造緩沖區接收消息,並給發送方 ACK。滑動窗口的實現只需要數組和少量的指針即可,是一個非常高效的算法。像這種算法,簡單又實用,比如求一個數組中最大的連續 k 項和,就可以使用滑動窗口算法。如果你對這個問題感興趣,不妨用你最熟悉的語言嘗試解決一下。
最后,滑動窗口和流速控制是怎么回事?
【解析】滑動窗口是 TCP 協議控制可靠性的核心。發送方將數據拆包,變成多個分組。然后將數據放入一個擁有滑動窗口的數組,依次發出,仍然遵循先入先出(FIFO)的順序,但是窗口中的分組會一次性發送。窗口中序號最小的分組如果收到 ACK,窗口就會發生滑動;如果最小序號的分組長時間沒有收到 ACK,就會觸發整個窗口的數據重新發送。
另一方面,在多次傳輸中,網絡的平均延遲往往是相對固定的,這樣 TCP 協議可以通過雙方協商窗口大小控制流速。補充下,上面我們說的分組和 TCP 段是一個意思。
05 | UDP 協議
TCP 和 UDP 是今天應用最廣泛的傳輸層協議,擁有最核心的壟斷地位。TCP 最核心的價值是提供了可靠性,而 UDP 最核心的價值是靈活,你幾乎可以用它來做任何事情。例如:HTTP 協議 1.1 和 2.0 都基於 TCP,而到了 HTTP 3.0 就開始用 UDP 了。
5.1 | UDP協議
UDP(User Datagram Protocol),目標是在傳輸層提供直接發送報文(Datagram)的能力。
- Datagram 是數據傳輸的最小單位。
- UDP 協議不會幫助拆分數據,它的目標只有一個,就是發送報文。
為什么不直接調用 IP 協議呢? 如果裸發數據,IP 協議不香嗎?
這是因為傳輸層協議在承接上方應用層的調用,需要提供應用到應用的通信——因此要附上端口號。每個端口,代表不同的應用。傳輸層下層的 IP 協議,承接傳輸層的調用,將數據從主機傳輸到主機。IP 層不能區分應用,導致哪怕是在 IP 協議上進行簡單封裝,也需要單獨一個協議。這就構成了 UDP 協議的市場空間。
5.2 | UDP的封包格式
設計目標:就是在允許用戶直接發送報文的情況下,最大限度地簡化應用的設計。
下圖是 UDP 的報文格式。
UDP 的報文非常簡化,只有 5 個部分:
- Source Port 是源端口號。因為 UDP 協議的特性(不需要 ACK),因此這個字段是可以省略的。但有時候對於防火牆、代理來說,Source Port 有很重要的意義,它們需要用這個字段行過濾和路由。
- Destination Port 是目標端口號(這個字段不可以省略)。
- Length 是消息體長度。
- Checksum 是校驗和,作用是檢查封包是否出錯。
- Data octets 就是一個字節一個字節的數據,Octet 是 8 位。
其中,校驗和(Checksum)機制,在很多的網絡協議中都會存在,因為校驗數據在傳輸過程中有沒有丟失、損壞是一個普遍需求。在一次網絡會話中,我們傳輸的內容可能是:“你好!”,但事實上傳輸的是 01 組成的二進制。請你思考這樣一個算法,我們把數據分成一個一個 byte,然后將所有 byte 相加,再將最終的結果取反。
比如現在數據有 4 個 byte:a,b,c,d,那么一種最簡單的校驗和就是:
checksum=(a+b+c+d) ^ 0xff
如果發送方用上述方式計算出 Checksum,並將 a,b,c,d 和 Checksum 一起發送給接收方,接收方就可以用同樣的算法再計算一遍,這樣就可以確定數據有沒有發生損壞(變化)。
當然 Checksum 的做法,只適用於數據發生少量變化的情況。如果數據發生較大的變動,校驗和也可能發生碰撞。
UDP 的可靠性保證僅僅就是 Checksum 一種。
如果一個數據封包 Datagram 發生了數據損壞,UDP 可以通過 Checksum 糾錯或者修復。 但是 UDP 沒有提供再多的任何機制,比如 ACK、順序保證以及流控等。
5.3 | UDP與TCP的區別
-
目的差異
- TCP 協議的核心目標是提供可靠的網絡傳輸
- UDP 的目標是在提供報文交換能力基礎上盡可能地簡化協議輕裝上陣。
-
可靠性差異
TCP 核心是要在保證可靠性基礎上提供更好的服務。TCP 會有握手的過程,需要建立連接,保證雙方同時在線。而且TCP 有時間窗口持續收集無序的數據,直到這一批數據都可以合理地排序組成連續的結果。
UDP 並不具備以上這些特性,它只管發送數據封包,而且 UDP 不需要 ACK,這意味着消息發送出去成功與否 UDP 是不管的。
-
連接 vs 無連接
- TCP 是一個面向連接的協議(Connection-oriented Protocol),傳輸數據必須先建立連接。
- UDP 是一個無連接協議(Connection-less Protocol),數據隨時都可以發送,只提供發送封包(Datagram)的能力。
-
流控技術(Flow Control)
TCP 使用了流控技術來確保發送方不會因為一次發送過多的數據包而使接收方不堪重負。TCP 在發送緩沖區中存儲數據,並在接收緩沖區中接收數據。當應用程序准備就緒時,它將從接收緩沖區讀取數據。如果接收緩沖區已滿,接收方將無法處理更多數據,並將其丟棄。UDP 沒有提供類似的能力。 -
傳輸速度
UDP 協議簡化,封包小,沒有連接、可靠性檢查等,因此單純從傳輸速度上講,UDP 更快。 -
場景差異
TCP 每個數據封包都需要確認,因此天然不適應高速數據傳輸場景,比如觀看視頻(流媒體應用)、網絡游戲(TCP 有延遲)等。具體來說,如果網絡游戲用 TCP,每個封包都需要確認,可能會造成一定的延遲;再比如音、視頻傳輸天生就允許一定的丟包率;Ping 和 DNSLookup,這類型的操作只需要一次簡單的請求/返回,不需要建立連接,用 UDP 就足夠了。
近些年有一個趨勢,TCP/UDP 的邊界逐漸變得模糊,UDP 應用越來越多。比如傳輸文件,如果考慮希望文件無損到達,可以用 TCP。如果考慮希望傳輸足夠塊,就可能會用 UDP。再比如 HTTP 協議,如果考慮請求/返回的可靠性,用 TCP 比較合適。但是像 HTTP 3.0 這類應用層協議,從功能性上思考,暫時沒有找到太多的優化點,但是想要把網絡優化到極致,就會用 UDP 作為底層技術,然后在 UDP 基礎上解決可靠性。
所以理論上,任何一個用 TCP 協議構造的成熟應用層協議,都可以用 UDP 重構。這就好比,本來用一個工具可以解決所有問題,但是如果某一類問題體量非常大,就會專門為這類問題創造工具。因此,UDP 非常適合需要定制工具的場景。
第一類:TCP場景
- 遠程控制(SSH)
- File Transfer Protocol(FTP)
- 郵件(SMTP、IMAP)等
- 點對點文件傳出(微信等
第二類:UDP 場景
- 網絡游戲
- 音視頻傳輸
- DNS
- Ping
- 直播
第三類:模糊地帶
- HTTP(目前以 TCP 為主)
- 文件傳輸
UDP 不提供可靠性,不代表我們不能解決可靠性。UDP 的核心價值是靈活、輕量,構造了最小版本的傳輸層協議。在這個之上,還可以實現連接(Connection),實現會話(Session),實現可靠性(Reliability)……
最后,TCP 協議和 UDP 協議的優勢和劣勢?
【解析】TCP 最核心的價值就是提供封裝好的一套解決可靠性的優秀方案。TCP 幫助我們在確保吞吐量、延遲、丟包率的基礎上,保證可靠性。
歷史上 TCP 也是靠可靠性起家的,有一次著名的實驗,TCP 協議的設計者做了一次演示——利用 TCP 協議將數據在衛星和地面之間傳播了很多次,沒有發生任何數據損壞。從那個時候開始,研發人員開始大量選擇 TCP 協議。然后隨着生態的發展,逐漸提供了流控等能力。TCP 的成功在於它給人們提供了很多現成、好用的能力。
UDP 則不同,UDP 提供了最小版的實現,只支持 Checksum。UDP 最核心的價值是靈活、輕量、傳輸速度快。考慮到不同應用的特性,如果不使用一個大而全的方案,為自己的應用特性量身定做,可能會做得更好。比如網絡游戲中游戲客戶端不斷向服務端發送玩家的位置,如果某一次消息丟失了,只要這個消息不影響最終的游戲結果,就可以只看下一個消息。不同應用有不同的特性,需要的可靠性級別不一樣,這就是越來越多的應用開始使用 UDP 的原因之一。
其實對於我們來說,TCP 協議和 UDP 協議根本不存在什么優勢和劣勢,只不過是場景不同,選擇不同而已。最后還有一個非常重要的考慮因素就是成本,如果沒有足夠專業的團隊解決網絡問題,TCP 無疑會是更好的選擇。
