跳转至

ADR-0002 客户端接入协议与回落链路

状态

已接受(2026-08-13)。对应 docs/PLAN.md §5.2、§15、§27.1、附录 A。

背景

CLAUDE.md 已锁定"TCP/TLS 自定义二进制协议为主链路,浏览器用 WebSocket 承载同一协议"。 但 v1 的 §5.2 只有五行,评审 001 的 C-1 指出:全文 0 次出现 QUIC、HTTP/3、gRPC、MQTT、 443、NAT、防火墙、代理、握手,等于没有做过选型论证。C-3 进一步指出 v1 自身矛盾—— 一边要求"用户固定到 ConnectionShard",一边又说"L4 可用源地址一致性分发",而源地址哈希 几乎必然落错分片。

同时缺失的还有工程侧的硬约束:端口、ALPN、回落顺序、TLS 终止位置、重定向次数上限。 没有这些,四端实现会各自发明接入策略,企业网络与移动网络下的可达性无法保证。

决策

1. 承载与端口

端口固定 443(唯一对外端口,不使用自定义端口)
TLS 1.3,ALPN 协商:
    qim/1      自定义二进制帧直接跑在 TLS 之上(主链路)
    http/1.1   走 WebSocket Upgrade,WS 帧内承载同一套 QM 帧(浏览器与受限网络)

FrameHeader(附录 A.1)在两条路径上逐字节相同;WebSocket 只是一层信封。

2. 强制回落顺序

1) ALPN 协商 qim/1 直连 443              超时 5 s
2) WSS 443(http/1.1 + Upgrade)          超时 5 s
3) 经系统 HTTP 代理 CONNECT 的 WSS 443    超时 5 s

客户端按 (网络类型, 运营商 MCC/MNC 或 WiFi BSSID 哈希) 缓存上次成功方式,TTL 7 天。
命中缓存时直接使用该方式,失败后从第 1 级重新探测。

3. 抽象层

Transport { open, sendFrame, onFrame, close }

TCP 与 WebSocket 两条路径必须复用同一个 codec 库,禁止各自演化帧方言。
CI 断言:两条路径对同一组帧的编码结果逐字节相同。

4. TLS 终止与负载均衡

  • TLS 终止在 ConnectionNode,保留 ALPN 快路径与真实源 IP。
  • L4 只做四层直通,用 PROXY protocol v2 透传源地址,分发键为最小连接数, 不使用源地址哈希。
  • 分片亲和完全由应用层达成:落点不是固定 ConnectionShard 时返回 REDIRECT{route_token, connection_shard, endpoint_hint, exp}。
  • redirect_max_per_connection = 1,route_token_ttl = 60 s 且单次使用。
  • 分片到节点的映射按 region 分组,保证就近接入。
  • 默认关闭 0-RTT;若为握手延迟开启,白名单仅限 PING,AUTH 与任何写操作禁止走 0-RTT。
  • 默认不启用 mTLS 与证书 pinning;私有化部署可按租户开启(属 §27.4 的租户策略)。

5. QUIC / WebTransport

明确列为二期。二期启动的前提是:移动端弱网切换场景下的实测重连收益 > 20%, 且中间设备穿透率(企业网络)实测 ≥ 95%。

理由

  • 443 + ALPN 是可达性的最优解:企业防火墙与运营商中间设备对 443/TLS 的放行率最高, ALPN 让"自定义协议"和"WebSocket"共用同一端口和同一证书,不增加任何暴露面。
  • 两条路径同一套帧使协议演进(§27.1)只需要维护一份 codec 与一份测试, 避免 v1 最容易出现的"TCP 端和 Web 端各写一套"。
  • 应用层路由而非 L4 哈希是唯一能保证分片亲和的做法:源地址哈希在 NAT 后的移动网络下 会把同一用户的重连打到任意节点,与"用户固定到 ConnectionShard"(§5.1)直接冲突。
  • TLS 终止在 ConnectionNode 是 ALPN 能生效的前提;若在 L4 之前终止,ALPN 信息丢失, 就必须退回按端口区分协议,回到自定义端口的老路。
  • 回落顺序有超时上界保证最坏情况下 15 秒内确定可用通道,与 reconnect_backoff(1 s 起,×1.8,上限 120 s)组合后不会造成登录风暴放大。

后果(正面 / 负面)

正面

  • 单端口、单证书、单 codec,运维面与测试面都最小。
  • 企业网络与受限运营商下有明确的三级回落,可达性问题有确定的排查路径。
  • 应用层路由使分片迁移(§5.5)与节点接管(§26.5.5)对 L4 完全透明。
  • 二进制帧头 16 字节,相比 gRPC/HTTP2 的帧+HPACK 开销,在千万连接下节省可观带宽与 CPU。

负面

  • 需要维护两条接入路径(TCP 直连与 WSS),四端实现成本高于只做 WSS 一条路径。
  • TLS 终止在 ConnectionNode 意味着证书轮换、TLS CPU 与会话票据都落在业务节点上, 必须为 TLS 握手单独预留 CPU 预算并做过载保护(unauth_connection_timeout = 10 s)。
  • 自定义协议缺少现成的中间件生态(无通用抓包解析、无标准 LB 七层能力), 可观测性需要自建:帧级埋点与 trace_id 贯穿必须在第一版就做完。
  • 关闭 0-RTT 意味着重连要多一个 RTT;在登录风暴场景下靠 sync_delay_hint_ms 错峰补偿。

替代方案及其否决理由

方案 否决理由
gRPC(双向流) 浏览器必须经 grpc-web 代理,多一跳且帧语义被改写;HTTP/2 流控与帧头开销不可控,无法实现附录 A.5 的四条流优先级;单连接的 HPACK 动态表在千万连接下内存不可接受
MQTT QoS 1/2 的重传与去重语义和本文"至少一次 + mailbox_seq 幂等 + 连续物化水位"(§9.2)重复且冲突;主题树模型无法表达个人邮箱的稀疏区间拉取;缺少批量拉取语义,1 万条积压会退化为 1 万次单条投递
纯 WebSocket(放弃 TCP 直连) 移动端每条消息多 2~14 字节掩码与帧头开销,且必须做客户端掩码(额外 CPU);WS 握手是 HTTP 升级,比 ALPN 多一次报文往返
HTTP/2 + Server-Sent Events SSE 单向,上行仍需另建通道;与 §3 "不用 HTTP 作为实时主链路"的禁令冲突
HTTP 长轮询 违反 §3 禁令;千万连接下的连接建立速率与队头阻塞不可接受
直接上 QUIC/WebTransport 中间设备对 UDP/443 的放行率在企业网络下不确定;四端库成熟度与可观测性工具链不足;因此列为二期而非否决
自定义端口(如 8443/5222) 企业防火墙与部分运营商会阻断;与 443 相比无任何收益

复评条件

出现以下任一情况时必须重开本 ADR:

  1. 实测 qim/1 ALPN 直连成功率低于 90%,说明中间设备对非 HTTP ALPN 的兼容性不达预期。
  2. QUIC/WebTransport 在目标市场的 UDP/443 放行率实测 ≥ 95%,且弱网重连收益 > 20%。
  3. TLS 握手 CPU 占 ConnectionNode 总 CPU 超过 30%,需要重新评估 TLS 终止位置。
  4. 浏览器端出现新的标准双向传输能力,可以取代 WSS 且无需额外代理。
  5. 附录 A 的帧头布局发生大版本变更(FrameHeader.version 递增),需同步复核回落链路。