總覽與定位
封包已經走到 L4 門口:這一層要決定的不再是「怎麼送達」,而是「送達要不要保證」。
分層模型篇把一個封包送到了主機門口——IP 負責找到哪一台機器,port 負責找到哪一個程式。 從這裡開始,傳輸層(L4)要回答的是另一個問題:這條資料通道,要給應用什麼樣的保證? TCP 與 UDP 就是兩個答案完全相反的協定,而它們的差異會一路影響到本系列後面每一個協定的設計。
network-layers-for-dev 講完封包如何被逐層封裝、如何一跳跳穿過交換器與路由器抵達主機;
nat-port-forwarding 講完 NAT 如何靠「IP + port」把連線映射回內網,以及誰能主動連誰。
這兩件事共同決定了本篇的起跑線:位址與路徑都已就緒,剩下的是端到端的傳輸語義。
TCP 給你一條可靠、有序的位元組串流(Byte Stream);UDP 給你一個盡力而為的資料報(Datagram)通道。
本篇只談這一層,不再重講 IP 子網切分與 LAN 路由細節。
- 先三向交握建連線,才有資料通道
- 序號 + 確認 + 重傳(Retransmission)=不掉、不重複
- 依序交付:應用讀到的順序等於送出順序
- 自帶流量控制(Flow Control)與壅塞控制(Congestion Control)
- header 20 bytes 起跳,狀態存在雙方核心裡
- 沒有交握、沒有連線狀態,送了就走
- 不保證到達、不保證順序、不保證只到一次
- 保留訊息邊界:一次送、一次收,不會黏在一起
- 不做流量控制與壅塞控制——由應用自己負責
- header 只有 8 bytes:來源 port、目的 port、長度、checksum
| 面向 | TCP | UDP | 對應用的實際意義 |
|---|---|---|---|
| 連線 | 面向連線(先交握) | 無連線 | TCP 有 1-RTT 的建連成本,UDP 第一個封包就能帶資料 |
| 可靠性 | 重傳直到確認 | 不重傳 | TCP 用延遲換到達,UDP 用到達率換延遲上限 |
| 順序 | 依序交付 | 抵達順序即交付順序 | TCP 前面塞住後面也讀不到;UDP 亂序由應用處理 |
| 資料邊界 | 無(位元組串流) | 有(資料報) | TCP 應用必須自己做 framing,UDP 天然一則一則 |
| 速率控制 | 內建雙重視窗 | 無 | UDP 應用不自律就會壓垮網路與自己 |
兩種契約沒有優劣,只有「這個應用能不能忍受掉包,以及能不能忍受等待」。
TCP 的可靠性機器:交握、序號與重傳
「可靠」不是形容詞,是四個具體機制:初始序號同步、累積確認、重傳、有序交付。
TCP 連線的一生從三向交握(Three-way Handshake)開始。很多人記得「三次」,卻答不出為什麼是三次: 因為要同步的不是一個方向,而是兩個方向各自的初始序號(ISN, Initial Sequence Number)。 A 告知並被確認、B 告知並被確認,總共四件事——中間兩件可以合併成一個封包,於是變成三次。
SYN_SENT:提出自己的初始序號 x,同時協商 MSS、視窗縮放、SACK 等選項。SYN_RECV,確認 x 並提出自己的初始序號 y。這條半完成的連線被放進核心的 SYN queue。ESTABLISHED;伺服器收到後把連線移進 accept queue,等應用呼叫 accept() 領走。一次來回(RTT,Round-Trip Time,封包往返一趟的時間)就能完成交握——但這 1-RTT 是 TCP 相對 UDP 的固定起手成本:TLS 篇的 0-RTT 只省得掉 TLS 交握那一趟、省不掉它;QUIC 篇的交握合一則把它和 TLS 交握併成同一趟。
連線建立後,TCP 眼中的資料是一條連續編號的位元組串流:每個 byte 都有序號,接收端用累積確認(Cumulative ACK) 回報「我已經連續收到 n 之前的所有資料,下一個請給我 n」。掉包的偵測與補救有兩條路:
| 機制 | 觸發條件 | 反應速度 | 對壅塞視窗的影響 |
|---|---|---|---|
| 超時重傳(RTO) | 計時器到期仍未收到 ACK | 慢(RTO 以 RTT 估計值為基礎,且會指數退避) | 視為嚴重壅塞:視窗砍回 1 |
| 快速重傳(Fast Retransmit) | 連收 3 個重複 ACK | 快(不必等計時器) | 視為輕度壅塞:視窗減半 |
| SACK(Selective ACK) | 選項啟用時隨 ACK 附帶 | — | 告知「哪些片段已收到」,避免整批重送 |
現代 Linux 預設開啟 SACK:發送端因此能只補洞、不必把整個視窗重來一遍。這個細節在本篇的互動模擬器裡看得到。
你呼叫三次 send() 各送 100 bytes,對端可能一次 recv() 就讀到 300 bytes,也可能分五次讀到。
TCP 的契約只有「位元組順序不變」,從來沒有承諾保留你的訊息切分。
所以任何跑在 TCP 上的協定都必須自己做 framing:長度前綴(HTTP/2、gRPC)、分隔符(HTTP/1.1 的 \r\n\r\n)或固定長度。
HTTP 篇的 Content-Length 與 HTTP/2 frame、WebSocket 篇的 framing、MQTT 篇的 Remaining Length,都是對這件事的回答——每個上層協定都得先解決它。
關閉則是四次揮手:兩個方向各自 FIN / ACK,因為 TCP 是全雙工,一方沒話說不代表另一方也講完了。
主動關閉的一方會停在 TIME_WAIT 約 2 倍 MSL,用來吸收延遲抵達的舊封包、並確保最後那個 ACK 有機會重送:
這解釋了一個經典現象:服務重啟時出現 Address already in use,正是舊連線還卡在 TIME_WAIT;
解法是 SO_REUSEADDR,而不是盲目調低 TIME_WAIT(那會讓舊封包有機會污染新連線)。
流量控制與壅塞控制
兩個都是「別送太快」,但保護的對象完全不同:一個保護接收端,一個保護整條網路。
面試最常混淆的一組概念。分清楚只要問一句:是誰喊停?
| 流量控制(Flow Control) | 壅塞控制(Congestion Control) | |
|---|---|---|
| 保護誰 | 接收端的緩衝區 | 中間網路(路由器佇列) |
| 誰喊停 | 接收端明示:ACK 裡的 window 欄位 | 沒人明示,發送端自己從丟包/延遲推測 |
| 控制變數 | rwnd(接收視窗) | cwnd(壅塞視窗,發送端私有) |
| 失效後果 | 接收端 buffer 溢位、資料被丟 | 全網佇列爆滿,吞吐量崩潰(congestion collapse) |
兩者同時生效,發送端每一刻能送出的量取兩者較小值。
發送端真正能送出而未被確認的資料量是:
其中 rwnd 由接收端在每個 ACK 中告知。若接收端應用讀取太慢,rwnd 會一路縮到 0(zero window), 發送端就只能定期送 window probe 等待對方鬆口——這就是 TCP 層的 backpressure,稍後在第 6 節〈I/O 模型與 Backpressure〉會看到它如何一路傳導回應用程式碼。
滑動視窗(Sliding Window)是實作方式:發送端維護一段「已送出但未確認」的區間, 收到累積 ACK 就把左邊界往右滑,空出額度再送新資料。視窗大小決定了管線能填多滿:
視窗若小於 BDP,鏈路永遠跑不滿——這就是「跨洲傳大檔案很慢,但不是因為頻寬不夠」的根因, 也是為什麼 window scaling 選項(把 16-bit 視窗欄位放大)是長肥管線的必要配備。
壅塞控制則是一套「試探-退讓」的迴圈。以經典的 Reno 家族為例:
- 慢啟動(Slow Start):cwnd 從 1 個 MSS 起跳,每個 RTT 翻倍——名字叫慢,成長其實是指數。
- 壅塞避免(Congestion Avoidance):cwnd 超過門檻 ssthresh 後改為每 RTT 加一,線性試探。
- 快速重傳/快速恢復:收到 3 個重複 ACK,判定只是輕度丟包,ssthresh 與 cwnd 減半後繼續。
- 超時:判定網路嚴重惡化,cwnd 直接砍回 1,重新慢啟動。
| 演算法 | 壅塞訊號 | 特性 | 典型場景 |
|---|---|---|---|
| Reno / NewReno | 丟包 | 教科書基準,成長保守 | 已少見於預設值 |
| CUBIC | 丟包 | 以三次函數探測,高 BDP 下回復快;Linux 預設 | 一般資料中心與公網服務 |
| BBR | 頻寬與 RTT 估計 | 不以丟包為準,主動避開緩衝區膨脹(bufferbloat) | 長肥管線、高丟包無線鏈路 |
關鍵區分:CUBIC 把「丟包」當成網路已經塞住的證據;BBR 認為丟包可能只是無線干擾,改用「送多快才不會讓佇列變長」來判斷。
TCP 承諾依序交付,於是產生一個結構性代價:Head-of-Line Blocking(HoL Blocking,隊頭阻塞)—— 序號較小的那個片段丟了,即使後面的資料早就安穩躺在接收端核心的緩衝區裡,也不能交給應用, 必須等破洞被重傳補上。這是可靠有序這個承諾的必然結果,不是實作缺陷。 請先記住這個名詞:它會在 HTTP/2 的多路復用(Multiplexing)與 QUIC 的設計動機裡再次登場,而且是主角。
UDP:最小契約,與它真正的用武之地
UDP 不是「殘缺的 TCP」,而是刻意保留的一塊空白——讓應用自己決定要什麼保證。
UDP 的 header 只有 8 bytes:來源 port、目的 port、長度、checksum。它做的事情僅僅是 「把 IP 的主機到主機,延伸成程式到程式」。沒有連線、沒有狀態、沒有重傳、沒有視窗。 但這份極簡帶來三個 TCP 給不了的特性:
| UDP 的特性 | 技術原因 | 誰需要它 |
|---|---|---|
| 延遲有上限 | 不重傳、不排隊等破洞 | 語音/視訊:晚到的音框不如直接丟掉補插值 |
| 保留訊息邊界 | 一次 sendto 對應一次 recvfrom | DNS 查詢、metrics、syslog:天然一則一則 |
| 一對多 | 支援廣播與多播 | 服務發現(mDNS)、區網探測 |
| 可自訂可靠性 | 應用層自己疊 ACK/序號/FEC | 遊戲狀態同步、QUIC 這類自建 transport |
最後一列是關鍵:選 UDP 常常不是為了「不要可靠性」,而是為了要一種 TCP 沒提供的可靠性。
典型的取捨是「部分可靠」:即時遊戲對玩家座標更新根本不需要重傳(下一個封包就會帶來更新的位置), 但對「玩家開了一槍」這種事件則需要確認與重送。TCP 無法區分這兩者——它一視同仁地保證全部; UDP 讓應用自由選擇要對哪些訊息付出可靠性成本。同樣的思路,也支撐了在 UDP 之上重建整個可靠傳輸層的做法(系列後面的 QUIC 篇會專門處理)。
1.「UDP 比較快」——快在沒有交握等待、沒有重傳造成的隊頭停等,不是在頻寬。同樣的鏈路上,穩定大檔案傳輸 TCP 通常更快,因為壅塞控制會把管線填滿。
2.「UDP 沒有大小限制」——單一資料報超過路徑 MTU 就會觸發 IP 分片,而只要掉一片,整個資料報都作廢。實務上把應用層 payload 控制在約 1200 bytes 以內最安全。
3.「反正是 UDP,狂送就好」——沒有壅塞控制的高速 UDP 流會擠垮共用鏈路上的 TCP 流,最終連自己也一起賠進去。自建 UDP 協定必須自己實作速率控制。
| 需求 | TCP 直接給你 | UDP 上要自己做 |
|---|---|---|
| 知道對方收到了 | 內建 ACK | 自訂應用層 ACK 與序號 |
| 補回遺失資料 | 重傳 | 自行重送,或用 FEC 冗餘編碼直接復原 |
| 順序正確 | 排序緩衝 | 自帶序號並在應用層重排(或乾脆不排) |
| 不壓垮網路 | cwnd 演算法 | 自行實作速率估計與退讓 |
| 連線概念 | 五元組 + 狀態機 | 自訂 session id(好處:換網不斷線) |
這張表是所有「在 UDP 上造輪子」專案的工作清單——每一列都是實打實的工程量。
Socket API:一條連線在程式碼裡的一生
bind / listen / accept / connect 不是四個 API,而是連線狀態機在使用者空間的四個掛載點。
Socket 是作業系統給的通訊端點抽象,在程式裡就是一個檔案描述符(fd)。 而一條 TCP 連線在核心裡的唯一識別,是眾所皆知的五元組:
這也解釋了「一台伺服器只開一個 port,為什麼能同時服務幾萬個客戶端」——因為每個客戶端的來源 IP/port 不同,五元組就不同。 真正的上限不是 65535 個 port,而是 fd 數量與記憶體。
EINPROGRESSTIME_WAITsendto/recvfrom 直接收送,connect() 只是記住預設對端# Server
fd = socket(AF_INET, SOCK_STREAM, 0) # 建立端點,尚未綁定任何位址
setsockopt(fd, SO_REUSEADDR, 1) # 允許重啟時立刻重新綁定(跳過 TIME_WAIT 殘留)
bind(fd, "0.0.0.0:8080") # 綁 0.0.0.0 = 所有網卡;綁 127.0.0.1 則外部永遠連不到
listen(fd, backlog=1024) # 轉被動;核心開始接受交握、填充 accept queue
while True:
conn, peer = accept(fd) # 取出已完成三向交握的連線,conn 是新的 fd
handle(conn)
# Client
fd = socket(AF_INET, SOCK_STREAM, 0)
connect(fd, "203.0.113.7:8080") # 送出 SYN;來源 port 由核心從 ephemeral 範圍挑選
注意 accept() 的語義:交握已經由核心做完了,應用只是來領取結果。
accept()。塞滿時核心預設直接丟掉那個 ACK,客戶端卻以為連上了,於是出現「連得上但一直沒回應」的詭異現象。因為交握由核心完成,即使你的應用整個卡死在某個鎖上、完全沒在呼叫 accept(),
客戶端的 connect() 仍會在 accept queue 未滿之前成功。
這正是「TCP 探針顯示健康、實際請求全部逾時」的成因——健康檢查請用應用層(送一個真的請求),不要只用 TCP connect。
線上看佇列堆積:ss -ltn 的 Recv-Q 就是目前等待 accept 的連線數,Send-Q 是佇列上限。
綁定了 port,不等於外面連得到你。這一段直接接回 nat-port-forwarding 的結論:
bind() 只決定「本機哪個位址上的封包歸這個 socket」,而「封包能不能走到這台機器」是路徑上 NAT 與防火牆說了算:
| 情境 | 能不能被主動連上 | 原因(回扣 NAT 篇) |
|---|---|---|
綁 127.0.0.1:8080 | 不能 | 只在 loopback 上收;就算做了 Port Forwarding 也送不進去。容器裡最常見的踩雷 |
綁 0.0.0.0:8080,位於 NAT 之後 | 不能(預設) | NAT 表只有出站建立的映射列;入站封包找不到對應列會被丟棄 |
| NAT 上設定 Port Forwarding(DNAT) | 可以 | 預先建立靜態映射,把公有 IP:port 改寫成內網主機:port |
| 位於 CGNAT/雙層 NAT 之後 | 不能 | 你控制不了上層 NAT,開洞無效;只能走 Tunnel 或改由內往外建連 |
客戶端主動 connect() 出去 | 可以 | 出站流量會在 NAT 上自動建立映射,回程封包因此有路可還原 |
最後一列是整個即時通訊生態的地基:因為「內往外」永遠通、「外往內」預設不通,所以裝置端一律主動建立並長期維持一條連線,之後所有雙向訊息都在這條連線上跑。本系列後面的 WebSocket 與 MQTT 都建立在這個前提上。
NAT 表項與有狀態防火牆的連線紀錄都有 idle timeout(家用路由器常見數分鐘到數十分鐘,行動網路可能更短)。
一條完全沒有流量的 TCP 長連線,會在你毫無察覺的情況下被中間設備悄悄拆掉——兩端的 socket 都還是 ESTABLISHED,但封包已經到不了對方。
這就是為什麼長連線一定要有應用層 Keep-Alive 或心跳(Heartbeat):它既是活性偵測,也是「餵養」NAT 映射的手段。
I/O 模型與 Backpressure
從 thread-per-connection 到 epoll 事件迴圈;以及當對端讀不動時,你的程式該做什麼。
拿到 fd 之後,「怎麼等資料」的策略決定了一台機器能扛多少連線。
| 模型 | 作法 | 連線數上限的瓶頸 | 適用 |
|---|---|---|---|
| Blocking I/O | 一連線一執行緒,read() 直接卡住 | 執行緒堆疊記憶體與 context switch 成本 | 連線數少、邏輯簡單的內部服務 |
| Non-blocking + 輪詢 | fd 設 O_NONBLOCK,沒資料回 EAGAIN | 空轉燒 CPU | 幾乎不單獨使用 |
| select / poll | 一次等多個 fd | 每次呼叫都要傳入並掃描全部 fd,O(n) | 可攜性需求、fd 數量小 |
| epoll / kqueue / IOCP | 核心維護興趣集合,只回報就緒的 fd | 成本隨就緒數而非總連線數成長 | C10K 以上的長連線服務 |
| io_uring | 共享環形佇列提交/完成事件 | 系統呼叫次數大幅下降 | 高 IOPS、新版核心 |
從 select 到 epoll 的躍遷關鍵不是「更快」,而是把複雜度從「總連線數」換成「當下有事的連線數」——這正是十萬級長連線得以成立的原因。
fd = socket(); set_nonblocking(fd);
ep = epoll_create1(0);
epoll_ctl(ep, ADD, listen_fd, EPOLLIN);
while (true) {
n = epoll_wait(ep, events, MAX, timeout); # 只回報「就緒」的 fd
for (ev in events[0..n]) {
if (ev.fd == listen_fd) accept_all(); # ET 模式必須 accept 到 EAGAIN 為止
else if (ev.mask & EPOLLIN) read_until_eagain(ev.fd);
else if (ev.mask & EPOLLOUT) flush_send_queue(ev.fd); # 補送上次沒寫完的資料
}
}
Level-Triggered(預設)只要還有資料就持續通知;Edge-Triggered 只在狀態變化時通知一次,因此必須把 fd 讀/寫到 EAGAIN,否則資料會卡在核心裡永遠等不到下一次事件。這是 ET 模式最經典的 bug。
事件迴圈解決了「同時等很多連線」,但沒解決另一半問題:對端讀不動的時候怎麼辦。
Backpressure(背壓)就是這個問題的答案,而它其實是第 3 節談流量控制時那個 rwnd 一路傳導上來的結果:
接收端應用讀得慢 → 接收端核心 buffer 滿 → rwnd 縮到 0 → 發送端不能再送 → 發送端核心 send buffer 滿 →
write() 在阻塞模式下卡住、在非阻塞模式下回 EAGAIN。
那個 EAGAIN 就是網路對你的應用說「慢一點」。
最常見的錯誤處理:write() 回 EAGAIN 時,把送不出去的資料塞進一個無上限的使用者態佇列,然後告訴自己「之後再送」。
這等於用你的堆積記憶體去補償網路的速度差——只要有一個慢速消費者(slow consumer),記憶體就會被單一連線吃光,最後整個行程 OOM,
所有連線一起陪葬。原本只是一個人網路差,變成全體服務中斷。
| 處理策略 | 作法 | 適用資料 |
|---|---|---|
| 暫停生產 | 通知上游停止產生資料,直到 EPOLLOUT 就緒 | 檔案傳輸、串流轉發,資料不可丟 |
| 丟棄與合併 | 只保留最新狀態,覆蓋佇列中的舊值 | 行情報價、玩家座標、儀表板指標 |
| 有界佇列 + 斷線 | 佇列超過上限就主動關閉這條連線 | 推播閘道:犧牲一個慢客戶端保全整體 |
| 降級取樣 | 降低發送頻率或解析度 | 視訊、日誌、追蹤資料 |
四種策略的共同點:都預先決定了「撐不住時要犧牲什麼」。沒有明確答案的系統,預設答案就是 OOM。
$ ss -ltn # 監聽中的 socket
State Recv-Q Send-Q Local Address:Port
LISTEN 0 1024 0.0.0.0:8080 # Recv-Q = 等 accept 的連線數,Send-Q = backlog 上限
$ ss -tn state established
Recv-Q Send-Q Local Address:Port Peer Address:Port
0 262144 10.0.0.5:8080 10.0.0.9:51234 # Send-Q 持續堆高 = 對端來不及收
131072 0 10.0.0.5:8080 10.0.0.9:51235 # Recv-Q 堆高 = 你自己來不及讀
看 ss 的兩個佇列就能立刻定位 backpressure 發生在哪一端:Send-Q 堆積是對方慢,Recv-Q 堆積是自己慢。
EAGAIN 就是 TCP flow control 傳到應用層的 backpressure 訊號——正確反應是暫停、丟棄或斷線,絕不是無上限地往記憶體堆。
互動 · TCP 可靠傳輸模擬器
親手在傳輸中途弄丟一個封包,看 TCP 如何重傳與排序,而 UDP 如何若無其事地留下一個永久缺口。
下面的模擬器把一段資料切成 8 個片段(seq 1–8)。點擊發送端任一個尚未確認的片段,就能預先注入丟包; 按「下一步」逐步推進,每一輪分成「發送」與「回應」兩個階段。切到 UDP 模式再跑一次,對照兩者的差別。
三個值得停下來觀察的畫面:(1) TCP 模式下對 seq 4 注入丟包(前兩輪先讓視窗長到 4),第三輪送出 4–7 時,5、6、7 會全部亮成「亂序緩衝」——它們已經安穩躺在接收端,卻因為前面缺一塊而不能交給應用,這就是 HoL Blocking 的具體樣貌。 (2) 下一輪只會補送 seq 4,5、6、7 因為已被 SACK 告知而跳過不重傳;同時 cwnd 立刻減半,順利的幾輪則從指數翻倍轉為線性加一——慢啟動與壅塞避免的分界就在 ssthresh。 (3) 切到 UDP 模式重跑:遺失的片段直接變成永久缺口,接收端毫不猶豫地把後面的資料交給應用——沒有重傳、沒有等待,也沒有人會告訴發送端出事了。
面試折衷與實務選型
面試官問的從來不是「TCP 和 UDP 差在哪」,而是「你這個場景為什麼選它、代價是什麼」。
| 決策點 | 選項 | Trade-off |
|---|---|---|
| 傳輸協定 | TCP vs UDP | TCP:省下所有可靠性工程,但吃 1-RTT 建連與 HoL Blocking;UDP:延遲可控且可自訂語義,但那張工作清單全得自己寫 |
| 連線策略 | 短連線 vs 長連線 | 短連線無狀態易擴充,但每次付交握成本;長連線省 RTT,但要處理心跳、重連、NAT 逾時與連線狀態管理 |
| 並行模型 | thread-per-conn vs event loop | 執行緒模型好寫好除錯,連線數上不去;事件迴圈可扛十萬連線,但任何阻塞操作都會拖垮整個迴圈 |
| 壅塞演算法 | CUBIC vs BBR | CUBIC 保守公平、生態成熟;BBR 在高丟包長距鏈路吞吐量顯著更好,但與 CUBIC 共用瓶頸時可能過度搶佔 |
| Backpressure | 緩衝 vs 丟棄 vs 斷線 | 緩衝保資料但風險是 OOM;丟棄保系統但損失內容;斷線最乾脆,代價是使用者體驗 |
| 健康檢查 | TCP connect vs 應用層探針 | TCP 探針便宜但只驗到核心;應用層探針才驗得到「accept 之後真的有人在處理」 |
每一列的共同結構:便宜的選項把成本推遲到故障時,昂貴的選項把成本前置到設計時。
Q:為什麼是三次交握,兩次不行嗎?
答:要同步的是雙向各自的初始序號,需要「告知+確認」共四件事,中間兩件可合併故為三次;兩次則伺服器無法確認客戶端真的收得到它的序號,也擋不住延遲重複的舊 SYN 建出幽靈連線。
Q:TIME_WAIT 太多怎麼辦?
答:先確認是誰主動關閉(TIME_WAIT 落在主動方);用連線復用/長連線減少關閉次數,並開 SO_REUSEADDR;不要粗暴縮短 2MSL。
Q:流量控制和壅塞控制差在哪?
答:一句話——rwnd 保護接收端 buffer(對方明說),cwnd 保護中間網路(自己猜),實際發送量取兩者最小值。
Q:伺服器連得上卻沒回應,怎麼查?
答:交握由核心完成,先看 ss -ltn 的 Recv-Q——accept queue 堆積代表應用沒在 accept(),問題在應用不在網路。
Q:既然 TCP 這麼好,為什麼還有人用 UDP 自己造輪子?
答:因為 TCP 的可靠性是全有全無且綁死有序交付;需要部分可靠、需要 stream 之間互不阻塞、或需要換網不斷線時,只能在 UDP 上重做。
把這一篇收攏成一句:TCP 幫你把不可靠的封包網路,包裝成一條看起來像檔案讀寫的可靠位元組串流—— 代價是建連的 RTT、有序交付帶來的 HoL Blocking,以及一套你必須看得懂的視窗與佇列行為。 而 UDP 把這些全部拿掉,換來一張白紙與完全的自由。
但這條可靠管線還缺了一件事:它從頭到尾只保證「不掉、不亂」,沒保證「沒人看得懂」。 你寫進 socket 的每一個位元組,在路徑上的每一台路由器、每一個中間設備眼裡都是明文。 下一篇補上這缺失的一層——TLS 與 HTTPS:如何在完全不改動 TCP 的前提下,把裸奔的位元組串流變成一條加密、且能驗證對方身分的通道。