Learning Objectives
- 說明 TCP 怎麼用確認、逾時重傳與快速重傳補回遺失的資料
- 比較停等與滑動視窗,用「視窗÷來回時間」估計傳輸量的上限
- 說明接收視窗(流量控制)保護的是接收端,壅塞視窗(壅塞控制)保護的是網路
- 描述慢啟動、壅塞避免,以及兩種遺失(重複確認、逾時)之後 cwnd 怎麼變
- 依應用的需求判斷該用 TCP 還是 UDP
可靠,而且不能太慢、不能太快
上一課看到 TCP 用序號與確認號記錄「誰收到了什麼」。這一課回答兩個問題:
- 封包遺失了怎麼辦? IP 不保證送達,路由器的佇列滿了就會丟封包。TCP 要發現遺失,並且重送。
- 一次可以送多少? 送太少,線路閒著,很慢;送太多,接收端來不及處理,或者把網路塞爆,反而丟更多封包。
| 機制 | 解決什麼 | 由誰決定 |
|---|---|---|
| 確認與重傳 | 遺失、重複、亂序 | 傳送端與接收端合作 |
| 滑動視窗 | 不必每送一個就停下來等 | 傳送端 |
| 流量控制(接收視窗 rwnd) | 別送得比接收端讀得快 | 接收端在每個 ACK 裡告訴傳送端 |
| 壅塞控制(壅塞視窗 cwnd) | 別送得比網路承受得了的快 | 傳送端依遺失的訊號自己推估 |
逾時重傳與快速重傳
傳送端每送出資料,就啟動一個重傳計時器(RTO)。在時間內等到確認就取消;時間到了還沒確認,就重送最早那個沒被確認的區段。
RTO 多長才對?太短會把只是比較慢的封包當成遺失,太長又讓遺失後空等。TCP 持續量測來回時間(RTT),依 RFC 6298 計算 RTO: 建議的初始值是 1 秒,每逾時一次就加倍,收到新的確認後再依量到的 RTT 重新計算。
等逾時太慢了,所以 TCP 還有另一個訊號:重複確認(duplicate ACK)。確認號是累積的,假設 1001 遺失、後面 1101、1201、1301 都到了:
| 接收端收到 | 回覆的確認號 | 傳送端看到 |
|---|---|---|
| 1101~1200 | 1001(還缺 1001) | 第 1 個重複確認 |
| 1201~1300 | 1001 | 第 2 個 |
| 1301~1400 | 1001 | 第 3 個:幾乎可以確定 1001 遺失了 |
依 RFC 5681,傳送端收到 3 個重複確認就不等逾時,立刻重送,這叫快速重傳(fast retransmit)。 接收端先把後面亂序到的區段暫存起來,缺的那段補到之後,確認號一次跳到 1401。
只有一兩個重複確認時不重送,因為封包也可能只是走了不同的路、晚一點到(亂序)。 另外,重複的資料(例如確認遺失、傳送端重送了對方其實已經收到的區段)靠序號就能認出來,接收端丟掉並再確認一次。
從停等到滑動視窗
最簡單的做法是停等:送一個區段,等到確認再送下一個。每個區段都要花一個 RTT,線路大部分時間在空等。
滑動視窗允許同時有好幾個區段「已送出、還沒確認」。視窗大小就是這個上限;最早的區段被確認後,視窗往前滑,就可以再送新的:
序號 → [已確認] [ 已送出、等確認 ][ 可以送 ][ 還不能送 ]
^最早未確認 ^視窗右緣
|←───────── 視窗大小 ─────→|
每個 RTT 最多送出一個視窗的資料,所以:
傳輸量上限 ≈ 視窗大小 ÷ RTT
例:視窗 65,535 位元組、RTT 10 毫秒 → 65,535 ÷ 0.01 = 6,553,500 位元組/秒 ≈ 52.4 Mbit/s
TCP 標頭的視窗欄位只有 16 位元(最大 65,535),在高速、長距離的網路上不夠用,所以現代系統會用「視窗縮放」選項把它放大。
下面的模擬器裡,連線已經建立,用戶端要送 6 個 100 位元組的區段給伺服器。先比較視窗 1(停等)與視窗 4 的完成時間, 再讓區段遺失,看快速重傳和逾時重傳的差別。
流量控制:接收視窗
接收端的作業系統先把收到的資料放進接收緩衝區,等應用程式呼叫 recv() 讀走。
如果應用程式忙著做別的事、讀得很慢,緩衝區會滿,再送來的資料只能丟掉。
所以接收端在每個 ACK 的視窗欄位寫上「我的緩衝區還剩多少位元組」,這叫接收視窗(rwnd)。傳送端保證「已送出、未確認」的資料不超過它:
| 剛才的模擬 | 伺服器回的 win | 傳送端 |
|---|---|---|
| 程式正常讀取 | 一直是 400 | 照傳送視窗送 |
| 暫停讀取,收了 1 個區段 | 300 | 還能再送 300 |
| 收了 4 個區段 | 0(零視窗) | 停下來,定期送視窗探測問「打開了沒」 |
| 程式讀走資料 | 400(視窗更新) | 繼續送 |
視窗探測是必要的:如果那個「視窗打開了」的 ACK 剛好遺失,兩邊會永遠互相等待。(實際的探測會帶 1 個位元組的資料,模擬器簡化成不帶資料。)
流量控制只照顧接收端。即使接收端很快,中間的網路也可能塞住,那是下一節壅塞控制的工作。
小測驗:重傳與視窗
傳送端依序送出 seq=1001、1101、1201、1301,各 100 位元組;1101 那段遺失了,其他都到。接收端收到 1201 與 1301 時,回覆的確認號是?
為什麼快速重傳要等到 3 個重複確認,而不是 1 個?
視窗大小固定 65,535 位元組,RTT 從 10 毫秒變成 100 毫秒(例如連到國外)。傳輸量上限會怎樣?
伺服器回的 ACK 裡 win=0,代表什麼?
壅塞控制:cwnd、慢啟動與減半
網路上同時有很多連線共用路由器。如果每條連線都盡量送,路由器的佇列滿了開始丟封包,大家都重送,網路更塞, 這叫壅塞。傳送端看不到網路裡面,只能從遺失推測「送太快了」,自己控制一個壅塞視窗(cwnd):
實際一次可以送出、還沒確認的量 = min(cwnd, rwnd)
RFC 5681 規定的基本做法(下面以「區段」為單位、以一個 RTT 為一輪來描述):
| 階段 | 什麼時候 | cwnd 怎麼變 |
|---|---|---|
| 慢啟動(slow start) | cwnd 小於門檻 ssthresh | 每收到一個確認加 1 個區段,所以每輪約加倍:1、2、4、8… |
| 壅塞避免(congestion avoidance) | cwnd 達到門檻之後 | 每輪只加約 1 個區段,小心試探 |
| 收到 3 個重複確認 | 有遺失,但後面的封包還到得了,網路只是有點塞 | 門檻設成目前送出量的一半;快速重傳後 cwnd 從門檻繼續(只減半) |
| 逾時 | 連後續的確認都沒有,情況比較嚴重 | 門檻設成一半,cwnd 回到 1 個區段,重新慢啟動 |
「慢啟動」其實成長得很快,名字是相對於「一開始就送滿整個視窗」而言。初始的 cwnd 也不一定是 1:RFC 5681 依區段大小允許 2~4 個區段,實際系統常用更大的值。
「沒有遺失就慢慢加,遺失就砍一半」叫做加法增加、乘法減少(AIMD),讓共用同一條瓶頸線路的連線大致公平地分享頻寬。 上面描述的是 Reno 一類的做法。現在 Linux、Windows 與 Apple 的系統預設用 CUBIC(RFC 9438),遺失後一樣乘法減少,但用三次函數決定增長的速度; 還有其他演算法依量到的頻寬與延遲調整。下面的模擬器用簡化的 Reno 規則。
TCP 還是 UDP
兩課看下來,可以用應用的需求來選:
| 需求 | 選擇 | 例子 |
|---|---|---|
| 每個位元組都要完整、依序送達 | TCP | 網頁、檔案下載、電子郵件、SSH、資料庫連線 |
| 即時性比完整重要,晚到不如不到 | UDP | 語音通話、視訊會議、直播、線上遊戲 |
| 一問一答、很短,失敗了再問就好 | UDP | DNS、NTP 校時 |
| 要廣播或群播給很多台 | UDP(TCP 只能一對一) | DHCP、區域網路裡的裝置探索 |
| 想要可靠,又不想被 TCP 的設計綁住 | 在 UDP 上自己做 | HTTP/3 用的 QUIC 在 UDP 上自己實作確認、重傳與壅塞控制 |
用 TCP 時,重傳與壅塞控制都由作業系統處理,程式只看到可靠的位元組流;用 UDP 時,這些事要不要做、怎麼做,都由應用程式自己決定。
小測驗:壅塞控制與選擇協定
接收視窗 rwnd 與壅塞視窗 cwnd,各自保護誰?
cwnd 從 1 個區段開始慢啟動,門檻 64,一路沒有遺失。4 輪之後 cwnd 是多少?
為什麼「逾時」時 cwnd 降得比「3 個重複確認」更多?
下列哪一個應用最適合用 TCP?
重點整理
- 遺失靠兩種方法發現:逾時(RTO 依 RTT 計算、逾時後加倍)與 3 個重複確認(快速重傳,不用等逾時)。
- 滑動視窗讓多個區段同時在路上,傳輸量上限約為視窗÷RTT;確認是累積的,遺失一個 ACK 通常不用重送。
- 流量控制:接收端在 ACK 裡告知接收視窗 rwnd;零視窗時傳送端停下並送探測。
- 壅塞控制:傳送端維護 cwnd,實際可送=min(cwnd, rwnd);慢啟動每輪加倍、壅塞避免每輪加 1;重複確認時減半,逾時時回到 1。
- 要完整依序用 TCP;重即時、一問一答或要廣播用 UDP,可靠性需要時由應用程式自己做。
傳輸層到這裡結束。下一課往上到應用層,深入看每次連網的第一步:DNS 怎麼把名稱變成位址。