连接与服务发现

Alt-Svc、DNS HTTPS 记录、GOAWAY 的双向语义与回落策略。

作者 David更新于 第 14 篇(共 16 篇)

发现问题

客户端不能凭空知道某个站点支持 h3——直接发 UDP 包给一个不支持的服务器只会超时。所以 h3 必须先被发现。

三条路径:

1. Alt-Svc 字段(RFC 7838)

服务端在 h2/1.1 响应里声明「我还有个 h3 端点」:

alt-svc: h3=":443"; ma=86400
alt-svc: h3=":443"; ma=86400, h2=":443"; ma=3600
参数含义
h3=":443"协议 ALPN + 端口(主机省略表示同主机)
mamax-age,该通告的有效秒数(默认 86400)
persist=1网络变更后仍保留该记录

客户端行为:

  • 本次请求仍走当前协议,把通告存入 Alt-Svc 缓存。
  • 后续请求尝试用 h3 连接;成功则持续使用,失败则回落。
  • 这意味着首次访问必然不走 h3——至少要一个来回才能升级。

2. DNS HTTPS/SVCB 记录(RFC 9460)

解决 Alt-Svc 的「首访必慢一拍」问题——把发现提前到 DNS 查询阶段:

example.com.  3600  IN  HTTPS  1 . alpn="h3,h2" port=443 ipv4hint=203.0.113.1

客户端在解析域名时就知道支持 h3,第一个请求即可直发 QUIC。同时 ipv4hint/ipv6hint 还能省一次 A/AAAA 查询,ech 参数用于加密 SNI。

这是目前推荐的方式,主流浏览器与 CDN 均已支持。

3. 先验知识

客户端被显式配置为使用 h3(如 API 客户端固定配置),跳过发现。

回落策略

h3 跑在 UDP 上,而 UDP 在部分网络中被封禁或严重限速。客户端必须具备回落能力:

1. 并发或竞速发起 QUIC 连接与 TCP 连接
2. QUIC 在超时内握手成功 → 用 h3,标记该网络可用 UDP
3. QUIC 超时/被拒 → 用已经建好的 TCP 连接走 h2/1.1
4. 记录失败,一段时间内不再对该网络/该源尝试 h3

Chrome 等实现使用「竞速(racing)」:同时发起两种连接,先成功者胜出,另一条丢弃。代价是多一点连接开销,换取无感回落。

服务端侧:必须同时保留 h2/1.1 端点。只提供 h3 会让相当比例的用户完全无法访问。

GOAWAY

h3 也有 GOAWAY,但语义比 h2 更完整——它是双向的:

发送方载荷含义
服务端最大的已处理请求流 ID。大于该值的请求一定未被处理
客户端最大的推送 ID(表示不再接受更大的推送)

h2 的 GOAWAY 只能由任一端发送并标注 Last-Stream-ID;h3 明确区分了两个方向的用途。

规则:

  • 只能在控制流上发送。
  • 可以发多次,但后发的 ID MUST NOT 大于先发的(只能收紧,不能放宽)——违反 → H3_ID_ERROR。
  • 与 h2 相同的两阶段优雅关闭做法同样适用:先发大值表达意图,处理完在途请求后再发真实边界。

连接关闭

QUIC 层提供了 h2 没有的能力:

场景机制
优雅关闭h3 GOAWAY → 处理完 → QUIC CONNECTION_CLOSE
立即关闭QUIC CONNECTION_CLOSE(带错误码与原因短语)
空闲超时QUIC 传输参数 max_idle_timeout,双方取较小值
无状态重置服务端重启丢失状态后,用 Stateless Reset Token 告知对端「这条连接我不认识了」,避免客户端长时间重试

无状态重置是 TCP 的 RST 在加密传输上的等价物——因为服务端重启后无法解密该连接的包,只能靠预先分发的 token 来证明「确实是我发的重置」。

与 h2 的连接管理对照

能力h2h3
连接前言PRI * HTTP/2.0 魔数无(ALPN 已确定)
参数协商流 0 的 SETTINGS + ACK控制流首帧 SETTINGS,无 ACK、不可重发
保活h2 PING 帧QUIC PING 帧
单流终止RST_STREAMQUIC RESET_STREAM / STOP_SENDING
优雅关闭GOAWAYGOAWAY(双向语义)+ QUIC CONNECTION_CLOSE
换网络断连重建连接迁移,无感

STOP_SENDING 是 QUIC 独有的:它表示「别再往这条流发了」,而 RESET_STREAM 是「我不发了」。h2 的 RST_STREAM 把两个方向混为一谈,QUIC 拆开了。

0-RTT 与幂等性

复用前文的结论,落到 HTTP 层:

  • 客户端 SHOULD 只在 0-RTT 中发送安全且幂等的请求。
  • 服务端若不愿在 0-RTT 中处理某请求,返回 425 Too Early;客户端 MUST 在握手完成后重发。
  • 服务端也可整体拒绝 0-RTT(不下发会话票据或忽略早期数据)。

这是 425 这个状态码存在的真正场景——它在 1.1 时代几乎无用武之地。

第 2 部分结束。下一篇:三版全面对照。

本页目录