TCP 凭什么敢说「可靠」?

0 阅读7分钟

你在浏览器地址栏输入一个网址,按下回车。页面几秒后出现了。

在这"几秒"里,你的数据穿越了光纤、路由器、海底光缆,途中可能被丢弃、被打乱顺序、被延迟——但你收到的页面完好无损。

这背后站着一个 50 年前设计的协议:TCP

但这里有一个常见误解:TCP 的"可靠",不是说网络不丢包。恰恰相反,TCP 假设网络一定会丢包,然后通过一套精密的修复机制——序号、确认、重传、窗口——把一条不可靠的链路修补成一个可靠的字节流。

这篇文章不讲 RFC 规范的每一行细节,而是帮你建立工程直觉:TCP 到底做了什么、为什么这么做、以及它的设计代价如何影响你的页面性能。

一、三次握手:一个 RTT 的代价

每个 TCP 连接的第一件事,是三次握手。

三次握手流程

三次握手流程

客户端                           服务器
  |--- SYN (seq=x) ------------>|   ① 我想建立连接
  |<-- SYN-ACK (seq=y, ack=x+1)|   ② 收到,我也准备好了
  |--- ACK (ack=y+1) ---------->|   ③ 好的,开始传数据

三个包,一个完整往返(RTT)。

纽约到伦敦的光纤单程 28ms,三次握手最少 56ms。注意,这还没传任何业务数据。

这就像去银行办业务——你到窗口,先填表(SYN)、柜员核实身份(SYN-ACK)、你签字确认(ACK),然后才开始办正事。如果每次存钱都要重新走一遍,效率可想而知。

这就是为什么连接复用如此重要。  HTTP/1.1 的 keep-alive、HTTP/2 的多路复用,本质都是在说同一句话:别反复握手了,一条连接用到底。

二、慢启动:新司机的第一天

握手完成,该传数据了。但问题来了:发送方不知道这条路有多宽

如果一上来就全速灌数据,路上一堵,全员完蛋。1986 年互联网上就发生过这种事——拥塞崩溃,网络容量直接暴跌 1000 倍。这像银行挤兑:所有人同时取钱,银行反而瘫痪。

TCP 的解法是慢启动:先发少量数据探路,收到确认后翻倍加量。

往返次数拥塞窗口(cwnd)可发送数据
第 0 轮10 个段~14 KB
第 1 轮20 个段~28 KB
第 2 轮40 个段~56 KB
第 3 轮80 个段(触顶)~64 KB

指数增长,听起来很快?但算一笔账:纽约到伦敦,RTT = 56ms,从初始 10 个段增长到 64KB 窗口,需要 3 个往返 = 168ms

这就像一个新快递司机第一天上班——公司不会一次给他 100 个包裹。先送 10 个,全部签收了,下次给 20 个,再签收给 40 个。不是公司不信任他,是不确定路况能不能撑住。

对短连接的杀伤力尤其大。  一个典型 HTTP 请求往往在窗口还没长满之前就结束了。你有 100Mbps 的带宽,但新连接的前几百毫秒根本用不上——延迟和拥塞窗口才是真正的瓶颈,不是带宽

三、流量控制 vs 拥塞控制:水龙头和水闸

TCP 有两道「闸门」,它们解决的问题完全不同:

维度流量控制(rwnd)拥塞控制(cwnd)
保护谁接收方整个网络
谁决定接收方通告发送方自行估算
类比水龙头:接收方说"我桶就这么大,慢点灌"水闸:管理河道总流量,防洪泛
上限16 位字段,默认最大 64KB(开启窗口缩放后可达 1GB)动态调整,从 10 个段起步

实际发送量 = min(rwnd, cwnd) ,取两个窗口的较小值。

这是一个精巧的双重保护:一个管"对面能吃多少",一个管"路上能通多少"。任何一个短板都会成为实际瓶颈。

窗口缩放:从小水桶到水塔

原始 TCP 规范给接收窗口只留了 16 位,硬上限 64KB。在高带宽 × 高延迟的网络环境里,这远远不够。

RFC 1323 引入窗口缩放选项,将上限扩展到 1GB。现代操作系统默认启用,但要注意:某些中间设备(防火墙、NAT)可能会剥离这个选项,导致连接退回 64KB 上限。

四、带宽延迟积:一道被忽视的算术题

有一个公式你需要记住:

最大吞吐量 = 窗口大小 ÷ RTT

举个例子:窗口 16KB,RTT 100ms:

吞吐量 = 16 KB / 100 ms = 160 KB/s ≈ 1.31 Mbps

无论你的带宽有多大,这条连接的吞吐量不会超过 1.31 Mbps。

反过来算:如果你的带宽是 10Mbps,RTT 100ms,需要多大窗口才能跑满?

所需窗口 = 10 Mbps × 100 ms ≈ 122 KB

回想一下,不开窗口缩放的默认上限是 64KB——连一半都用不上

这就是为什么"延迟是 TCP 的瓶颈,不是带宽"。  你花钱升级到千兆宽带,但如果 RTT 没降下来、窗口没调上去,新增的带宽只是在空转。

五、队头阻塞:可靠性的代价

TCP 保证有序交付。这是它最大的卖点,也是最大的代价。

队头阻塞示意

队头阻塞示意

想象你收到了 5 个包:1 号已到、2 号丢了、3/4/5 号也到了。

TCP 的做法:把 3/4/5 号扣在缓冲区里,等 2 号重传到达后再一起交给应用层。应用层什么都看不到,只会感觉——卡了一下。

这就是队头阻塞(Head-of-Line Blocking)

对网页加载来说,这意味着:一个 CSS 文件的某个包丢了,后面排队的 JS 文件也得等着。延迟像病毒一样扩散。

场景TCP 的行为影响
网页加载一个资源的丢包阻塞所有后续资源白屏时间增加
视频通话等待重传导致画面冻结体验断续
在线游戏T-1 时刻的包还没到,T 时刻的包被阻塞操作延迟

有些场景其实不需要这种"完美可靠"。  视频丢一帧、游戏丢一个状态包,用户根本感知不到。这也是 WebRTC 选择 UDP、HTTP/3 选择 QUIC 的根本原因——用「流级别」的独立性换回被 TCP 锁死的并行度

六、给前端工程师的性能清单

理解了 TCP 的机制,很多"前端优化经验"就不再是黑箱了:

优化手段TCP 层面的原因
减少 HTTP 请求数每个新连接 = 1 RTT 握手 + 慢启动
使用 CDNRTT 从 100ms 降到 20ms → 吞吐量提升 5 倍
开启 HTTP/2多路复用 = 一条连接传所有资源,避免重复握手和慢启动
首屏资源 < 14KB初始 cwnd = 10 段 ≈ 14KB,第一个 RTT 能传完
关注 HTTP/3QUIC 基于 UDP,彻底消除传输层队头阻塞
内联关键 CSS/JS减少额外连接,避免慢启动延迟

那个"首屏 14KB"的经验法则,现在你知道它的由来了:TCP 初始拥塞窗口就是 10 个段(约 14KB)。第一个 RTT 能传完的数据量,就是 14KB。  超过这个数,多等一个 RTT。

七、一张表回顾 TCP 的设计哲学

设计决策机制代价类比
建立连接需要确认双方状态三次握手1 RTT 延迟银行开户先验证身份
不知道网络容量时小心试探慢启动前几百 ms 带宽利用率低新司机第一天送货先少量
防止发送方压垮接收方流量控制(rwnd)窗口太小限制吞吐水龙头调节
防止所有人一起压垮网络拥塞控制(cwnd)丢包后窗口减半央行收紧货币
保证数据有序到达序号 + 缓冲队头阻塞快递必须按编号签收

每一个"可靠"的特性,都对应一项"性能"的代价。  TCP 的设计哲学从来不是"又快又好",而是——先保证对,再想办法快

如果你只想带走一句话,我建议记这个:

TCP 的"可靠"不是网络不丢包,而是允许不可靠存在,再用机制修复出可靠的视图。理解这些机制的代价,才能理解为什么你的页面会慢。

参考原文:

• Ilya Grigorik — Building Blocks of TCP (High Performance Browser Networking)

qrcode_for_gh_6a9e7f3719d6_344.jpg