40 張圖帶你搞懂 TCP 和 UDP

程式設計師cxuan發表於2020-11-30

前言

歡迎閱讀「程式設計師cxuan」 的文章,從今往後,你就是我的讀者了。

我的 github bestJavaer 已經收錄此文章,目錄在

https://github.com/crisxuan/bestJavaer

希望你可以給我一個 star 哦!

這一篇文章是計算機網路連載文章的第四篇,歷史文章請閱讀

一不小心畫了 24 張圖剖析計網應用層協議!

TCP/IP 基礎知識

計算機網路基礎知識總結

下面開始本篇文章。

運輸層位於應用層和網路層之間,是 OSI 分層體系中的第四層,同時也是網路體系結構的重要部分。運輸層主要負責網路上的端到端通訊。

40 張圖帶你搞懂 TCP 和 UDP

運輸層為執行在不同主機上的應用程式之間的通訊起著至關重要的作用。下面我們就來一起探討一下關於運輸層的協議部分

運輸層概述

計算機網路的運輸層非常類似於高速公路,高速公路負責把人或者物品從一端運送到另一端,而計算機網路的運輸層則負責把報文從一端運輸到另一端,這個端指的就是 端系統。在計算機網路中,任意一個可以交換資訊的介質都可以稱為端系統,比如手機、網路媒體、電腦、運營商等。

在運輸層運輸報文的過程中,會遵守一定的協議規範,比如一次傳輸的資料限制、選擇什麼樣的運輸協議等。運輸層實現了讓兩個互不相關的主機進行邏輯通訊的功能,看起來像是讓兩個主機相連一樣。

運輸層協議是在端系統中實現的,而不是在路由器中實現的。路由只是做識別地址並轉發的功能。這就比如快遞員送快遞一樣,當然是要由地址的接受人也就是 xxx 號樓 xxx 單元 xxx 室的這個人來判斷了!

TCP 如何判斷是哪個埠的呢?

還記得資料包的結構嗎,這裡來回顧一下

資料包經過每層後,該層協議都會在資料包附上包首部,一個完整的包首部圖如上所示。

在資料傳輸到運輸層後,會為其附上 TCP 首部,首部包含著源埠號和目的埠號。

在傳送端,運輸層將從傳送應用程式程式接收到的報文轉化成運輸層分組,分組在計算機網路中也稱為 報文段(segment)。運輸層一般會將報文段進行分割,分割成為較小的塊,為每一塊加上運輸層首部並將其向目的地傳送。

在傳送過程中,可選的運輸層協議(也就是交通工具) 主要有 TCPUDP ,關於這兩種運輸協議的選擇及其特性也是我們著重探討的重點。

TCP 和 UDP 前置知識

在 TCP/IP 協議中能夠實現傳輸層功能的,最具代表性的就是 TCP 和 UDP。提起 TCP 和 UDP ,就得先從這兩個協議的定義說起。

TCP 叫做傳輸控制協議(TCP,Transmission Control Protocol),通過名稱可以大致知道 TCP 協議有控制傳輸的功能,主要體現在其可控,可控就表示著可靠,確實是這樣的,TCP 為應用層提供了一種可靠的、面向連線的服務,它能夠將分組可靠的傳輸到服務端。

UDP 叫做 使用者資料包協議(UDP,User Datagram Protocol),通過名稱可以知道 UDP 把重點放在了資料包上,它為應用層提供了一種無需建立連線就可以直接傳送資料包的方法。

怎麼計算機網路中的術語對一個資料的描述這麼多啊?

在計算機網路中,在不同層之間會有不同的描述。我們上面提到會將運輸層的分組稱為報文段,除此之外,還會將 TCP 中的分組也稱為報文段,然而將 UDP 的分組稱為資料包,同時也將網路層的分組稱為資料包

但是為了統一,一般在計算機網路中我們統一稱 TCP 和 UDP 的報文為 報文段,這個就相當於是約定,到底如何稱呼不用過多糾結啦。

套接字

在 TCP 或者 UDP 傳送具體的報文資訊前,需要先經過一扇 ,這個門就是套接字(socket),套接字向上連線著應用層,向下連線著網路層。在作業系統中,作業系統分別為應用和硬體提供了介面(Application Programming Interface)。而在計算機網路中,套接字同樣是一種介面,它也是有介面 API 的。

使用 TCP 或 UDP 通訊時,會廣泛用到套接字的 API,使用這套 API 設定 IP 地址、埠號,實現資料的傳送和接收。

現在我們知道了, Socket 和 TCP/IP 沒有必然聯絡,Socket 的出現只是方便了 TCP/IP 的使用,如何方便使用呢?你可以直接使用下面 Socket API 的這些方法。

方法 描述
create() 建立一個 socket
bind() 套接字標識,一般用於繫結埠號
listen() 準備接收連線
connect() 準備充當傳送者
accept() 準備作為接收者
write() 傳送資料
read() 接收資料
close() 關閉連線

套接字型別

套接字的主要型別有三種,下面我們分別介紹一下

  • 資料包套接字(Datagram sockets):資料包套接字提供一種無連線的服務,而且並不能保證資料傳輸的可靠性。資料有可能在傳輸過程中丟失或出現資料重複,且無法保證順序地接收到資料。資料包套接字使用UDP( User DatagramProtocol)協議進行資料的傳輸。由於資料包套接字不能保證資料傳輸的可靠性,對於有可能出現的資料丟失情況,需要在程式中做相應的處理。
  • 流套接字(Stream sockets):流套接字用於提供面向連線、可靠的資料傳輸服務。能夠保證資料的可靠性、順序性。流套接字之所以能夠實現可靠的資料服務,原因在於其使用了傳輸控制協議,即 TCP(The Transmission Control Protocol)協議
  • 原始套接字(Raw sockets): 原始套接字允許直接傳送和接收 IP 資料包,而無需任何特定於協議的傳輸層格式,原始套接字可以讀寫核心沒有處理過的 IP 資料包。

套接字處理過程

在計算機網路中,要想實現通訊,必須至少需要兩個端系統,至少需要一對兩個套接字才行。下面是套接字的通訊過程。

40 張圖帶你搞懂 TCP 和 UDP
  1. socket 中的 API 用於建立通訊鏈路中的端點,建立完成後,會返回描述該套接字的套接字描述符

就像使用檔案描述符來訪問檔案一樣,套接字描述符用來訪問套接字。

  1. 當應用程式具有套接字描述符後,它可以將唯一的名稱繫結在套接字上,伺服器必須繫結一個名稱才能在網路中訪問
  2. 在為服務端分配了 socket 並且將名稱使用 bind 繫結到套接字上後,將會呼叫 listen api。listen 表示客戶端願意等待連線的意願,listen 必須在 accept api 之前呼叫。
  3. 客戶端應用程式在流套接字(基於 TCP)上呼叫 connect 發起與伺服器的連線請求。
  4. 伺服器應用程式使用acceptAPI 接受客戶端連線請求,伺服器必須先成功呼叫 bind 和 listen 後,再呼叫 accept api。
  5. 在流套接字之間建立連線後,客戶端和伺服器就可以發起 read/write api 呼叫了。
  6. 當伺服器或客戶端要停止操作時,就會呼叫 close API 釋放套接字獲取的所有系統資源。

雖然套接字 API 位於應用程式層和傳輸層之間的通訊模型中,但是套接字 API 不屬於通訊模型。套接字 API 允許應用程式與傳輸層和網路層進行互動。

在往下繼續聊之前,我們先播放一個小插曲,簡單聊一聊 IP。

聊聊 IP

IPInternet Protocol(網際互連協議)的縮寫,是 TCP/IP 體系中的網路層協議。設計 IP 的初衷主要想解決兩類問題

  • 提高網路擴充套件性:實現大規模網路互聯
  • 對應用層和鏈路層進行解藕,讓二者獨立發展。

IP 是整個 TCP/IP 協議族的核心,也是構成網際網路的基礎。為了實現大規模網路的互通互聯,IP 更加註重適應性、簡潔性和可操作性,並在可靠性做了一定的犧牲。IP 不保證分組的交付時限和可靠性,所傳送分組有可能出現丟失、重複、延遲或亂序等問題。

我們知道,TCP 協議的下一層就是 IP 協議層,既然 IP 不可靠,那麼如何保證資料能夠準確無誤地到達呢?

這就涉及到 TCP 傳輸機制的問題了,我們後面聊到 TCP 的時候再說。

埠號

在聊埠號前,先來聊一聊檔案描述以及 socket 和埠號的關係

為了方便資源的使用,提高機器的效能、利用率和穩定性等等原因,我們的計算機都有一層軟體叫做作業系統,它用於幫我們管理計算機可以使用的資源,當我們的程式要使用一個資源的時候,可以向作業系統申請,再由作業系統為我們的程式分配和管理資源。通常當我們要訪問一個核心裝置或檔案時,程式可以呼叫系統函式,系統就會為我們開啟裝置或檔案,然後返回一個檔案描述符fd(或稱為ID,是一個整數),我們要訪問該裝置或檔案,只能通過該檔案描述符。可以認為該編號對應著開啟的檔案或裝置。

而當我們的程式要使用網路時,要使用到對應的作業系統核心的操作和網路卡裝置,所以我們可以向作業系統申請,然後系統會為我們建立一個套接字 Socket,並返回這個 Socket 的ID,以後我們的程式要使用網路資源,只要向這個 Socket 的編號 ID 操作即可。而我們的每一個網路通訊的程式至少對應著一個 Socket。向 Socket 的 ID 中寫資料,相當於向網路傳送資料,向 Socket 中讀資料,相當於接收資料。而且這些套接字都有唯一識別符號——檔案描述符 fd。

埠號是 16 位的非負整數,它的範圍是 0 - 65535 之間,這個範圍會分為三種不同的埠號段,由 Internet 號碼分配機構 IANA 進行分配

  • 周知/標準埠號,它的範圍是 0 - 1023
  • 註冊埠號,範圍是 1024 - 49151
  • 私有埠號,範圍是 49152 - 6553

一臺計算機上可以執行多個應用程式,當一個報文段到達主機後,應該傳輸給哪個應用程式呢?你怎麼知道這個報文段就是傳遞給 HTTP 伺服器而不是 SSH 伺服器的呢?

是憑藉埠號嗎?當報文到達伺服器時,是埠號來區分不同應用程式的,所以應該藉助埠號來區分。

舉個例子反駁一下 cxuan,假如到達伺服器的兩條資料都是由 80 埠發出的你該如何區分呢?或者說到達伺服器的兩條資料埠一樣,協議不同,該如何區分呢?

所以僅憑埠號來確定某一條報文顯然是不夠的。

網際網路上一般使用 源 IP 地址、目標 IP 地址、源埠號、目標埠號 來進行區分。如果其中的某一項不同,就被認為是不同的報文段。這些也是多路分解和多路複用 的基礎。

確定埠號

在實際通訊之前,需要先確定一下埠號,確定埠號的方法分為兩種:

  • 標準既定的埠號

標準既定的埠號是靜態分配的,每個程式都會有自己的埠號,每個埠號都有不同的用途。埠號是一個 16 位元的數,其大小在 0 - 65535 之間,0 - 1023 範圍內的埠號都是動態分配的既定埠號,例如 HTTP 使用 80 埠來標識,FTP 使用 21 埠來標識,SSH 使用 22 來標識。這類埠號有一個特殊的名字,叫做 周知埠號(Well-Known Port Number)

  • 時序分配的埠號

第二種分配埠號的方式是一種動態分配法,在這種方法下,客戶端應用程式可以完全不用自己設定埠號,憑藉作業系統進行分配,作業系統可以為每個應用程式分配互不衝突的埠號。這種動態分配埠號的機制即使是同一個客戶端發起的 TCP 連線,也能識別不同的連線。

多路複用和多路分解

我們上面聊到了在主機上的每個套接字都會分配一個埠號,當報文段到達主機時,運輸層會檢查報文段中的目的埠號,並將其定向到相應的套接字,然後報文段中的資料通過套接字進入其所連線的程式。下面我們來聊一下什麼是多路複用和多路分解的概念。

多路複用和多路分解分為兩種,即無連線的多路複用(多路分解)和面向連線的多路複用(多路分解)

無連線的多路複用和多路分解

開發人員會編寫程式碼確定埠號是周知埠號還是時序分配的埠號。假如主機 A 中的一個 10637 埠要向主機 B 中的 45438 埠傳送資料,運輸層採用的是 UDP 協議,資料在應用層產生後,會在運輸層中加工處理,然後在網路層將資料封裝得到 IP 資料包,IP 資料包通過鏈路層盡力而為的交付給主機 B,然後主機 B 會檢查報文段中的埠號判斷是哪個套接字的,這一系列的過程如下所示

UDP 套接字就是一個二元組,二元組包含目的 IP 地址和目的埠號。

所以,如果兩個 UDP 報文段有不同的源 IP 地址和/或相同的源埠號,但是具有相同的目的 IP 地址和目的埠號,那麼這兩個報文會通過套接字定位到相同的目的程式。

這裡思考一個問題,主機 A 給主機 B 傳送一個訊息,為什麼還需要知道源埠號呢?比如我給妹子表達出我對你有點意思的資訊,妹子還需要知道這個資訊是從我的哪個器官發出的嗎?知道是我這個人對你有點意思不就完了?實際上是需要的,因為妹子如果要表達出她對你也有點意思,她是不是可能會親你一口,那她得知道往哪親吧?

這就是,在 A 到 B 的報文段中,源埠號會作為 返回地址 的一部分,即當 B 需要回發一個報文段給 A 時,B 需要從 A 到 B 中的源埠號取值,如下圖所示

面向連線的多路複用與多路分解

如果說無連線的多路複用和多路分解指的是 UDP 的話,那麼面向連線的多路複用與多路分解指的是 TCP 了,TCP 和 UDP 在報文結構上的差別是,UDP 是一個二元組而 TCP 是一個四元組,即源 IP 地址、目標 IP 地址、源埠號、目標埠號 ,這個我們上面也提到了。當一個 TCP 報文段從網路到達一臺主機時,這個主機會根據這四個值拆解到對應的套接字上。

上圖顯示了面向連線的多路複用和多路分解的過程,圖中主機 C 向主機 B 發起了兩個 HTTP 請求,主機 A 向主機 C 發起了一個 HTTP 請求,主機 A、B、C 都有自己唯一的 IP 地址,當主機 C 發出 HTTP 請求後,主機 B 能夠分解這兩個 HTTP 連線,因為主機 C 發出請求的兩個源埠號不同,所以對於主機 B 來說,這是兩條請求,主機 B 能夠進行分解。對於主機 A 和主機 C 來說,這兩個主機有不同的 IP 地址,所以對於主機 B 來說,也能夠進行分解。

UDP

終於,我們開始了對 UDP 協議的探討,淦起!

UDP 的全稱是 使用者資料包協議(UDP,User Datagram Protocol),UDP 為應用程式提供了一種無需建立連線就可以傳送封裝的 IP 資料包的方法。如果應用程式開發人員選擇的是 UDP 而不是 TCP 的話,那麼該應用程式相當於就是和 IP 直接打交道的。

從應用程式傳遞過來的資料,會附加上多路複用/多路分解的源和目的埠號欄位,以及其他欄位,然後將形成的報文傳遞給網路層,網路層將運輸層報文段封裝到 IP 資料包中,然後盡力而為的交付給目標主機。最關鍵的一點就是,使用 UDP 協議在將資料包傳遞給目標主機時,傳送方和接收方的運輸層實體間是沒有握手的。正因為如此,UDP 被稱為是無連線的協議。

UDP 特點

UDP 協議一般作為流媒體應用、語音交流、視訊會議所使用的傳輸層協議,我們大家都知道的 DNS 協議底層也使用了 UDP 協議,這些應用或協議之所以選擇 UDP 主要是因為以下這幾點

  • 速度快,採用 UDP 協議時,只要應用程式將資料傳給 UDP,UDP 就會將此資料打包進 UDP 報文段並立刻傳遞給網路層,然後 TCP 有擁塞控制的功能,它會在傳送前判斷網際網路的擁堵情況,如果網際網路極度阻塞,那麼就會抑制 TCP 的傳送方。使用 UDP 的目的就是希望實時性。
  • 無須建立連線,TCP 在資料傳輸之前需要經過三次握手的操作,而 UDP 則無須任何準備即可進行資料傳輸。因此 UDP 沒有建立連線的時延。如果使用 TCP 和 UDP 來比喻開發人員:TCP 就是那種凡事都要設計好,沒設計不會進行開發的工程師,需要把一切因素考慮在內後再開幹!所以非常靠譜;而 UDP 就是那種上來直接乾乾幹,接到專案需求馬上就開幹,也不管設計,也不管技術選型,就是幹,這種開發人員非常不靠譜,但是適合快速迭代開發,因為可以馬上上手!
  • 無連線狀態,TCP 需要在端系統中維護連線狀態,連線狀態包括接收和傳送快取、擁塞控制引數以及序號和確認號的引數,在 UDP 中沒有這些引數,也沒有傳送快取和接受快取。因此,某些專門用於某種特定應用的伺服器當應用程式執行在 UDP 上,一般能支援更多的活躍使用者
  • 分組首部開銷小,每個 TCP 報文段都有 20 位元組的首部開銷,而 UDP 僅僅只有 8 位元組的開銷。

這裡需要注意一點,並不是所有使用 UDP 協議的應用層都是不可靠的,應用程式可以自己實現可靠的資料傳輸,通過增加確認和重傳機制。所以使用 UDP 協議最大的特點就是速度快。

UDP 報文結構

下面來一起看一下 UDP 的報文結構,每個 UDP 報文分為 UDP 報頭和 UDP 資料區兩部分。報頭由 4 個 16 位長(2 位元組)欄位組成,分別說明該報文的源埠、目的埠、報文長度和校驗值。

40 張圖帶你搞懂 TCP 和 UDP
  • 源埠號(Source Port) :這個欄位佔據 UDP 報文頭的前 16 位,通常包含傳送資料包的應用程式所使用的 UDP 埠。接收端的應用程式利用這個欄位的值作為傳送響應的目的地址。這個欄位是可選項,有時不會設定源埠號。沒有源埠號就預設為 0 ,通常用於不需要返回訊息的通訊中。
  • 目標埠號(Destination Port): 表示接收端埠,欄位長為 16 位
  • 長度(Length): 該欄位佔據 16 位,表示 UDP 資料包長度,包含 UDP 報文頭和 UDP 資料長度。因為 UDP 報文頭長度是 8 個位元組,所以這個值最小為 8,最大長度為 65535 位元組。
  • 校驗和(Checksum):UDP 使用校驗和來保證資料安全性,UDP 的校驗和也提供了差錯檢測功能,差錯檢測用於校驗報文段從源到目標主機的過程中,資料的完整性是否發生了改變。傳送方的 UDP 對報文段中的 16 位元字的和進行反碼運算,求和時遇到的位溢位都會被忽略,比如下面這個例子,三個 16 位元的數字進行相加
40 張圖帶你搞懂 TCP 和 UDP

​ 這些 16 位元的前兩個和是

40 張圖帶你搞懂 TCP 和 UDP

​ 然後再將上面的結果和第三個 16 位元的數進行相加

40 張圖帶你搞懂 TCP 和 UDP

最後一次相加的位會進行溢位,溢位位 1 要被捨棄,然後進行反碼運算,反碼運算就是將所有的 1 變為 0 ,0 變為 1。因此 1000 0100 1001 0101 的反碼就是 0111 1011 0110 1010,這就是校驗和,如果在接收方,資料沒有出現差錯,那麼全部的 4 個 16 位元的數值進行運算,同時也包括校驗和,如果最後結果的值不是 1111 1111 1111 1111 的話,那麼就表示傳輸過程中的資料出現了差錯。

下面來想一個問題,為什麼 UDP 會提供差錯檢測的功能?

這其實是一種 端到端 的設計原則,這個原則說的是要讓傳輸中各種錯誤發生的概率降低到一個可以接受的水平

檔案從主機A傳到主機B,也就是說AB主機要通訊,需要經過三個環節:首先是主機A從磁碟上讀取檔案並將資料分組成一個個資料包packet,,然後資料包通過連線主機A和主機B的網路傳輸到主機B,最後是主機B收到資料包並將資料包寫入磁碟。在這個看似簡單其實很複雜的過程中可能會由於某些原因而影響正常通訊。比如:磁碟上檔案讀寫錯誤、緩衝溢位、記憶體出錯、網路擁擠等等這些因素都有可能導致資料包的出錯或者丟失,由此可見用於通訊的網路是不可靠的。

由於實現通訊只要經過上述三個環節,那麼我們就想是否在其中某個環節上增加一個檢錯糾錯機制來用於對資訊進行把關呢?

網路層肯定不能做這件事,因為網路層的最主要目的是增大資料傳輸的速率,網路層不需要考慮資料的完整性,資料的完整性和正確性交給端系統去檢測就行了,因此在資料傳輸中,對於網路層只能要求其提供儘可能好的資料傳輸服務,而不可能寄希望於網路層提供資料完整性的服務。

UDP 不可靠的原因是它雖然提供差錯檢測的功能,但是對於差錯沒有恢復能力更不會有重傳機制

TCP

UDP 是一種沒有複雜的控制,提供無連線通訊服務的一種協議,換句話說,它將部分控制部分交給應用程式去處理,自己只提供作為傳輸層協議最基本的功能。

而與 UDP 不同的是,同樣作為傳輸層協議,TCP 協議要比 UDP 的功能多很多。

TCP 的全稱是 Transmission Control Protocol,它被稱為是一種面向連線(connection-oriented) 的協議,這是因為一個應用程式開始向另一個應用程式傳送資料之前,這兩個程式必須先進行握手,握手是一個邏輯連線,並不是兩個主機之間進行真實的握手。

這個連線是指各種裝置、線路或者網路中進行通訊的兩個應用程式為了相互傳遞訊息而專有的、虛擬的通訊鏈路,也叫做虛擬電路。

一旦主機 A 和主機 B 建立了連線,那麼進行通訊的應用程式只使用這個虛擬的通訊線路傳送和接收資料就可以保證資料的傳輸,TCP 協議負責控制連線的建立、斷開、保持等工作。

TCP 連線是全雙工服務(full-duplex service) 的,全雙工是什麼意思?全雙工指的是主機 A 與另外一個主機 B 存在一條 TCP 連線,那麼應用程資料就可以從主機 B 流向主機 A 的同時,也從主機 A 流向主機 B。

TCP 只能進行 點對點(point-to-point) 連線,那麼所謂的多播,即一個主機對多個接收方傳送訊息的情況是不存在的,TCP 連線只能連線兩個一對主機。

TCP 的連線建立需要經過三次握手,這個我們下面再說。一旦 TCP 連線建立後,主機之間就可以相互傳送資料了,客戶程式通過套接字傳送資料流。資料一旦通過套接字後,它就由客戶中執行的 TCP 協議所控制。

TCP 會將資料臨時儲存到連線的傳送快取(send buffer) 中,這個 send buffer 是三次握手之間設定的快取之一,然後 TCP 在合適的時間將傳送快取中的資料傳送到目標主機的接收快取中,實際上,每一端都會有傳送快取和接收快取,如下所示

主機之間的傳送是以 報文段(segment) 進行的,那麼什麼是 Segement 呢?

TCP 會將要傳輸的資料流分為多個塊(chunk),然後向每個 chunk 中新增 TCP 標頭,這樣就形成了一個 TCP 段也就是報文段。每一個報文段可以傳輸的長度是有限的,不能超過最大資料長度(Maximum Segment Size),俗稱 MSS。在報文段向下傳輸的過程中,會經過鏈路層,鏈路層有一個 Maximum Transmission Unit ,最大傳輸單元 MTU, 即資料鏈路層上所能通過最大資料包的大小,最大傳輸單元通常與通訊介面有關。

那麼 MSS 和 MTU 有啥關係呢?

因為計算機網路是分層考慮的,這個很重要,不同層的稱呼不一樣,對於傳輸層來說,稱為報文段而對網路層來說就叫做 IP 資料包,所以,MTU 可以認為是網路層能夠傳輸的最大 IP 資料包,而 MSS(Maximum segment size)可以認為是傳輸層的概念,也就是 TCP 資料包每次能夠傳輸的最大量

TCP 報文段結構

在簡單聊了聊 TCP 連線後,下面我們就來聊一下 TCP 的報文段結構,如下圖所示

TCP 報文段結構相比 UDP 報文結構多了很多內容。但是前兩個 32 位元的欄位是一樣的。它們是 源埠號目標埠號,我們知道,這兩個欄位是用於多路複用和多路分解的。另外,和 UDP 一樣,TCP 也包含校驗和(checksum field) ,除此之外,TCP 報文段首部還有下面這些

  • 32 位元的序號欄位(sequence number field) 和 32 位元的確認號欄位(acknowledgment number field) 。這些欄位被 TCP 傳送方和接收方用來實現可靠的資料傳輸。

  • 4 位元的首部欄位長度欄位(header length field),這個欄位指示了以 32 位元的字為單位的 TCP 首部長度。TCP 首部的長度是可變的,但是通常情況下,選項欄位為空,所以 TCP 首部欄位的長度是 20 位元組。

  • 16 位元的 接受視窗欄位(receive window field) ,這個欄位用於流量控制。它用於指示接收方能夠/願意接受的位元組數量

  • 可變的選項欄位(options field),這個欄位用於傳送方和接收方協商最大報文長度,也就是 MSS 時使用

  • 6 位元的 標誌欄位(flag field)ACK 標誌用於指示確認欄位中的值是有效的,這個報文段包括一個對已被成功接收報文段的確認;RSTSYNFIN 標誌用於連線的建立和關閉;CWRECE 用於擁塞控制;PSH 標誌用於表示立刻將資料交給上層處理;URG 標誌用來表示資料中存在需要被上層處理的 緊急 資料。緊急資料最後一個位元組由 16 位元的緊急資料指標欄位(urgeent data pointer field) 指出。一般情況下,PSH 和 URG 並沒有使用。

TCP 的各種功能和特點都是通過 TCP 報文結構來體現的,在聊完 TCP 報文結構之後,我們下面就來聊一下 TCP 有哪些功能及其特點了。

序號、確認號實現傳輸可靠性

TCP 報文段首部中兩個最重要的欄位就是 序號確認號,這兩個欄位是 TCP 實現可靠性的基礎,那麼你肯定好奇如何實現可靠性呢?要了解這一點,首先我們得先知道這兩個欄位裡面存了哪些內容吧?

一個報文段的序號就是資料流的位元組編號 。因為 TCP 會把資料流分割成為一段一段的位元組流,因為位元組流本身是有序的,所以每一段的位元組編號就是標示是哪一段的位元組流。比如,主機 A 要給主機 B 傳送一條資料。資料經過應用層產生後會有一串資料流,資料流會經過 TCP 分割,分割的依據就是 MSS,假設資料是 10000 位元組,MSS 是 2000 位元組,那麼 TCP 就會把資料拆分成 0 - 1999 , 2000 - 3999 的段,依次類推。

所以,第一個資料 0 - 1999 的首位元組編號就是 0 ,2000 - 3999 的首位元組編號就是 2000 。。。。。。

然後,每個序號都會被填入 TCP 報文段首部的序號欄位中。

至於確認號的話,會比序號要稍微麻煩一些。這裡我們先擴充下幾種通訊模型。

  • 單工通訊:單工資料傳輸只支援資料在一個方向上傳輸;在同一時間只有一方能接受或傳送資訊,不能實現雙向通訊,比如廣播、電視等。
  • 雙工通訊是一種點對點系統,由兩個或者多個在兩個方向上相互通訊的連線方或者裝置組成。雙工通訊模型有兩種:全雙工(FDX)和半雙工(HDX)
    • 全雙工:在全雙工系統中,連線雙方可以相互通訊,一個最常見的例子就是電話通訊。全雙工通訊是兩個單工通訊方式的結合,它要求傳送裝置和接收裝置都有獨立的接收和傳送能力。
    • 半雙工:在半雙工系統中,連線雙方可以彼此通訊,但不能同時通訊,比如對講機,只有把按鈕按住的人才能夠講話,只有一個人講完話後另外一個人才能講話。

單工、半雙工、全雙工通訊如下圖所示

TCP 是一種全雙工的通訊協議,因此主機 A 在向主機 B 傳送訊息的過程中,也在接受來自主機 B 的資料。主機 A 填充進報文段的確認號是期望從主機 B 收到的下一位元組的序號。稍微有點繞,我們來舉個例子看一下。比如主機 A 收到了來自主機 B 傳送的編號為 0 - 999 位元組的報文段,這個報文段會寫入序號中,隨後主機 A 期望能夠從主機 B 收到 1000 - 剩下的報文段,因此,主機 A 傳送到主機 B 的報文段中,它的確認號就是 1000 。

累積確認

這裡再舉出一個例子,比如主機 A 在傳送 0 - 999 報文段後,期望能夠接受到 1000 之後的報文段,但是主機 B 卻給主機 A 傳送了一個 1500 之後的報文段,那麼主機 A 是否還會繼續進行等待呢?

答案顯然是會的,因為 TCP 只會確認流中至第一個丟失位元組為止的位元組,因為 1500 雖然屬於 1000 之後的位元組,但是主機 B 沒有給主機 A 傳送 1000 - 1499 之間的位元組,所以主機 A 會繼續等待。

在瞭解完序號和確認號之後,我們下面來聊一下 TCP 的傳送過程。下面是一個正常的傳送過程

TCP 通過肯定的確認應答(ACK) 來實現可靠的資料傳輸,當主機 A將資料發出之後會等待主機 B 的響應。如果有確認應答(ACK),說明資料已經成功到達對端。反之,則資料很可能會丟失。

如下圖所示,如果在一定時間內主機 A 沒有等到確認應答,則認為主機 B 傳送的報文段已經丟失,並進行重發。

主機 A 給主機 B 的響應可能由於網路抖動等原因無法到達,那麼在經過特定的時間間隔後,主機 A 將重新傳送報文段。

主機 A 沒有收到主機 B 的響應還可能是因為主機 B 在傳送給主機 A 的過程中丟失。

如上圖所示,由主機 B 返回的確認應答,由於網路擁堵等原因在傳送的過程中丟失,並沒有到達主機 A。主機 A 會等待一段時間,如果在這段時間內主機 A 仍沒有等到主機 B 的響應,那麼主機 A 會重新傳送報文段。

那麼現在就存在一個問題,如果主機 A 給主機 B 傳送了一個報文段後,主機 B 接受到報文段傳送響應,此刻由於網路原因,這個報文段並未到達,等到一段時間後主機 A 重新傳送報文段,然後此時主機 B 傳送的響應在主機 A 第二次傳送後失序到達主機 A,那麼主機 A 應該如何處理呢?

TCP RFC 並未為此做任何規定,也就是說,我們可以自己決定如何處理失序到達的報文段。一般處理方式有兩種

  • 接收方立刻丟棄失序的報文段
  • 接收方接受時許到達的報文段,並等待後續的報文段

一般來說通常採取的做法是第二種。

傳輸控制

利用視窗控制提高速度

前面我們介紹了 TCP 是以資料段的形式進行傳送,如果經過一段時間內主機 A 等不到主機 B 的響應,主機 A 就會重新傳送報文段,接受到主機 B 的響應,再會繼續傳送後面的報文段,我們現在看到,這一問一答的形式還存在許多條件,比如響應未收到、等待響應等,那麼對崇尚效能的網際網路來說,這種形式的效能應該不會很高。

那麼如何提升效能呢?

為了解決這個問題,TCP 引入了 視窗 這個概念,即使在往返時間較長、頻次很多的情況下,它也能控制網路效能的下降,聽起來很牛批,那它是如何實現的呢?

如下圖所示

我們之前每次請求傳送都是以報文段的形式進行的,引入視窗後,每次請求都可以傳送多個報文段,也就是說一個視窗可以傳送多個報文段。視窗大小就是指無需等待確認應答就可以繼續傳送報文段的最大值。

在這個視窗機制中,大量使用了 緩衝區 ,通過對多個段同時進行確認應答的功能。

如下圖所示,傳送報文段中高亮部分即是我們提到的視窗,在視窗內,即是沒有收到確認應答也可以把請求傳送出去。不過,在整個視窗的確認應答沒有到達之前,如果部分報文段丟失,那麼主機 A 將仍會重傳。為此,主機 A 需要設定快取來保留這些需要重傳的報文段,直到收到他們的確認應答。

在滑動視窗以外的部分是尚未傳送的報文段和已經接受到的報文段,如果報文段已經收到確認則不可進行重發,此時報文段就可以從緩衝區中清除。

在收到確認的情況下,會將視窗滑動到確認應答中確認號的位置,如上圖所示,這樣可以順序的將多個段同時傳送,用以提高通訊效能,這種視窗也叫做 滑動視窗(Sliding window)

視窗控制和重發

報文段的傳送和接收,必然伴隨著報文段的丟失和重發,視窗也是同樣如此,如果在視窗中報文段傳送過程中出現丟失怎麼辦?

首先我們先考慮確認應答沒有返回的情況。在這種情況下,主機 A 傳送的報文段到達主機 B,是不需要再進行重發的。這和單個報文段的傳送不一樣,如果傳送單個報文段,即使確認應答沒有返回,也要進行重發

視窗在一定程度上比較大時,即使有少部分確認應答的丟失,也不會重新傳送報文段。

我們知道,如果在某個情況下由於傳送的報文段丟失,導致接受主機未收到請求,或者主機返回的響應未到達客戶端的話,會經過一段時間重傳報文。那麼在使用視窗的情況下,報文段丟失會怎麼樣呢?

如下圖所示,報文段 0 - 999 丟失後,但是主機 A 並不會等待,主機 A 會繼續傳送餘下的報文段,主機 B 傳送的確認應答卻一直是 1000,同一個確認號的應答報文會被持續不斷的返回,如果傳送端主機在連續 3 次收到同一個確認應答後,就會將其所對應的資料重發,這種機制要比之前提到的超時重發更加高效,這種機制也被稱為 高速重發控制。這種重發的確認應答也被稱為 冗餘 ACK(響應)

主機 B 在沒有接收到自己期望序列號的報文段時,會對之前收到的資料進行確認應答。傳送端則一旦收到某個確認應答後,又連續三次收到同樣的確認應答,那麼就會認為報文段已經丟失。需要進行重發。使用這種機制可以提供更為快速的重發服務

流量控制

前面聊的是傳輸控制,下面 cxuan 再和你聊一下 流量控制。我們知道,在每個 TCP 連線的一側主機都會有一個 socket 緩衝區,緩衝區會為每個連線設定接收快取和傳送快取,當 TCP 建立連線後,從應用程式產生的資料就會到達接收方的接收緩衝區中,接收方的應用程式並不一定會馬上讀區緩衝區的資料,它需要等待作業系統分配時間片。如果此時傳送方的應用程式產生資料過快,而接收方讀取接受緩衝區的資料相對較慢的話,那麼接收方中緩衝區的資料將會溢位

但是還好,TCP 有 流量控制服務(flow-control service) 用於消除緩衝區溢位的情況。流量控制是一個速度匹配服務,即傳送方的傳送速率與接受方應用程式的讀取速率相匹配。

TCP 通過使用一個 接收視窗(receive window) 的變數來提供流量控制。接受視窗會給傳送方一個指示到底還有多少可用的快取空間。傳送端會根據接收端的實際接受能力來控制傳送的資料量。

接收端主機向傳送端主機通知自己可以接收資料的大小,傳送端會傳送不超過這個限度的資料,這個大小限度就是視窗大小,還記得 TCP 的首部麼,有一個接收視窗,我們上面聊的時候說這個欄位用於流量控制。它用於指示接收方能夠/願意接受的位元組數量。

那麼只知道這個欄位用於流量控制,那麼如何控制呢?

傳送端主機會定期傳送一個視窗探測包,這個包用於探測接收端主機是否還能夠接受資料,當接收端的緩衝區一旦面臨資料溢位的風險時,視窗大小的值也隨之被設定為一個更小的值通知傳送端,從而控制資料傳送量。

下面是一個流量控制示意圖

傳送端主機根據接收端主機的視窗大小進行流量控制。由此也可以防止傳送端主機一次傳送過大資料導致接收端主機無法處理。

如上圖所示,當主機 B 收到報文段 2000 - 2999 之後緩衝區已滿,不得不暫時停止接收資料。然後主機 A 傳送視窗探測包,視窗探測包非常小僅僅一個位元組。然後主機 B 更新緩衝區接收視窗大小併傳送視窗更新通知給主機 A,然後主機 A 再繼續傳送報文段。

在上面的傳送過程中,視窗更新通知可能會丟失,一旦丟失傳送端就不會傳送資料,所以視窗探測包會隨機傳送,以避免這種情況發生。

連線管理

在繼續介紹下面有意思的特性之前,我們先來把關注點放在 TCP 的連線管理上,因為沒有 TCP 連線,也就沒有後續的一系列 TCP 特性什麼事兒了。假設執行在一臺主機上的程式想要和另一臺主機上的程式建立一條 TCP 連線,那麼客戶中的 TCP 會使用下面這些步驟與伺服器中的 TCP 建立連線。

  • 首先,客戶端首先向伺服器傳送一個特殊的 TCP 報文段。這個報文段首部不包含應用層資料,但是在報文段的首部中有一個 SYN 標誌位 被置為 1。因此,這個特殊的報文段也可以叫做 SYN 報文段。然後,客戶端隨機選擇一個初始序列號(client_isn) ,並將此數字放入初始 TCP SYN 段的序列號欄位中,SYN 段又被封裝在 IP 資料段中傳送給伺服器。

  • 一旦包含 IP 資料段到達伺服器後,服務端會從 IP 資料段中提取 TCP SYN 段,將 TCP 緩衝區和變數分配給連線,然後給客戶端傳送一個連線所允許的報文段。這個連線所允許的報文段也不包括任何應用層資料。然而,它卻包含了三個非常重要的資訊。

    這些緩衝區和變數的分配使 TCP 容易受到稱為 SYN 泛洪的拒絕服務攻擊。

    • 首先,SYN 位元被置為 1 。
    • 然後,TCP 報文段的首部確認號被設定為 client_isn + 1
    • 最後,伺服器選擇自己的初始序號(server_isn),並將其放置到 TCP 報文段首部的序號欄位中。

    如果用大白話解釋下就是,我收到了你發起建立連線的 SYN 報文段,這個報文段具有首部欄位 client_isn。我同意建立該連線,我自己的初始序號是 server_isn。這個允許連線的報文段被稱為 SYNACK 報文段

  • 第三步,在收到 SYNACK 報文段後,客戶端也要為該連線分配緩衝區和變數。客戶端主機向伺服器傳送另外一個報文段,最後一個報文段對伺服器傳送的響應報文做了確認,確認的標準是客戶端傳送的資料段中確認號為 server_isn + 1,因為連線已經建立,所以 SYN 位元被置為 0 。以上就是 TCP 建立連線的三次資料段傳送過程,也被稱為 三次握手

一旦完成這三個步驟,客戶和伺服器主機就可以相互傳送報文段了,在以後的每一個報文段中,SYN 位元都被置為 0 ,整個過程描述如下圖所示

在客戶端主機和服務端主機建立連線後,參與一條 TCP 連線的兩個程式中的任何一個都能終止 TCP 連線。連線結束後,主機中的快取和變數將會被釋放。假設客戶端主機想要終止 TCP 連線,它會經歷如下過程

客戶應用程式發出一個關閉命令,客戶 TCP 向伺服器程式傳送一個特殊的 TCP 報文段,這個特殊的報文段的首部標誌 FIN 被設定為 1 。當伺服器收到這個報文段後,就會向傳送方傳送一個確認報文段。然後,伺服器傳送它自己的終止報文段,FIN 位被設定為 1 。客戶端對這個終止報文段進行確認。此時,在兩臺主機上用於該連線的所有資源都被釋放了,如下圖所示

在一個 TCP 連線的生命週期內,執行在每臺主機中的 TCP 協議都會在各種 TCP 狀態(TCP State) 之間進行變化,TCP 的狀態主要有 LISTEN、SYN-SEND、SYN-RECEIVED、ESTABLISHED、FIN-WAIT-1、FIN-WAIT-2、CLOSE-WAIT、CLOSING、LAST-ACK、TIME-WAIT 和 CLOSED 。這些狀態的解釋如下

  • LISTEN: 表示等待任何來自遠端 TCP 和埠的連線請求。
  • SYN-SEND: 表示傳送連線請求後等待匹配的連線請求。
  • SYN-RECEIVED: 表示已接收併傳送連線請求後等待連線確認,也就是 TCP 三次握手中第二步後服務端的狀態
  • ESTABLISHED: 表示已經連線已經建立,可以將應用資料傳送給其他主機

上面這四種狀態是 TCP 三次握手所涉及的。

  • FIN-WAIT-1: 表示等待來自遠端 TCP 的連線終止請求,或者等待先前傳送的連線終止請求的確認。

  • FIN-WAIT-2: 表示等待來自遠端 TCP 的連線終止請求。

  • CLOSE-WAIT: 表示等待本地使用者的連線終止請求。

  • CLOSING: 表示等待來自遠端 TCP 的連線終止請求確認。

  • LAST-ACK: 表示等待先前傳送給遠端 TCP 的連線終止請求的確認(包括對它的連線終止請求的確認)。

  • TIME-WAIT: 表示等待足夠的時間以確保遠端 TCP 收到其連線終止請求的確認。

  • CLOSED: 表示連線已經關閉,無連線狀態。

上面 7 種狀態是 TCP 四次揮手,也就是斷開連結所設計的。

TCP 的連線狀態會進行各種切換,這些 TCP 連線的切換是根據事件進行的,這些事件由使用者呼叫:OPEN、SEND、RECEIVE、CLOSE、ABORT 和 STATUS。涉及到 TCP 報文段的標誌有 SYN、ACK、RST 和 FIN ,當然,還有超時。

我們下面加上 TCP 連線狀態後,再來看一下三次握手和四次揮手的過程。

三次握手建立連線

下圖畫出了 TCP 連線建立的過程。假設圖中左端是客戶端主機,右端是服務端主機,一開始,兩端都處於CLOSED(關閉)狀態。

  1. 服務端程式準備好接收來自外部的 TCP 連線,一般情況下是呼叫 bind、listen、socket 三個函式完成。這種開啟方式被認為是 被動開啟(passive open)。然後服務端程式處於 LISTEN 狀態,等待客戶端連線請求。
  2. 客戶端通過 connect 發起主動開啟(active open),向伺服器發出連線請求,請求中首部同步位 SYN = 1,同時選擇一個初始序號 sequence ,簡寫 seq = x。SYN 報文段不允許攜帶資料,只消耗一個序號。此時,客戶端進入 SYN-SEND 狀態。
  3. 伺服器收到客戶端連線後,,需要確認客戶端的報文段。在確認報文段中,把 SYN 和 ACK 位都置為 1 。確認號是 ack = x + 1,同時也為自己選擇一個初始序號 seq = y。請注意,這個報文段也不能攜帶資料,但同樣要消耗掉一個序號。此時,TCP 伺服器進入 SYN-RECEIVED(同步收到) 狀態。
  4. 客戶端在收到伺服器發出的響應後,還需要給出確認連線。確認連線中的 ACK 置為 1 ,序號為 seq = x + 1,確認號為 ack = y + 1。TCP 規定,這個報文段可以攜帶資料也可以不攜帶資料,如果不攜帶資料,那麼下一個資料包文段的序號仍是 seq = x + 1。這時,客戶端進入 ESTABLISHED (已連線) 狀態
  5. 伺服器收到客戶的確認後,也進入 ESTABLISHED 狀態。

TCP 建立一個連線需要三個報文段,釋放一個連線卻需要四個報文段。

四次揮手

資料傳輸結束後,通訊的雙方可以釋放連線。資料傳輸結束後的客戶端主機和服務端主機都處於 ESTABLISHED 狀態,然後進入釋放連線的過程。

TCP 斷開連線需要歷經的過程如下

  1. 客戶端應用程式發出釋放連線的報文段,並停止傳送資料,主動關閉 TCP 連線。客戶端主機傳送釋放連線的報文段,報文段中首部 FIN 位置為 1 ,不包含資料,序列號位 seq = u,此時客戶端主機進入 FIN-WAIT-1(終止等待 1) 階段。

  2. 伺服器主機接受到客戶端發出的報文段後,即發出確認應答報文,確認應答報文中 ACK = 1,生成自己的序號位 seq = v,ack = u + 1,然後伺服器主機就進入 CLOSE-WAIT(關閉等待) 狀態,這個時候客戶端主機 -> 伺服器主機這條方向的連線就釋放了,客戶端主機沒有資料需要傳送,此時伺服器主機是一種半連線的狀態,但是伺服器主機仍然可以傳送資料。

  3. 客戶端主機收到服務端主機的確認應答後,即進入 FIN-WAIT-2(終止等待2) 的狀態。等待客戶端發出連線釋放的報文段。

  4. 當伺服器主機沒有資料傳送後,應用程式就會通知 TCP 釋放連線。這時服務端主機會發出斷開連線的報文段,報文段中 ACK = 1,序列號 seq = w,因為在這之間可能已經傳送了一些資料,所以 seq 不一定等於 v + 1。ack = u + 1,在傳送完斷開請求的報文後,服務端主機就進入了 LAST-ACK(最後確認)的階段。

  5. 客戶端收到服務端的斷開連線請求後,客戶端需要作出響應,客戶端發出斷開連線的報文段,在報文段中,ACK = 1, 序列號 seq = u + 1,因為客戶端從連線開始斷開後就沒有再傳送資料,ack = w + 1,然後進入到 TIME-WAIT(時間等待) 狀態,請注意,這個時候 TCP 連線還沒有釋放。必須經過時間等待的設定,也就是 2MSL 後,客戶端才會進入 CLOSED 狀態,時間 MSL 叫做最長報文段壽命(Maximum Segment Lifetime)

  6. 服務端主要收到了客戶端的斷開連線確認後,就會進入 CLOSED 狀態。因為服務端結束 TCP 連線時間要比客戶端早,而整個連線斷開過程需要傳送四個報文段,因此釋放連線的過程也被稱為四次揮手。

什麼是 TIME-WAIT

我上面只是簡單提到了一下 TIME-WAIT 狀態和 2MSL 是啥,下面來聊一下這兩個概念。

MSL 是 TCP 報文段可以存活或者駐留在網路中的最長時間。RFC 793 定義了 MSL 的時間是兩分鐘,但是具體的實現還要根據程式設計師來指定,一些實現採用了 30 秒的這個最大存活時間。

那麼為什麼要等待 2MSL 呢?

主要是因為兩個理由

  • 為了保證最後一個響應能夠到達伺服器,因為在計算機網路中,最後一個 ACK 報文段可能會丟失,從而致使客戶端一直處於 LAST-ACK 狀態等待客戶端響應。這時候伺服器會重傳一次 FINACK 斷開連線報文,客戶端接收後再重新確認,重啟定時器。如果客戶端不是 2MSL ,在客戶端傳送 ACK 後直接關閉的話,如果報文丟失,那麼雙方主機會無法進入 CLOSED 狀態。
  • 還可以防止已失效的報文段。客戶端在傳送最後一個 ACK 之後,再經過經過 2MSL,就可以使本連結持續時間內所產生的所有報文段都從網路中消失。從保證在關閉連線後不會有還在網路中滯留的報文段去騷擾伺服器。

這裡注意一點:在伺服器傳送了 FIN-ACK 之後,會立即啟動超時重傳計時器。客戶端在傳送最後一個 ACK 之後會立即啟動時間等待計時器。

說好的 RST 呢

說好的 RSTSYNFIN 標誌用於連線的建立和關閉,那麼 SYN 和 FIN 都現身了,那 RST 呢?也是啊,我們上面探討的都是一種理想的情況,就是客戶端伺服器雙方都會接受傳輸報文段的情況,還有一種情況是當主機收到 TCP 報文段後,其 IP 和埠號不匹配的情況。假設客戶端主機傳送一個請求,而伺服器主機經過 IP 和埠號的判斷後發現不是給這個伺服器的,那麼伺服器就會發出一個 RST 特殊報文段給客戶端。

因此,當服務端傳送一個 RST 特殊報文段給客戶端的時候,它就會告訴客戶端沒有匹配的套接字連線,請不要再繼續傳送了

上面探討的是 TCP 的情況,那麼 UDP 呢?

使用 UDP 作為傳輸協議後,如果套接字不匹配的話,UDP 主機就會傳送一個特殊的 ICMP 資料包。

SYN 洪泛攻擊

下面我們來討論一下什麼是 SYN 洪泛攻擊

我們在 TCP 的三次握手中已經看到,伺服器為了響應一個收到的 SYN,分配並初始化變數連線和快取,然後伺服器傳送一個 SYNACK 作為響應,然後等待來自於客戶端的 ACK 報文。如果客戶端不傳送 ACK 來完成最後一步的話,那麼這個連線就處在一個掛起的狀態,也就是半連線狀態。

攻擊者通常在這種情況下傳送大量的 TCP SYN 報文段,服務端繼續響應,但是每個連線都完不成三次握手的步驟。隨著 SYN 的不斷增加,伺服器會不斷的為這些半開連線分配資源,導致伺服器的連線最終被消耗殆盡。這種攻擊也是屬於 Dos 攻擊的一種。

抵禦這種攻擊的方式是使用 SYN cookie ,下面是它的工作流程介紹

  • 當伺服器收到一個 SYN 報文段時,它並不知道這個報文段是來自哪裡,是來自攻擊者主機還是客戶端主機(雖然攻擊者也是客戶端,不過這麼說更便於區分) 。因此伺服器不會為報文段生成一個半開連線。與此相反,伺服器生成一個初始的 TCP 序列號,這個序列號是 SYN 報文段的源和目的 IP 地址與埠號這個四元組構造的一個複雜的雜湊函式,這個雜湊函式生成的 TCP 序列號就是 SYN Cookie,用於快取 SYN 請求。然後,伺服器會傳送帶著 SYN Cookie 的 SYNACK 分組。有一點需要注意的是,伺服器不會記憶這個 Cookie 或 SYN 的其他狀態資訊
  • 如果客戶端不是攻擊者的話,它就會返回一個 ACK 報文段。當伺服器收到這個 ACK 後,需要驗證這個 ACK 與 SYN 傳送的是否相同,驗證的標準就是確認欄位中的確認號和序列號,源和目的 IP 地址與埠號以及和雜湊函式的是否一致,雜湊函式的結果 + 1 是否和 SYNACK 中的確認值相同。(大致是這樣,說的不對還請讀者糾正) 。如果有興趣讀者可以自行深入瞭解。如果是合法的,伺服器就會生成一個具有套接字的全開連線。
  • 如果客戶端沒有返回 ACK,即認為是攻擊者,那麼這樣也沒關係,伺服器沒有收到 ACK,不會分配變數和快取資源,不會對伺服器產生危害。

擁塞控制

有了 TCP 的視窗控制後,使計算機網路中兩個主機之間不再是以單個資料段的形式傳送了,而是能夠連續傳送大量的資料包。然而,大量資料包同時也伴隨著其他問題,比如網路負載、網路擁堵等問題。TCP 為了防止這類問題的出現,使用了 擁塞控制 機制,擁塞控制機制會在面臨網路擁塞時遏制傳送方的資料傳送。

擁塞控制主要有兩種方法

  • 端到端的擁塞控制: 因為網路層沒有為運輸層擁塞控制提供顯示支援。所以即使網路中存在擁塞情況,端系統也要通過對網路行為的觀察來推斷。TCP 就是使用了端到端的擁塞控制方式。IP 層不會向端系統提供有關網路擁塞的反饋資訊。那麼 TCP 如何推斷網路擁塞呢?如果超時或者三次冗餘確認就被認為是網路擁塞,TCP 會減小視窗的大小,或者增加往返時延來避免
  • 網路輔助的擁塞控制: 在網路輔助的擁塞控制中,路由器會向傳送方提供關於網路中擁塞狀態的反饋。這種反饋資訊就是一個位元資訊,它指示鏈路中的擁塞情況。

下圖描述了這兩種擁塞控制方式

TCP 擁塞控制

如果你看到這裡,那我就暫定認為你瞭解了 TCP 實現可靠性的基礎了,那就是使用序號和確認號。除此之外,另外一個實現 TCP 可靠性基礎的就是 TCP 的擁塞控制。如果說

TCP 所採用的方法是讓每一個傳送方根據所感知到的網路的擁塞程度來限制發出報文段的速率,如果 TCP 傳送方感知到沒有什麼擁塞,則 TCP 傳送方會增加傳送速率;如果傳送方感知沿著路徑有阻塞,那麼傳送方就會降低傳送速率。

但是這種方法有三個問題

  1. TCP 傳送方如何限制它向其他連線傳送報文段的速率呢?
  2. 一個 TCP 傳送方是如何感知到網路擁塞的呢?
  3. 當傳送方感知到端到端的擁塞時,採用何種演算法來改變其傳送速率呢?

我們先來探討一下第一個問題,TCP 傳送方如何限制它向其他連線傳送報文段的速率呢

我們知道 TCP 是由接收快取、傳送快取和變數(LastByteRead, rwnd,等)組成。傳送方的 TCP 擁塞控制機制會跟蹤一個變數,即 擁塞視窗(congestion window) 的變數,擁塞視窗表示為 cwnd,用於限制 TCP 在接收到 ACK 之前可以傳送到網路的資料量。而接收視窗(rwnd) 是一個用於告訴接收方能夠接受的資料量。

一般來說,傳送方未確認的資料量不得超過 cwnd 和 rwnd 的最小值,也就是

LastByteSent - LastByteAcked <= min(cwnd,rwnd)

由於每個資料包的往返時間是 RTT,我們假設接收端有足夠的快取空間用於接收資料,我們就不用考慮 rwnd 了,只專注於 cwnd,那麼,該傳送方的傳送速率大概是 cwnd/RTT 位元組/秒 。通過調節 cwnd,傳送方因此能調整它向連線傳送資料的速率。

一個 TCP 傳送方是如何感知到網路擁塞的呢

這個我們上面討論過,是 TCP 根據超時或者 3 個冗餘 ACK 來感知的。

當傳送方感知到端到端的擁塞時,採用何種演算法來改變其傳送速率呢 ?

這個問題比較複雜,且容我娓娓道來,一般來說,TCP 會遵循下面這幾種指導性原則

  • 如果在報文段傳送過程中丟失,那就意味著網路擁堵,此時需要適當降低 TCP 傳送方的速率。
  • 一個確認報文段指示傳送方正在向接收方傳遞報文段,因此,當對先前未確認報文段的確認到達時,能夠增加傳送方的速率。為啥呢?因為未確認的報文段到達接收方也就表示著網路不擁堵,能夠順利到達,因此傳送方擁塞視窗長度會變大,所以傳送速率會變快
  • 頻寬探測,頻寬探測說的是 TCP 可以通過調節傳輸速率來增加/減小 ACK 到達的次數,如果出現丟包事件,就會減小傳輸速率。因此,為了探測擁塞開始出現的頻率, TCP 傳送方應該增加它的傳輸速率。然後慢慢使傳輸速率降低,進而再次開始探測,看看擁塞開始速率是否發生了變化。

在瞭解完 TCP 擁塞控制後,下面我們就該聊一下 TCP 的 擁塞控制演算法(TCP congestion control algorithm) 了。TCP 擁塞控制演算法主要包含三個部分:慢啟動、擁塞避免、快速恢復,下面我們依次來看一下

慢啟動

當一條 TCP 開始建立連線時,cwnd 的值就會初始化為一個 MSS 的較小值。這就使得初始傳送速率大概是 MSS/RTT 位元組/秒 ,比如要傳輸 1000 位元組的資料,RTT 為 200 ms ,那麼得到的初始傳送速率大概是 40 kb/s 。實際情況下可用頻寬要比這個 MSS/RTT 大得多,因此 TCP 想要找到最佳的傳送速率,可以通過 慢啟動(slow-start) 的方式,在慢啟動的方式中,cwnd 的值會初始化為 1 個 MSS,並且每次傳輸報文確認後就會增加一個 MSS,cwnd 的值會變為 2 個 MSS,這兩個報文段都傳輸成功後每個報文段 + 1,會變為 4 個 MSS,依此類推,每成功一次 cwnd 的值就會翻倍。如下圖所示

傳送速率不可能會一直增長,增長總有結束的時候,那麼何時結束呢?慢啟動通常會使用下面這幾種方式結束髮送速率的增長。

  • 如果在慢啟動的傳送過程出現丟包的情況,那麼 TCP 會將傳送方的 cwnd 設定為 1 並重新開始慢啟動的過程,此時會引入一個 ssthresh(慢啟動閾值) 的概念,它的初始值就是產生丟包的 cwnd 的值 / 2,即當檢測到擁塞時,ssthresh 的值就是視窗值的一半。

  • 第二種方式是直接和 ssthresh 的值相關聯,因為當檢測到擁塞時,ssthresh 的值就是視窗值的一半,那麼當 cwnd > ssthresh 時,每次翻番都可能會出現丟包,所以最好的方式就是 cwnd 的值 = ssthresh ,這樣 TCP 就會轉為擁塞控制模式,結束慢啟動。

  • 慢啟動結束的最後一種方式就是如果檢測到 3 個冗餘 ACK,TCP 就會執行一種快速重傳並進入恢復狀態。

擁塞避免

當 TCP 進入擁塞控制狀態後,cwnd 的值就等於擁塞時值的一半,也就是 ssthresh 的值。所以,無法每次報文段到達後都將 cwnd 的值再翻倍。而是採用了一種相對保守的方式,每次傳輸完成後只將 cwnd 的值增加一個 MSS,比如收到了 10 個報文段的確認,但是 cwnd 的值只增加一個 MSS。這是一種線性增長模式,它也會有增長逾值,它的增長逾值和慢啟動一樣,如果出現丟包,那麼 cwnd 的值就是一個 MSS,ssthresh 的值就等於 cwnd 的一半;或者是收到 3 個冗餘的 ACK 響應也能停止 MSS 增長。如果 TCP 將 cwnd 的值減半後,仍然會收到 3 個冗餘 ACK,那麼就會將 ssthresh 的值記錄為 cwnd 值的一半,進入 快速恢復 狀態。

快速恢復

在快速恢復中,對於使 TCP 進入快速恢復狀態缺失的報文段,對於每個收到的冗餘 ACK,cwnd 的值都會增加一個 MSS 。當對丟失報文段的一個 ACK 到達時,TCP 在降低 cwnd 後進入擁塞避免狀態。如果在擁塞控制狀態後出現超時,那麼就會遷移到慢啟動狀態,cwnd 的值被設定為 1 個 MSS,ssthresh 的值設定為 cwnd 的一半。

後記

如果你能用心看到這裡,我相信你定會有所收穫。

這篇文章寫的時間很長,圖中很多樣式和配色都是精挑細選,如果你仔細閱讀,可以看到我的用心良苦。

如果你覺得文章寫的還不錯,歡迎你幫助 cxuan 擴散一下,這將是我繼續更新的動力,切忌不要白嫖,會讓自己變得廉價,好的文章值得分享。

請記得給我一個贊哦!

另外,我自己肝了六本 PDF,微信搜尋「程式設計師cxuan」關注公眾號後,在後臺回覆 cxuan ,領取全部 PDF,這些 PDF 如下

六本 PDF 連結

相關文章