JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

UDP 與 TCP:兩種送達哲學

在同一張不可靠的網路上,兩種傳輸層通訊協定下了相反的賭注:UDP 保持輕薄,只是把你的訊息往目的地一丟;TCP 則用一整套機制把它包起來,送達一條有序、可靠的位元組串流。搞懂這兩者為何各自存在,正是傳輸層的核心。

同一張網路,兩個相反的賭注

在上一篇指南裡,你看到了連接埠與通訊端如何讓單一一台主機同時進行許多對話,也看到傳輸層是第一個「行程對行程」(而非「主機對主機」)對話的一層。但傳輸層在同一個概念之上,提供了兩種非常不同的產品,而本篇指南的全部用意,就是搞懂為什麼這兩者都要存在。它們是 UDP(使用者資料包協定)與 TCP(傳輸控制協定)。

這裡有個關鍵的背景。位在它們倆底下的網路層——也就是 IP——幾乎什麼都不保證。它會盡最大努力把封包送到目的地,但封包仍可能在途中遺失、被複製、被打亂順序,或悄悄地被損毀。我們把這稱為盡力而為(best-effort)的服務,而這是個刻意的設計抉擇:讓核心保持笨拙而簡單,正是當初讓網際網路得以擴展的原因。所以傳輸層的工作,是決定要為這種不可靠做多少事(甚至做不做)——而 UDP 與 TCP 用相反的方式回答了這個問題。

這個分裂,正是端到端原則的實際展現:與其把可靠性建進盡力而為核心裡的每一台交換器,不如把聰明的部分推到兩端的主機去,讓每個應用程式各自選擇要其中的多少。UDP 說「給我最低限度就好」;TCP 說「給我全套服務」。兩者都不是對方的故障版本;它們是為兩種不同工作打造的兩件完成品。

UDP:輕薄而誠實的包裝

UDP 簡單到幾乎令人吃驚。它接過你的訊息,貼上一個小小的 8 位元組標頭,就交給 IP。那個標頭只帶四樣東西:來源連接埠、目的連接埠、長度,以及一個檢查碼(針對資料、用來偵測錯誤的選用碼)。這就是整個協定。UDP 是非連線導向的——沒有建立連線的對話、沒有交握,你直接送就是了。它不做重送、不維持順序,也不保證你的訊息真的會送達。人們把這稱為盡力而為的傳遞:它會試試看,僅此而已。

UDP 相較於原始 IP,真正多加的唯一一項服務是多工:那兩個連接埠號碼讓接收端的主機能把訊息交給正確的行程,正如上一篇指南所描述的。除此之外的一切,UDP 都刻意省略掉了。而這種省略,本身就是它的特色。如果你的應用程式能忍受偶爾遺失一則訊息、卻無法忍受等待,那麼 UDP 的輕薄就是一份禮物。

誰會想要這樣?即時語音與視訊——VoIP 與視訊通話——寧可跳過一個微小的卡頓,也不願為了一個早已過時、來不及播放的重送封包而把整通電話凍住。你在前一級階梯裡追蹤過的 DNS 查詢,會射出一個微小的請求、想要一個微小的回覆,且不願付建立連線的代價。線上遊戲送出大量的位置更新,其中只有最新的那筆才重要。對以上這些情境,TCP 那套小心翼翼的機制反而會幫倒忙,而 UDP 才是正確、刻意的選擇——不是壞掉的選擇。

TCP:可靠、有序的位元組串流

TCP 下了相反的賭注:拿底下那張同樣不可靠的 IP,在它之上打造出一條完美管線的「幻覺」。當你在一端把資料寫進一條 TCP 連線時,一模一樣的位元組會從另一端出來,順序一模一樣,沒有任何遺漏、也沒有任何重複。這就是位元組串流的抽象:你不是在送一則則訊息,而是寫進一條串流,像把水倒進一條水管。TCP 自己會決定如何把那條串流切成一塊塊、稱為區段的東西來上路,而接收端會把它們重組得天衣無縫,讓應用程式完全看不見接縫。

它是怎麼在一張不可靠的網路上達成可靠的?靠的是一小組機制,組合起來就成了可靠資料傳輸——下一篇指南會把這具引擎一根螺絲一根螺絲地拆開。簡言之:每個位元組都會拿到一個序號,好讓接收端能偵測缺口並重排順序;接收端會回送一個確認(ACK),表示「直到第 N 個位元組為止,我都收到了」;若一個 ACK 沒有及時回來,重送計時器就會到時,觸發重送;而檢查碼則攔下損毀。遺失、亂序、重複、損毀——每一種都有對應的補救。

由於 TCP 是連線導向的,兩端會先進行一段建立連線的對話——也就是著名的三向交握,一段「你聽得到我嗎?——聽得到,你呢?——聽得到」、形式為 SYN、SYN-ACK、ACK 的對話——好在任何真正的資料流動之前,先就起始序號達成共識。完事之後,它們又會乾淨俐落地拆掉連線。這兩場儀式,以及交握真正在做的事(它同步的是序號,而不是身分驗證),就是本級第四篇指南的整個主題。

                 UDP datagram                 TCP segment
              +----------------+         +------------------------+
  header:     | src port       |         | src port | dst port    |
  (8 bytes    | dst port       |         | sequence number        |
   for UDP,   | length         |         | acknowledgment number   |
   20+ bytes  | checksum       |         | flags (SYN/ACK/FIN..)   |
   for TCP)   +----------------+         | receive window | chksum |
              | your data...   |         +------------------------+
              +----------------+         | your data...           |
                                         +------------------------+

  UDP: 4 fields, then go.   TCP: seq#, ack#, window, flags -> reliability.
兩個標頭,兩種哲學:UDP 那 8 位元組的標頭只負責多工;TCP 較大的標頭則帶著序號、ACK、旗標與視窗,用來驅動一條可靠、有序、受流量控制的串流。

流量控制:別把接收端淹沒

可靠並不是 TCP 唯一的工作。想像一台快速的伺服器,對著一個讀取很慢的微小感測器猛灌資料。就算每個位元組都完美抵達,感測器的接收緩衝區仍會滿溢,多出來的部分會被丟棄——這是把資料可靠地送進一個沒有底的桶子裡。流量控制就是 TCP 的解法:接收端不斷告訴發送端它目前還有多少空閒的緩衝空間,這個數字裝在每個 ACK 都會帶的一個欄位裡,稱為接收視窗。發送端承諾,在外飛行、尚未被確認的資料,永遠不會超過那個視窗所允許的量。

它的運作像一個滑動的視窗。假設接收端公告了一個 4000 位元組的視窗,那麼發送端最多只能有 4000 位元組懸而未決(已送出但尚未被確認)。隨著 ACK 一個個回來,視窗向前滑動,便可再送出更多;若接收端的應用程式讀取得慢,公告出來的視窗就會縮小,甚至縮到零,發送端便會暫停。這就是你之後會再遇見、作為可靠傳輸核心的滑動視窗——在這裡,它純粹是用來讓發送端的步調與接收端相匹配。

隱藏的代價:行頭阻塞

TCP 嚴格的「按序送達」有一道值得替它命名的利刃,因為它能解釋許多現代協定的設計。由於 TCP 必須照順序把位元組交給應用程式,單單一個遺失的區段就會卡住排在它後面的一切。假設區段 1、2、3、4 都送了出去,卻只有區段 2 遺失。區段 3 和 4 平安抵達——但 TCP 還不能把它們交付出去,因為位元組順序必須被維持。它們會等著,明明已完整收到,卻得等到區段 2 被重送、抵達為止。這種停滯就是行頭阻塞:排在隊伍最前頭的那一項,把它後面所有人都凍住了。

當單一一條 TCP 連線承載許多彼此獨立的串流時,這道刺最痛。載入一個網頁會拉下數十個檔案;若它們共用同一條連線,檔案 A 上單單一個遺失的封包,就會無謂地卡住與該遺失毫無關係的檔案 B、C、D。正是這種痛,推動了 QUIC 的設計——一種較新、跑在 UDP 之上、並以「每串流為單位」重建可靠性的傳輸協定,於是某一條串流的遺失不再會阻塞其他串流。HTTP/3 不過就是承載在 QUIC 之上的 HTTP,而它存在的一大原因,就是要逃離 TCP 的行頭阻塞。說來諷刺,UDP 竟成了下一個可靠協定的地基。

在你繼續往上爬之前,最後做一次誠實的檢查。TCP 給你的是可靠、有序的位元組——但可靠不等於安全,而 TCP 本身什麼都不加密。路徑上的竊聽者能讀到每一個位元組。機密性與身分驗證來自疊在上頭的 TLS,那是後面某一級階梯裡另一篇指南的主題。把這幾條軸線在腦海裡分清楚:可靠性講的是遺失與順序;安全性講的是保密與身分。TCP 替你買到了前者,卻一點也買不到後者。