計算機網路: 傳輸層
TCP 和 UDP 大家應該都聽說過,也是面試中比較常見的內容,這兩個協議都是在傳輸層的。這篇文章會講述 TCP 和 UDP 裡面實現的內容。
傳輸層是幹什麼用的
在學習一個東西之前我們應該先看要這個東西幹嘛用的,傳輸層主要提供以下服務:
- 傳送方將應用層的 Message 轉成 Segment
- 接收方將接收到的 Segment 轉成 Message,再給應用層
- 一般用於不同主機之間的程序交流
這裡要和網路層對比一下,網路層主要用於兩個主機(裝置)間的交流,而傳輸層是用於程序之間的交流。

好像很簡單呀,不就資料上傳到下,下再傳到上麼?在理想情況下就是這麼簡單,但是實現總是不如意嘛。
多路複用
這也叫 Multiplexing,為什麼要有這個東西呢?因為在計算機裡每個程序向外面傳資料都要通過不同 Socket 的,Multiplexing 就是將一個計算機裡不同 Socket 資料集合到一個 Data 裡再加上 Header,傳給別的主機。別的主機收到後,再將這個大 Data 分成小塊,將這些小塊送到對應程序使用的 Socket 中,這個過程叫做 Demultiplexing,就是反過來用。

埠號
現在我們來想一個問題,上面 Multiplexing 裡資料集合很容易呀,可以用一個數組存一存,那 Demultiplexing 怎麼知道要送到哪個 Socket 呢?這就需要埠號了。
每個程序都會對應一個埠號,比如 8080 我們特別熟悉的,一般對應 Tomcat 程序或者別的本地伺服器程序。每個程序向外傳送的資訊都會帶有 <IP Adress, Port Number>
這樣的組合,來告訴接收方我應該要將資訊發給哪個程序。
下面是一個例子:

上面進完怎麼將資訊送到對應的程序,下面就來說說資訊是怎麼傳輸的,這就要說到我們很耳熟的兩個協議——UDP,TCP。
UDP
特點
先來說說 UDP 的特點
- Connectionless
- 接收方和傳送方都沒有 Handshaking,也就我們所說的握手過程,後面會說到握手
- Unreliable
- UDP 的包丟了不會去恢復,丟了就丟了
- UDP 的包是亂序的
- 沒有 Congestion Control
- 沒有 Flow Control
上面這兩個 Control 會在 TCP 裡講,因為 TCP 要寫的太多了。。。
格式
一個基礎的 UDP 包格式如下

這裡說一下 checksum 是啥。在傳送資料之前,傳送方會先根據整個 UDP segment 算一個 checksum 值,然後寫在 Header 裡。接收方拿到這個 segment 後,再根據拿到的 segment 算一個 checksum 去對比傳送方算出來的 checksum 是否相等。如果相等就 OK,如果不等就不 OK,傳送方要再重傳這個資料包。
UDP 就沒啥可講了,你看 UDP segment format 就知道沒多少東西,所以 UDP 的一個優點就是 簡單 。
Reliable Data Transfer
在說 TCP 之前,我們先了解一下 Reliable Data Transfer (rdt),因為 TCP 是這個東西的實現。要說的東西太多,只好再分一章放在前面寫。這主要研究的是怎麼傳送更 Reliable 的資料。
資料傳輸的模型可以表示如下圖所示。

這裡的 udt 就是 Unreliable Data Transfer。rcv 是 receive。
rdt 1.0
我們先來想一個很簡單的資料傳輸模型,如圖

使用狀態機可以表示如下

假如我們的 Channel 是可靠的(不發生丟包)的,那麼就沒問題了,也就沒有後面的事情了。
rdt 2.0
一般 Channel 總是會有丟包的情況的,所以每次丟包後都需要 Sender 重新傳那個 Packet。那怎麼告訴 Sender 要重傳呢?就要說到 ACK 和 NAK 了。
ACK = Acknowledgement,Receiver 告訴 Sender 我收到啦,一切 OK
NAK = Negative Acknowledgement,Receiver 告訴 Sender 我沒收到,不 OK,你重傳吧
所以 2.0 新加的功能就是
- 錯誤檢測
- 接收方會發 Feedback 給傳送方 (ACK, NAK)
2.0 的狀態機圖如下圖所示

那是不是這樣就解決所有問題呢?嗯,還沒有,還有別的問題。比如,如果 Sender 沒接收 ACK 或者 NAK 怎麼辦?ACK,NAK 有錯誤怎麼辦?下面來說說怎麼解決這個問題。
解決 rdt 2.0 的問題
Stop and Wait
我們很容易想到一個簡單的方案:我加個定時器 Timer,如果超時了說明沒收到唄,再發一次。雖然可能有重複,不過接收方可以判斷是否重複,然後再選擇要不要這個 Packet。
我們初步的想法可以用下面這張圖表示

但是這裡有個隱藏的效能問題,我們每次都要等前一個 Packet 搞完了,才能發下一個好麻煩呀。假如網速 1Gbps,要傳 8000 bits 的 Packet 就要時間:
假如 RTT (Round Trip Time) 是30

那麼我們的利用率才
我們需要更高效的方法來傳輸 Packet 和 Feedback (ACK, NAK)。
更高效的方法解決 rdt2.0 問題
這個方法就是 Pipelining,Sender 先發一堆 Packet 給 Receiver,Receiver 再發一堆的 Feedback 回 Sender。

要怎麼一堆發和一收一堆 Feedback 那等會再說,反正現在是不用每發完一個 Packet 再發下一個 Packet 了。

這種 Pipelining 方法具體有兩種實現方式: Go-Back-N 和 Selective Repeat。
Go-Back-N
這個方案是先發一堆 Packet,Receiver 再發一堆 Feedback,如果其中有 NAK 或者丟失了 ACK,那麼從那個 Packet 開始後面的 Packet 都要重新發一次,圖示:

在上圖中,因為 Sender 沒有收到 pkt2 的 Feedback,所以 pk2, 3, 4, 5 都要再重新傳一次,哪怕 pkt4, 5 都發了。
因為每次重傳都是從第一個錯誤的 Packet 開始的,所以 Timer 只要一個就可以了,就是從最開始那個 Packet 開始計時。
Selective Repeat
這個就沒上面的那麼簡單粗暴了,每個 Packet 都會有一個 Timer。每次 Packet Loss 時只會重傳丟失的 Packet ,而不是全部都重傳。

從上圖可以看到,pkt2 丟失了,只需要重傳 pkt 2 就好了。
還有一個問題
Selective Repeat 方法總是完美的,我們來想像下面的場景

上面是 pkt 3 丟失了,後面再傳了一次 pkt 0,這個沒什麼問題,因為 pkt 3 後面會再重傳的。再來看另一個場景

如果三個 Packet 都丟失了,那會從 pkt 0 開始重傳,這就有問題了。看看 Reciever 那邊第四個 0,這第四個到底是 Sender 的第一個 0 還是 Sender 的第四個 0 呢?這就是 Selective Repeat 的問題。
要解決這個問題只需要將 Window Size 小於 Sequence Number 就好了,比如這裡 Window Size 最好小於 3.
TCP
現在回到傳輸層 TCP,TCP 的其中的 Reliable 特點就是上面說了一大堆的東西。當然它還有別的特點:
- Point to Point: 一個 Sender 和一個 Receiver
- Reliable (前面說完了),傳輸的包都是有順序的
- 有 Flow Control 和 Congestion Control
- 雙工: 資料可以在同一個連線裡雙向傳遞
Segment Format

這裡面和 UDP 相比是多了那麼點東西,其中 Sequence Number 和 Acknowledgement Number 是比較重要的。
- Sequence Number: 用來標識 Stream 裡的某個一個 Byte,通常來說 Sequence Number 是 32 位的
- Acknowledge Number: 用於傳遞另一方下一個 Byte 對應的 Sequence Number

- RTT: Round Trip Time: 用來設定超時的值
TCP 的 Reliable
參考前面的 Reliable。
這裡要注意的一點是,TCP 重傳會在收到同一個 Packet 的 4 次 ACK 後會立馬開始重傳,而不是要等到超時後再重傳,相對來說會快一點。
Flow Control
TCP 是和應用層連線的,應用層也會用到傳輸層的資料,所以如果應用層取走資料的速度遠低於傳輸層接收資料的速度就會出現 Buffer Overflow 了。

所以 TCP 需要一個機制來控制 Sender 傳輸速度。具體來說,當 Receiver 要發 Feedback 給 Sender 時,會加一項 rwnd ,這個東西的值就是 Buffer 可用空間還有多少。

這樣 Sender 就會根據這個 rwnd 來控制自己傳送資料的速度了。
Connection Manager
這個就是我們所說的 “三次握手” 了。
- 客戶端:你好伺服器,我想連你可以麼
- 伺服器:你好客戶端,你連我吧
- 客戶端:那我發你資料 XXX 嘍

Congestion Control
這個和 Flow Control 不同,這裡要控制的是太多主機發送包給自己,而 Flow Control 只不過是控制傳輸層和應用層的資料流而已。
Congestion Control 方式有兩種:Tahoe 和 RENO。他們都通過控制傳送方的傳送速度來實現 Congestion Control 的。原理如下:
- 傳送方首先要線性方向來傳資料,每次 cwnd (Congestion Window Size) 都會加 1,這個過程稱為 Additive Increase
- 當出現丟包時,cwnd 會直接減一半,這個過程稱為 Multiplicative Decrease

Slow Start, Congestion Avoidance
SS (Slow Start) 和 Congestion Avoidance (CA) 是 Congestion Control 的兩個階段。上面已經說了增加 cwnd 和減少 cwnd 的基本準則,而這兩個階段是在基本準則上的修改。
SS 階段會先以指數倍地增加 cwnd
CA 階段是指當 cwnd 達到闕值 (這裡稱為 ssthresh) 時,再以線性去增加 cwnd
所以先 cwnd = 1, 2, 4, 8, 16 (假如 16 是 ssthresh),然後 cwnd = 16, 17, 18, ...
丟包怎麼辦
剛剛只是說了怎麼增加 cwnd,現在說說丟包後怎麼減少 cwnd。當出現丟包後有兩種響應方式:
- Tahoe
- ssthresh 設定成 cwnd / 2,然後 cwnd 重置為 1 MMS,再開始 SS 階段
- RENO
- 將 ssthresh 設定成當前的 cwnd,然後將 cwnd 設定成當前 cwnd 的一半 (cwnd / 2)