你在浏览器地址栏输入一个网址,按下回车。页面几秒后出现了。
在这"几秒"里,你的数据穿越了光纤、路由器、海底光缆,途中可能被丢弃、被打乱顺序、被延迟——但你收到的页面完好无损。
这背后站着一个 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 握手 + 慢启动 |
| 使用 CDN | RTT 从 100ms 降到 20ms → 吞吐量提升 5 倍 |
| 开启 HTTP/2 | 多路复用 = 一条连接传所有资源,避免重复握手和慢启动 |
| 首屏资源 < 14KB | 初始 cwnd = 10 段 ≈ 14KB,第一个 RTT 能传完 |
| 关注 HTTP/3 | QUIC 基于 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)