阅读提要

协议升级主要在边缘完成,后端多数时候无感。但有三件事会直接影响你的实现:请求头变大、连接复用、以及「客户端 IP」取不到。

开始阅读

字号

一句话概括演进

HTTP/1.1 的问题是队头阻塞:一个连接同时只能处理一个请求,前一个慢了,后面的全等。

HTTP/2 用多路复用解决:一个 TCP 连接上并发多个流。但 TCP 层仍然有队头阻塞——一个包丢了,整条连接的所有流都要等重传。

HTTP/3 换成 QUIC(基于 UDP),把「流」下沉到传输层,单个流丢包不影响其他流。

对后端开发者,这三层的差别大部分被反向代理吃掉了。但有几处会漏到你这里。

协议演进

影响一:请求头压缩改变了「头部开销」的直觉

HTTP/1.1 里,一个请求带几百字节的头很常见。HTTP/2 引入 HPACK 压缩,重复的头字段只在首帧传一次。

结果是:过去为了省头部而做的设计,现在没必要了。比如把多个资源合并成一个大文件(雪碧图、CSS 合并),在 HTTP/2 下收益大幅下降,反而破坏了缓存粒度——改一个图标要重新下载整张图。

对后端的具体影响:不要为了减少请求数而设计「批量接口」,除非业务上确实需要原子性。合并请求会让缓存失效、错误处理变复杂,收益却很有限。

影响二:连接复用让「每请求一连接」的假设失效

HTTP/1.1 时代,一个 TCP 连接对应一个请求(或一个短时间的 keep-alive)。有些后端逻辑依赖这个假设——比如「连接建立时初始化上下文」。

HTTP/2 下,一个连接承载几百个请求,连接可能保持几分钟。任何绑在连接上的状态(会话、事务、临时变量)都会串味。

// 危险:把状态挂在连接上
server.on('connection', (socket) => {
  socket.userContext = {}   // 这个连接上的所有请求共享它
})

正确做法是每个请求独立上下文,用 AsyncLocalStorage 之类的机制传递,不要挂在 socket 上。

影响三:客户端 IP 会变成代理的 IP

这是最实际的一个坑。HTTP/2 和 HTTP/3 通常由反向代理(Nginx、Caddy、CDN)终结,后端拿到的是代理的地址。

必须从 X-Forwarded-For 或 Forwarded 头里取真实 IP。但直接取第一个值是不安全的——客户端可以自己伪造这个头。

正确逻辑是:只信任已知代理传来的头,并从右往左取第一个非可信地址。

function clientIp(req, trustedProxies) {
  const chain = String(req.headers['x-forwarded-for'] || '')
    .split(',').map(s => s.trim()).filter(Boolean)
  chain.push(req.socket.remoteAddress)   // 最右是直连方
  for (let i = chain.length - 1; i >= 0; i--) {
    if (!trustedProxies.has(chain[i])) return chain[i]
  }
  return chain[0]
}

从右往左,是因为左侧的值可以被客户端随意伪造,右侧的值是每一跳代理追加的。只有可信代理追加的那一段才可信。

如果这一步做错,限流、封禁、访问统计全部失真——而且是被攻击者主动操纵的失真。

HTTP/3 的特殊性

QUIC 基于 UDP,这意味着:

  • 防火墙要放行 UDP 443,很多企业网络默认封 UDP;
  • 部分老旧的负载均衡不支持 QUIC,会回退到 HTTP/2;
  • 连接迁移(换 WiFi 不断连)是 QUIC 的特性,你的会话如果绑 IP,用户切网络就会掉线——这反而是提醒你「会话不该绑 IP」。

实践中,HTTP/3 通常由 CDN 或边缘层开启,源站仍然是 HTTP/1.1 或 HTTP/2。所以后端能做的主要是:别依赖连接和 IP 做状态。

什么时候值得关心

  • 你在写网关、代理、或者自己实现 HTTP 服务:必须关心;
  • 你用 Nginx/Caddy 反代一个普通应用:基本不用改代码,但 IP 和连接状态这两点要检查;
  • 你直接对外提供 API 且客户端是浏览器:确认 CDN 开了 HTTP/2,这是免费的收益。

一个检查清单

  1. 日志里记录的 IP 是真实客户端还是代理?
  2. 有没有把状态挂在连接对象上?
  3. X-Forwarded-Proto 有没有正确处理(避免 HTTPS 站点生成 HTTP 链接)?
  4. 大量小文件是否还在做无意义的合并?
  5. 会话是否绑定了 IP(换网络就掉线)?

这五条比「支持 HTTP/3」这种口号实在得多。

一个容易忽略的副作用

HTTP/2 的多路复用让「并发请求」变得廉价,客户端的请求模式随之改变:过去为了减少连接数而合并的请求,现在会拆开并发发出。

结果是后端的瞬时并发上升。原来串行发 5 个请求,现在同时发 5 个,数据库连接池的压力会比 HTTP/1.1 时代更大。

这不是协议的问题,是容量规划需要跟着调整。压测时如果只用 HTTP/1.1 的客户端,测出来的并发能力会偏乐观。

一条实践建议

如果你的服务在反向代理后面,先花十分钟确认三件事:真实 IP 是否取对、X-Forwarded-Proto 是否处理、会话是否绑了 IP。这三件事和协议版本无关,但会在升级时集中暴露。

你认为这篇文章怎么样?

还没有表情反馈

分享文章