Home
探索 Uedu
學生控制台
註冊會員/登入
研究知情同意中心
教師控制台
課程設定
支援與訊息
Uptime 數據

UeduGPTs

--

Jupyters

5

UG26 CISOSE26
臺北 AQI 46 · 臺中 AQI 26 · 臺南 AQI 21 · 高雄 AQI 33

AI 回覆桌面通知

AI 助教回覆完成時顯示桌面通知

聊天訊息通知

同學在討論區發送訊息時通知

聲音通知

每當有新通知時播放提示音

協定層與 TCP/IP

協定層與 TCP/IP(進階):你的 1 Gbps 光纖為何只跑得到 200 Mbps

從頻寬延遲乘積、壅塞控制的數學,到 NAT 子網路運算與 HTTP/3 如何繞過隊頭阻塞——深入現代傳輸層的設計權衡。

為什麼你的 1 Gbps 光纖,下載卻只有 200 Mbps?

你升級了家裡的光纖,帳單上寫著 1 Gbps。可是從一台位於美國西岸的伺服器下載大檔案,速度卻卡在 200 Mbps 上不去,而從台灣本地的 CDN 下載卻能輕鬆跑滿。線路明明一樣寬,差別到底在哪?

入門篇告訴我們 TCP 提供「可靠、有序的位元組串流」,並用三次握手建立連線。但它沒回答這個切身的問題:在一條真實、會塞車、有延遲的網路上,TCP 究竟用多快的速度送資料?答案是一條每隔幾毫秒就重新計算一次的動態曲線。理解這條曲線,你就能解釋上面的怪現象,也能看懂為什麼整個業界要花三十年,把壅塞控制(congestion control)演算法翻新一遍又一遍。

這篇進階篇不再重述分層與握手。我們直接鑽進三個入門篇只點到為止的硬核主題:頻寬延遲乘積與壅塞控制的數學位址耗盡逼出來的 NAT 與子網路運算,以及讓 HTTP/3 拋棄 TCP 的隊頭阻塞(head-of-line blocking)

頻寬延遲乘積:管線裡能塞多少水

要理解上面的怪現象,先建立一個關鍵直覺:網路像一條水管。它的「容量」不是只看管子多粗(頻寬 bandwidth),還要看管子多長(延遲 latency)。這個乘積叫做頻寬延遲乘積(BDP, Bandwidth-Delay Product)

$$\text{BDP} = \text{頻寬} \times \text{RTT}$$

協定層與 TCP/IP進階概念示意圖

BDP 代表「為了讓管線時時刻刻都滿載,必須有多少資料同時在飛」。TCP 靠滑動視窗(sliding window) 控制在途未確認(in-flight)的資料量;如果視窗比 BDP 小,管線就會出現空檔,頻寬被白白浪費。

動手算一下

回到開頭的台北↔美西連線。假設往返時間 RTT = 160 ms,我們想跑滿 1 Gbps:

BDP = 1 Gbps × 160 ms
    = 1 × 10^9 bit/s × 0.16 s
    = 1.6 × 10^8 bit
    = 2 × 10^7 byte
    = 20 MB

也就是說,要填滿這條管線,TCP 必須隨時保持 20 MB 的資料在途。問題來了:TCP 標頭裡宣告接收視窗的欄位只有 16 位元,最大 65535 byte(64 KB)。算一下這個上限能撐多少吞吐量:

window_bytes = 65535          # 16-bit 視窗欄位上限
rtt_s = 0.160                 # 160 ms
throughput_bps = window_bytes * 8 / rtt_s
print(f"{throughput_bps / 1e6:.1f} Mbps")   # 3.3 Mbps

只有 3.3 Mbps!這正是 1980 年代原始 TCP 在跨洲連線上的天花板。解法是 TCP 的視窗縮放選項(Window Scale Option):在三次握手時協商一個位移量 $s$,把實際視窗放大為 $\text{window} \times 2^s$,最高可達 1 GB 等級,才足以餵飽現代長肥管線(long fat network)。

但即使視窗夠大,還有第二道關卡:封包遺失率。這就引出壅塞控制的核心數學。

壅塞控制的數學:從 Reno 到 CUBIC

入門篇提過 AIMD(加法增大、乘法減小)與慢啟動。這裡我們把它量化,看看「為什麼遺失率一高,吞吐量就崩」。

經典的 TCP Reno 在穩態下,壅塞視窗 cwnd 每個 RTT 線性加一,一遇到遺失就砍半。把這條鋸齒狀曲線取平均,可推導出著名的 Mathis 公式:吞吐量與遺失率 $p$ 的關係是

$$\text{Throughput} \approx \frac{\text{MSS}}{\text{RTT}} \cdot \frac{C}{\sqrt{p}}$$

其中 MSS 是最大區段大小,$C$ 是常數(約 1.22)。注意那個 $\frac{1}{\sqrt{p}}$——吞吐量與遺失率的平方根成反比。我們來看這有多殘酷:

import math

def reno_throughput_mbps(mss=1460, rtt_ms=160, loss=1e-4):
    rtt = rtt_ms / 1000
    C = 1.22
    bytes_per_s = (mss / rtt) * (C / math.sqrt(loss))
    return bytes_per_s * 8 / 1e6

for p in [1e-5, 1e-4, 1e-3, 1e-2]:
    print(f"遺失率 {p:.0e} → {reno_throughput_mbps(loss=p):6.1f} Mbps")
遺失率 1e-05 → 282.0 Mbps
遺失率 1e-04 →  89.2 Mbps
遺失率 1e-03 →  28.2 Mbps
遺失率 1e-02 →   8.9 Mbps

開頭的謎底揭曉:跨洲連線的長 RTT 配上哪怕只是萬分之一的遺失率,Reno 就把你壓在不到 90 Mbps。線路的「寬度」根本不是瓶頸,RTT 與遺失率的組合才是。本地 CDN 之所以快,正因為 RTT 只有幾毫秒,且路徑短、遺失率極低。

CUBIC:用立方曲線記住上次的高點

Reno 在長肥管線上探測頻寬太慢(線性加一,要好幾秒才爬回高點)。Linux 預設的 CUBIC 改用一條三次方曲線來調整視窗。設 $W_{max}$ 是上次發生遺失時的視窗、$t$ 是距離上次遺失的時間,視窗依下式成長:

$$W(t) = C(t - K)^3 + W_{max}, \quad K = \sqrt[3]{\frac{W_{max}(1-\beta)}{C}}$$

這條曲線的妙處在於它的形狀:剛縮窗後,曲線在 $W_{max}$ 附近平緩(小心翼翼地逼近上次的瓶頸),離 $W_{max}$ 越遠則越來越陡(大膽探測新頻寬)。

def cubic_window(t, w_max=1000, C=0.4, beta=0.7):
    K = (w_max * (1 - beta) / C) ** (1/3)
    return C * (t - K) ** 3 + w_max

for t in [0, 2, 4, 6, 8, 10]:
    print(f"t={t:2d}s  cwnd≈{cubic_window(t):7.1f} 個區段")

關鍵是 CUBIC 的成長與 RTT 無關(只看真實時間 $t$),所以長 RTT 連線不再吃虧,這是它取代 Reno 成為主流的原因之一。

BBR:不再把遺失當作壅塞訊號

Reno 與 CUBIC 都把「封包遺失」當作網路塞車的訊號。但這個假設在現代網路越來越不成立——無線網路會因為干擾而遺失(不是塞車),而過大的路由器緩衝區又會造成緩衝膨脹(bufferbloat):封包不丟,但全卡在佇列裡排隊,延遲飆高。

Google 的 BBR(Bottleneck Bandwidth and RTT) 換了一套哲學:直接量測瓶頸頻寬 $BtlBw$ 與最小往返時間 $RTprop$,然後把在途資料量精準控制在 $BDP = BtlBw \times RTprop$ 附近。它追求的是那個「管線剛好填滿、佇列剛好不排隊」的甜蜜點——這正是排隊理論裡 Kleinrock 早在 1979 年指出的最佳運作點。BBR 不等遺失發生才反應,因此在有損的無線與長肥網路上吞吐量往往大幅領先。

NAT 與子網路:43 億個位址不夠用的後果

入門篇說 IPv4 只有約 43 億個位址早已不夠。它沒說的是:我們是怎麼撐到今天還沒用完的。答案是兩項技術:CIDR 子網路切割NAT(Network Address Translation)

動手算一下:子網路切割

你拿到公司配發的網段 203.0.113.0/24,要切成 4 個部門、每部門獨立子網路。/24 代表前 24 位元是網路前綴,剩 8 位元(256 個位址)是主機空間。切成 4 份,需要再借 2 個位元當子網路編號(因為 $2^2 = 4$):

import ipaddress

net = ipaddress.ip_network("203.0.113.0/24")
for sub in net.subnets(prefixlen_diff=2):   # /24 → /26,多借 2 位元
    hosts = list(sub.hosts())
    print(f"{sub}  可用主機 {hosts[0]} ~ {hosts[-1]}  共 {len(hosts)} 台")
203.0.113.0/26    可用主機 203.0.113.1 ~ 203.0.113.62    共 62 台
203.0.113.64/26   可用主機 203.0.113.65 ~ 203.0.113.126  共 62 台
203.0.113.128/26  可用主機 203.0.113.129 ~ 203.0.113.190 共 62 台
203.0.113.192/26  可用主機 203.0.113.193 ~ 203.0.113.254 共 62 台

注意每個 /26 有 64 個位址,但只有 62 台可用——因為每個子網路的第一個位址(網路位址,如 .0)與最後一個位址(廣播位址,如 .63)被保留。可用主機數的通式是 $2^{(32-n)} - 2$,其中 $n$ 是前綴長度。這個「減 2」是 CIDR 運算最常被學生忽略的陷阱。

NAT:一個公網位址,背後藏一整個家

你家裡可能有十幾台裝置同時上網,但對外只有一個公網 IP。這靠的是路由器上的 NAT,更精確地說是 PAT(Port Address Translation,又稱 NAPT)。它的把戲是:改寫封包的來源位址與來源埠號(source port),並維護一張轉換表記住對應關係。

內網裝置 A  192.168.0.10:51000 ─┐
內網裝置 B  192.168.0.11:51000 ─┤  NAT 改寫
                                 ▼
       對外都變成  203.0.113.5:?????(不同埠號區分)

NAT 轉換表:
┌─────────────────────┬──────────────────────┬─────────────────┐
│ 內部 (IP:port)      │ 對外 (IP:port)       │ 目的地          │
├─────────────────────┼──────────────────────┼─────────────────┤
│ 192.168.0.10:51000  │ 203.0.113.5:60001    │ 93.184.216.34:443│
│ 192.168.0.11:51000  │ 203.0.113.5:60002    │ 93.184.216.34:443│
└─────────────────────┴──────────────────────┴─────────────────┘

回應封包回來時,NAT 查表把目的地址改回內網裝置。這招讓一個公網 IP 服務數萬個連線(埠號有 16 位元,約 6.5 萬個),是 IPv4 苟延殘喘至今的關鍵。

但 NAT 也打破了網際網路「端到端(end-to-end)」的原始設計:外部主機無法主動連入內網裝置(因為轉換表只在內網先發起連線時才建立),這對 P2P、線上遊戲、視訊通話造成困擾,催生了 STUN/TURN/ICE 這類「打洞(hole punching)」技術。IPv6 位址空間夠大($2^{128}$ 個),本意就是要徹底擺脫 NAT,回歸端到端——但現實是 NAT 還會陪我們很久。

隊頭阻塞:為什麼 HTTP/3 把 TCP 給換掉了

入門篇結尾提到 QUIC 與隊頭阻塞,這裡我們把它講透。

隊頭阻塞(head-of-line blocking, HoL blocking) 指的是:一列隊伍中,只要排頭那個人卡住,後面所有人都得乾等,即使他們本身早就準備好了。

TCP 的可靠有序保證,恰恰製造了這個問題。現代網頁會在單一 TCP 連線上同時傳輸幾十個檔案(HTTP/2 的多工 multiplexing)。但 TCP 在它眼中只是「一條位元組串流」,並不知道上層其實是多個獨立檔案。於是:

單一 TCP 連線傳三個檔案:  [檔A片段][檔B片段][檔C片段][檔A片段]...
                                       ↑ 這個封包遺失了

TCP 的反應:在 B 的這個片段重傳到齊前,
            「按序交付」規則逼迫 C 的片段也得在緩衝區裡乾等
            ── 即使 C 早就完整收到!

檔 C 明明跟檔 B 毫無關係,卻因為 TCP 的有序保證被牽連卡住。連線數越多、遺失率越高,這個懲罰越重。

QUIC 的解法是:把「串流(stream)」的概念下放到傳輸層自己管理。QUIC 跑在 UDP 之上,在內部維護多條彼此獨立的串流,各自有獨立的序號與重傳。某條串流掉了一個封包,只會阻塞那一條串流,其他串流照常交付。這就根除了傳輸層的隊頭阻塞。

QUIC 還順手解決了另外兩件事:

  1. 連線建立更快。傳統 HTTPS 要先 TCP 三次握手,再 TLS 握手,來回好幾趟。QUIC 把傳輸與加密握手合併,新連線最快 1-RTT,重訪甚至 0-RTT 就能開始傳資料。
  2. 連線遷移(connection migration)。TCP 連線由「四元組」(來源 IP、來源埠、目的 IP、目的埠) 定義,你從 Wi-Fi 切到行動網路、IP 一變,連線就斷了。QUIC 用一個獨立的連線 ID(Connection ID) 辨識連線,換網路也不中斷——這對手機使用者體驗是巨大的改善。

這就是為什麼 HTTP/3 直接建立在 QUIC 上,而 QUIC 又建立在 UDP 上。繞了一大圈,我們又回到那個輕量、不可靠的 UDP——只是這次,可靠性、有序性、加密與多工,全部由 QUIC 在使用者空間重新、而且更聰明地實作了一遍。

重點回顧

  • 頻寬延遲乘積(BDP) = 頻寬 × RTT 決定要多少在途資料才能填滿管線;長肥網路必須靠視窗縮放才不會被 64 KB 的舊上限卡死。
  • Reno 的吞吐量正比於 $\frac{1}{\sqrt{p}}$(Mathis 公式),所以高 RTT + 微小遺失率就能讓跨洲連線遠遠跑不滿線路寬度。
  • CUBIC 用三次方曲線、成長與 RTT 解耦,在長肥網路勝過 Reno;BBR 改以量測頻寬與最小 RTT 為訊號,避開緩衝膨脹與「把無線遺失誤判為壅塞」的陷阱。
  • CIDR 子網路每段可用主機數是 $2^{(32-n)}-2$(保留網路與廣播位址);NAT/PAT 靠改寫埠號讓一個公網 IP 服務上萬連線,代價是破壞端到端連通性。
  • 隊頭阻塞源自 TCP 的有序保證;QUIC/HTTP3 把多串流下放到 UDP 之上各自重傳,並支援 0-RTT 與連線遷移,從根本上繞過了它。

深入探討(研究所視角)

把上面的線索收束起來,會看到一個更深的張力:TCP 的設計把「可靠」「有序」「壅塞控制」三件事綁死在同一個位元組串流的抽象裡,而這個抽象正在被現代網路逐一拆解。

第一條拆解線是控制迴路的訊號選擇。Reno/CUBIC 是遺失型(loss-based),把佇列填滿、丟封包當作回饋訊號,本質上會誘發 bufferbloat;BBR 是模型型(model-based),主動建構瓶頸頻寬與傳播延遲的模型,逼近 Kleinrock 的最佳點。但兩者共存於同一網路時的公平性是個尚未完全解決的研究難題——BBR 早期版本在與 CUBIC 競爭時會搶走過多頻寬,BBRv2/v3 為此加入了對遺失與 ECN(Explicit Congestion Notification) 訊號的回應,試圖更「禮貌」。這牽涉到控制理論裡的穩定性與收斂性分析,也牽涉到賽局論裡的公平分配。

第二條拆解線是把可靠性從核心搬到使用者空間。TCP 寫在作業系統核心裡,演進速度受制於全球作業系統的更新週期——一個新演算法要普及可能要十年。QUIC 在使用者空間實作,應用程式自帶協定堆疊,讓 Google、Meta 這類業者能以週為單位部署實驗與迭代。代價是 CPU 開銷較高(少了核心與網卡的硬體卸載),以及中間設備(middlebox)對 UDP 的不友善——這又回頭推動了 UDP 卸載與 GSO/GRO 等核心優化。

第三條線最根本:端到端原則(end-to-end principle) 的侵蝕與重建。NAT、防火牆、透明代理這些「中間設備」違背了「網路只負責傳遞、智慧放在端點」的原始信條,造成協定僵化(protocol ossification)——中間設備會偷看甚至改寫 TCP 標頭,導致任何新傳輸協定都難以上線。QUIC 的回應相當激進:把幾乎整個傳輸標頭都加密,讓中間設備什麼都看不到、無從干預,從而為傳輸層的未來演進「保留進化空間」。這是一個耐人尋味的轉折——為了讓協定能繼續演化,我們選擇對網路核心隱藏資訊。從 Saltzer、Reed 與 Clark 1984 年的端到端論文,到今天 QUIC 的加密傳輸層,這條主線串起了整部網路協定的演化史,也提醒我們:再穩固的基礎協定,都不是終點,而是下一輪設計權衡的起點。

AI 共讀助教正在陪你讀:協定層與 TCP/IP(進階):你的 1 Gbps 光纖為何只跑得到 200 Mbps
嗨!我是這篇文章的共讀助教,只根據〈協定層與 TCP/IP(進階):你的 1 Gbps 光纖為何只跑得到 200 Mbps〉的內容回答。可以問我「解釋某段」「舉個例子」「出題考我」,或反白文中段落後點下方「解釋選取段落」。