跳转到内容

H1/H2 → H3 协商前端:upgrade 模块与 Alt-Svc

本文梳理 quicX 中 src/upgrade/ 模块的设计:它是 H1/H2 与 H3 之间的协商前端——不是 H3 服务器,也不是反向代理,只负责在 TCP 的 80/443 端口上向所有 cleartext / TLS 客户端反复回应同一句话:

Alt-Svc: h3=":443"; ma=86400

浏览器发出的第一个包永远是 TCP/443 上的 TLS ClientHello,它不会主动尝试 UDP/443。如果只启动 QUIC 服务器、不在 TCP 端发出任何信号,客户端就永远不会发现 H3 的存在——这个模块存在的意义就是补上这条服务发现链路。本文尝试回答以下问题:

  1. TCP 端的 ALPN 列表里为什么绝对不能写 h3? —— h3 协商只能通过 Alt-Svc 这条带外路径透露给客户端;
  2. ProtocolDetector 为什么只嗅探 cleartext,不嗅探 TLS? —— 端口已经决定了 TLS 的分流,嗅探只用来在 cleartext 内部分辨 H1 / H2 prior-knowledge;
  3. HTTP/2 路径为什么要手搓一段 HPACK literal 编码? —— 单次 emit、内容固定、长度 < 127 字节,不值得引入整个 hpack 实现;
  4. UpgradeManager::ProcessUpgrade 里 Protocol::HTTP3 分支为什么是死代码? —— HTTP3 不可能跑在 TCP 上被检测到,这条分支是在类型系统层面提醒”H3 是出口、不是入口”。

UDP 端 · quic + http3 模块

TCP 端 · upgrade 模块

① TCP/443 TLS

ALPN: h2,http/1.1

② 200 OK + Alt-Svc: h3=":443"

③ 重连 UDP/443

QUIC ClientInitial

fd:80

plaintext

HttpSmartHandler

(detector + Alt-Svc 注入)

fd:443

TLS

HttpsSmartHandler

(ALPN h2/http1.1 + Alt-Svc 注入)

fd:443

UDP

IQuicServer

IConnection (H3)

浏览器/curl

四个事实:

  • upgrade 与 quic 完全解耦:两边只通过端口约定(默认 h3_port = 443)和用户配置对齐,没有任何代码级 hand-off。upgrade 不调 IQuicServer,quic 也不知道 upgrade 存在。
  • 各跑各的 IEventLoop:IUpgrade::MakeUpgrade() 不再接收外部 loop,upgrade 服务器自建 common::IEventLoop 和驱动它的线程(首次 AddListener() 时启动)。这不是偷懒:两边没有任何共享状态(见上一条),而 EventLoop 有线程亲和性(Init() 记录 thread_id_,之后所有 RegisterFd/AddTimer 走 AssertInLoopThread(),不符直接 abort()),共用一个 loop 只会把 TCP 首跳和 QUIC 的 PTO/定时器精度绑死在同一条时间线上,还让 teardown 的析构顺序纠缠不清。代价仅是一个 epoll fd + 一个 wakeup fd + 一个线程栈。
  • TCP 端永远不会变成 H3 服务器:HttpsSmartHandler 在 ALPN 里只声明 h2,http/1.1,TLS 握完后要么进 H1 要么进 H2 协商响应路径,两条路径的唯一目的都是发一行 Alt-Svc 然后 close(H1 用 Connection: close,H2 用 GOAWAY)。
  • 客户端必须主动二次连接:发完 Alt-Svc,TCP 这边的工作就结束了。浏览器随后会按 RFC 7838 §3 在自己的连接池里记录一项 alt-authority,下次访问同名 origin 时优先尝试 UDP/443 + QUIC + ALPN=h3。整个跳跃过程完全发生在客户端,服务端没有任何状态关联两次连接。

include/quicx/upgrade/if_upgrade.h 整个文件只有这些(注释省略):

class IUpgrade {
public:
virtual bool AddListener(UpgradeSettings& settings) = 0;
virtual void Stop() = 0;
static std::unique_ptr<IUpgrade> MakeUpgrade();
};

没有回调签名、没有 IEventLoop/IFdHandler、没有连接计数、没有 fd 暴露。这反映模块定位:这个模块不需要业务逻辑——它要么在端口上发 Alt-Svc,要么没起来;调用者只关心后者。

唯一的生命周期入口是 Stop()(析构函数也会调,幂等)。它做三件事,且全部发生在模块自己的 loop 线程上:摘掉 client fd(ISmartHandler::CloseAllConnections())、摘掉并关闭 listen fd、停线程并 join。放在 loop 线程上是硬性要求——EventLoop::RemoveFd/RegisterFd/AddTimer 都要过 AssertInLoopThread(),从别的线程调就是 abort();旧实现在析构函数里直接 RemoveFd(),而析构跑在调用者线程上,跨线程 teardown 是这个模块最容易踩的一颗雷。

UpgradeSettings(include/quicx/upgrade/type.h)的字段分四组:

组别字段真实是否被消费
监听listen_addr_ http_port_ https_port_ h3_port_✅ 全部被 UpgradeServer::AddListener 读取
协议开关enable_http1_ enable_http2_ enable_http3_⚠️ 当前实现未读取——decision 由”端口是否非 0 + 是否配了证书”反推
优选列表preferred_protocols_ = {"h3","h2","http/1.1"}⚠️ 未读取——服务端 ALPN 偏好硬编码在 HttpsSmartHandler::ALPNSelectCallback 的 kPreferred 数组里
凭据/超时cert_file_ key_file_ cert_pem_ key_pem_ detection_timeout_ms_ upgrade_timeout_ms_✅ 凭据被读;⚠️ 两个 timeout 未读取,handler 用 src/upgrade/config.h 的 kUpgradeNegotiationTimeoutMs = 30000 硬编码值
日志log_level_ = LogLevel::kInfo⚠️ 未读取——模块日志级别未与该字段联动

对账诚实度:把”未读取”字段保留在公共结构里是历史遗留,理论上应当:(1) 把 preferred_protocols 注入到 ALPNSelectCallback;(2) 把两个 timeout 注入到 BaseSmartHandler。短期维持现状的代价是配置 silent ignore,本文档显式记录这个 gap。


3. 检测:为什么先 HTTP/2 后 HTTP/1.1

Section titled “3. 检测:为什么先 HTTP/2 后 HTTP/1.1”

ProtocolDetector::Detect 在 cleartext 路径上调用,决策链:

preface 24B 全等

或合法 SETTINGS 帧

否

CRLF×2 + 方法 + http/1.1

否

buffered bytes

IsHTTP2?

Protocol::HTTP2

IsHTTP1_1?

Protocol::HTTP1_1

Protocol::UNKNOWN

继续 buffer 等待更多字节

三个值得记的细节:

  • 顺序反直觉地是 H2 先。HTTP/2 的连接 preface PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n 起始字节是 P,跟 HTTP/1.1 的 POST/PUT 同字母,朴素的”按方法名命中”会把 H2 误判成 H1。IsHTTP2 用全长 24 字节字面比对或完整 9 字节帧头自洽性校验两个零冲突信号,把 H2 一票否决出去再轮到 H1,命中率/误判率都最优。
  • HTTP/1.1 必须看到完整双 CRLF(headers 结束)才返回 true:避免在客户端只发了一半请求行就误判,造成 buffer 还没填满就走进 negotiate 分支然后回不来。
  • TLS 路径完全不进 detector:HttpSmartHandler::OnRead 才调 ProtocolDetector::Detect;HttpsSmartHandler 走的是 OpenSSL SSL_read/SSL_accept,加密后字节早就被 TLS 封装层抢走了,对它来说”协议就是 ALPN 选出来的字符串”——ProtocolDetector 在 https 路径毫无用武之地。

Protocol 枚举里有 HTTP3,但 Detect 永远不返回它——因为 HTTP/3 跑在 UDP 上,TCP detector 物理上看不到 H3 字节。这就是 §0 第四问的根因。


这一节是整个模块最容易被外部读者误判的地方。HttpsSmartHandler::SetupALPN 这段注释含金量极高,必须原文引用:

// ALPN protocols advertised by THIS TCP/TLS endpoint.
// Important: do NOT advertise "h3" here. HTTP/3 lives on QUIC over UDP
// and never appears as an ALPN value on a TCP/TLS connection. Browsers
// discover h3 out-of-band via the `Alt-Svc` HTTP response header.
static const unsigned char alpn_protocols[] = {
0x02, 'h', '2',
0x08, 'h','t','t','p','/','1','.','1',
};

四层语义堆叠:

  1. 协议地理学:ALPN 是 TLS 扩展,TLS 跑在 TCP 上;QUIC 自己实现了一套 TLS 1.3,跑在 UDP 上。两套 ALPN 的命名空间相同(同样用 IANA 注册的协议串),但物理传输层互斥。在 TCP/TLS 的 ALPN 里塞 h3,对端会真的认为”这条 TCP 连接接下来会跑 HTTP/3”,然后等待你发 QUIC initial 包——但你发不出来,因为你在 TCP 上。结果是连接级死锁。
  2. 服务端 callback 才是真正生效点:SSL_CTX_set_alpn_protos 在客户端 SSL_CTX 上设置”我作为客户端会发什么 ALPN 列表”,服务端模式下毫无作用。真正在服务端选定 ALPN 的是 SSL_CTX_set_alpn_select_cb 注册的 ALPNSelectCallback。这两个函数容易被搞混,注释 https_smart_handler.cpp:380-389 就是为了消歧。
  3. 服务端选择策略硬编码:
    static constexpr std::array<const char*, 2> kPreferred = {"http/1.1", "h2"};
    优先 http/1.1 而非 h2 是有意的——浏览器/curl 同时报这两个 ALPN 时,走 H1 路径走 GenerateHTTP1UpgradeData 生成的响应(一行 200 OK + Alt-Svc + 简单 body)比 H2 路径手搓 HPACK 简单一个数量级,而最终送到客户端的 Alt-Svc 字段值完全相同。
  4. 降级策略:如果客户端 ALPN 里既没有 h2 也没有 http/1.1(极少见,比如某些 grpc 客户端只报 h2),回调返回 SSL_TLSEXT_ERR_NOACK 让 TLS 握手继续而不带 ALPN(注释 https_smart_handler.cpp:444-448)——这模拟了 nginx 的容错行为,避免协议名小冲突就直接 alert。

OnConnect

第一批字节到达

字节不足继续 buffer

Detect 返回 HTTP1_1/HTTP2

30s 超时 或 Detect 永远 UNKNOWN

pending_response 全部写出

write 失败

socket->Close()

400 写出后 Close

INITIAL

DETECTING

NEGOTIATING

FAILED

UPGRADED

四个 handler 角色:

类职责
ISmartHandler(接口)OnConnect / OnRead / OnTimeout / GetType()
BaseSmartHandler(公共基类)ConnectionContext 池、状态迁移、pending_response 分片写、超时定时器、UpgradeManager 持有
HttpSmartHandlercleartext 路径:OnRead → ProtocolDetector::Detect → manager_->ProcessUpgrade
HttpsSmartHandlerTLS 路径:OnRead → SSL_read → ALPN 已选定 → manager_->ProcessUpgrade,跳过 detector

SmartHandlerFactory::CreateHandler(settings, loop, kind) 根据 HandlerKind::kHttp / kHttps 二选一。没有第三种 kind——这强化了”端口决定路径”的设计:80 → kHttp,443 → kHttps,没有”443 上跑 cleartext H2”的路径(拒收 H2C,符合 RFC 7540 §3.2 最终被 RFC 9113 §3.1 弃用的现实)。

ConnectionContext(connection_context.h)是跨 handler 共享的小结构,关键字段:

ConnectionState state; // INITIAL/DETECTING/NEGOTIATING/UPGRADED/FAILED
Protocol detected_protocol; // detector 的输出
std::vector<uint8_t> read_buf; // 累积 buffer
std::vector<uint8_t> pending_response; // 协商响应字节,待写
size_t response_sent; // 已写偏移,支持分片 write
std::shared_ptr<ITcpSocket> socket;

pending_response + response_sent 这一对是分片写的核心:协商响应一次性生成(H1 几百字节、H2 ~250 字节),但内核 send buffer 满时写不完,必须 EAGAIN 后下次可写时续写——BaseSmartHandler::TrySendResponse 跑的就是这个 partial write 循环。


6. 协商响应:双码路径的 Alt-Svc 注入

Section titled “6. 协商响应:双码路径的 Alt-Svc 注入”

VersionNegotiator::Negotiate 根据 context.detected_protocol 分流到两个生成器。

6.1 HTTP/1.1 路径:朴素字符串拼接

Section titled “6.1 HTTP/1.1 路径:朴素字符串拼接”
std::string body = "h3 available on :" + std::to_string(settings.h3_port_) + "\n";
std::string alt_svc = "h3=\":" + std::to_string(settings.h3_port_) + "\"; ma=86400";
std::string response =
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/plain\r\n"
"Content-Length: " + std::to_string(body.size()) + "\r\n"
"Alt-Svc: " + alt_svc + "\r\n"
"Connection: close\r\n"
"\r\n" + body;

为什么用 200 OK 而不是 RFC 7230 §6.7 那种 101 Switching Protocols + Upgrade: h3?因为 绝大多数浏览器不会响应 Upgrade: h3——h3 不是 RFC 7230 意义上的 in-band 升级(switching 后必须在同一 TCP 连接上跑新协议;但 h3 必须换到 UDP)。RFC 9114 §3.3 明确说 h3 的发现路径是 Alt-Svc 或 DNS HTTPS 记录,不走 Upgrade 头。我们这里用 200 OK + Alt-Svc 是符合 RFC 7838 §3 的标准做法。

Connection: close 是关键:“任务完成、Alt-Svc 已发,请你断开然后用 alt-authority 重新拨”。

完整序列(version_negotiator.cpp:121-265):

1) Server SETTINGS 帧(empty payload)
2) SETTINGS ACK 帧(preemptive — RFC 7540 §6.5.3 容忍乱序 ACK)
3) HEADERS 帧 on stream 1(END_HEADERS)
:status: 200 — indexed header (static index 8 → 0x88)
content-type: text/plain — literal-name without indexing
content-length: <body.len> — literal-name without indexing
alt-svc: h3=":<port>"; ma=86400 — literal-name without indexing
4) DATA 帧 on stream 1(END_STREAM)with body
5) GOAWAY 帧(last_stream_id=1, error=NO_ERROR)

四个手搓而非引整套 HPACK 的合理化:

  1. 零依赖:upgrade 模块连 hpack 库都不需要 link,连 H2 协议状态机都不需要——它只是按字节顺序往外吐五个固定结构帧。
  2. HPACK literal-without-indexing(0x00 prefix, RFC 7541 §6.2.2)是最简形式:name-len(7bit, H=0) name-bytes value-len(7bit, H=0) value-bytes,所有字段长度均 < 127 字节,所以 7-bit 前缀单字节即可表示长度,零 varint 复杂度。
  3. 预先发 SETTINGS ACK:通常 ACK 应在收到对端 SETTINGS 后回,但 RFC 7540 §6.5.3 只要求 “as soon as possible”——我们提前发实际是 永远不会读对端 SETTINGS 的简化(反正发完 Alt-Svc 就 GOAWAY),违反了”先收后 ACK”的语义但被所有实现容忍。这是用 protocol elasticity 换 implementation simplicity。
  4. 路径冷门:服务端 ALPN 选择策略偏好 http/1.1(§4),所以这条 H2 路径只在客户端 ALPN 列表里没有 http/1.1 时被触发——比如 nghttp / h2load / 某些 gRPC 客户端。给这些极少数客户端单独保留路径,但用最少代码维持 spec compliance。

6.3 双码路径的”等价输出”不变量

Section titled “6.3 双码路径的”等价输出”不变量”

任意客户端经历过 upgrade 模块后,应当看到完全等价的 alt-svc 字段值 h3=":<h3_port>"; ma=86400。这是模块的 contract bisection:客户端不应当因为走了 H1 还是 H2 路径而对 H3 端点产生不同认知。代码中两条路径都从同一个 settings.h3_port_ 派生 alt_svc 字符串,这条不变量靠”两条路径里 alt_svc 计算公式同源”维持。


ConnectionContextISmartHandler(per-client-fd)ConnectionHandler(per-listen-fd)UpgradeServerIEventLoop(服务器私有 + 私有线程)AppConnectionContextISmartHandler(per-client-fd)ConnectionHandler(per-listen-fd)UpgradeServerIEventLoop(服务器私有 + 私有线程)App首次调用时自建 Loop + 线程,bind/RegisterFd 投递到该线程执行,调用方阻塞等待结果listeners_ 持强引用(Loop 内部存 weak_ptr)MakeUpgrade()AddListener(settings)bind_one(80, kHttp)bind_one(443, kHttps) (if cert)RegisterFd(listen_fd, ET_READ, CH)OnRead(listen_fd)accept() → client_fdOnConnect(client_fd, ctx)RegisterFd(client_fd, ET_READ, SH)OnRead(client_fd)累积 read_bufProtocolDetector::Detect or SSL_acceptUpgradeManager::ProcessUpgradepending_response 填充OnWrite(client_fd)TrySendResponse (partial write)state = UPGRADEDRemoveFd(client_fd)socket->Close()

四个工程教训:

  1. 双监听设计:AddListener 一次调用同时绑 80 和 443 两个 fd,分别配 HttpSmartHandler 和 HttpsSmartHandler。upgrade_server.cpp:36-130 的注释回溯了一段历史 bug——前任版本”有 cert 就只绑 443,没 cert 就只绑 80”,结果一旦你给配置了证书,浏览器先打 http://host:port/ 直接 connection refused,h3 被静默淹没。修正后两条监听独立、各自带各自的 handler,plaintext 字节永远不会进 SSL 状态机,反之亦然。
  2. listeners_ 必须强引用:EventLoop::fd_to_handler_ 内部存的是 std::weak_ptr<IFdHandler>,如果 bind_one 的 lambda 退出时 connection_handler 这个 shared_ptr 也跟着析构,下一次 epoll 唤醒就会拿到一个失效 weak_ptr,日志上看到”No handler found for fd N”,accept loop 永远不会跑。upgrade_server.cpp:103 的 listeners_.push_back(...) 是这个 bug 的 trip wire。
  3. 客户端 fd 的 handler 是 ISmartHandler 自己:listen_fd 用 ConnectionHandler 适配 accept;accept 出来的 client_fd 直接把 ISmartHandler 注册到 EventLoop——后者的生命周期通过 ConnectionHandler::handler_ 这条路径间接保活。
  4. 拆解时序:UpgradeServer::~UpgradeServer 显式 RemoveFd + Close 每个 listen_fd(upgrade_server.cpp:21-34)。这是为了在 EventLoop 自己析构期间避免 race:如果先析构 EventLoop,它会遍历 fd_to_handler_,对每个 weak_ptr.lock() 后 dispatch,而此时 ConnectionHandler 已经被释放——会拿到一个失效对象。先 RemoveFd 把 weak_ptr 项从 loop 清掉再让 listeners_ 清空,保证遗忘顺序正确。

8. 客户端跳跃:Alt-Svc 之后发生了什么

Section titled “8. 客户端跳跃:Alt-Svc 之后发生了什么”

服务端的工作到 GOAWAY/Connection-close 就结束了。客户端的 H1→H3 跳跃链(参考 RFC 7838 §3 + RFC 9114 §3.3):

  1. 接收 Alt-Svc:HTTP 客户端解析 Alt-Svc: h3=":443"; ma=86400,把 (origin, h3, alt-authority=":443", expiry=now+86400s) 写入 alt-svc cache。
  2. 下次访问该 origin:客户端发起 HTTP 请求时检查 cache:
    • 如果在 ma 内 → race:同时拨 TCP/443 (旧路径) 和 UDP/443 + QUIC + ALPN=h3(新路径),首个完成握手者赢;这就是 Chrome/Firefox 的”happy-eyeballs for H3”行为。
    • 如果 cache 失效或不存在 → 退回纯 TCP 路径,重新触发 §1 流程。
  3. QUIC 握手:这一步走的就是 crypto_keying.md 描述的密钥派生 / ALPN=h3、然后 h3_connection.md 的 SETTINGS / control 流装配。与 upgrade 模块完全无关——客户端和 quic 服务器直接交互。
  4. 失败回退:如果 UDP/443 被中间盒丢包导致 QUIC 握手超时,客户端回退到 TCP/443 + h2/http1.1,并把 alt-authority 标记为”broken”在一段时间内不再尝试(Chrome 是 5 分钟)。服务端没有任何信号能介入这个回退,所以 quic 层的可达性是 H3 部署的硬要求。

关键不变量:服务端和客户端在协商上的契约完全是异步的、单向的、stateless 的。upgrade 模块发完 Alt-Svc 就忘掉这个客户端;客户端可能从此再也不来,或者立刻在 UDP 上拨过来,或者一周后才来。这种松耦合是 Alt-Svc 设计的精髓——它让你可以把这个模块部署成无状态边缘服务、放在 CDN 前面、用任何负载均衡策略,都不影响 H3 协商正确性。


这一节是对照”理想 upgrade 协商前端”列出本仓库的未完成项,避免读者把 src/upgrade/ 当成成品:

项现状缺口影响
preferred_protocols_ 字段公共结构体里有,未消费无法运行时调整 ALPN 偏好需要支持”优先 H2 而非 H1”的部署需手改源码
enable_http1_ / enable_http2_ / enable_http3_未消费无法关闭某条路径想做”只 H2 + H3,禁 H1”目前做不到
detection_timeout_ms_ / upgrade_timeout_ms_未消费用 hardcoded 30s部署中无法调短超时以提高僵死连接清理
0-RTT / TLS session ticketTLS context 默认配置,未启用 ticket 持久化同源重连无法 0-RTT客户端每次回到 TCP/443 仍需完整握手
Upgrade: h2c 头解析完全未实现(H2C 已被 RFC 9113 弃用)cleartext H2 必须 prior-knowledge不影响主流客户端
DNS HTTPS RR 记录(RFC 9460)不在 upgrade 模块职责范围无该路径完全靠运维侧 DNS 配置
Alt-Svc 缓存清除信号未实现 RFC 7838 §3.3 的 Alt-Svc: clearh3 端口下线无法主动通知客户端会按 ma 自然过期

值得收紧的两项:把 preferred_protocols_ 接到 ALPNSelectCallback 是低风险 5 行修改;把两个 timeout 字段读进 BaseSmartHandler 是 3 行修改。两者是工作量最小、ROI 最高的紧迫项。


跨整个 src/upgrade/,下列断言在任何代码路径下都不应当被违反——任何后续重构都必须维持:

  1. TCP/TLS 端的 ALPN 列表中绝不包含 h3(https_smart_handler.cpp:362-389)。
  2. ProtocolDetector::Detect 永远不会返回 Protocol::HTTP3。
  3. cleartext 字节永远不会进入 SSL 状态机:HttpSmartHandler 和 HttpsSmartHandler 由 SmartHandlerFactory 在 listen 阶段就分流,accept 出来的 client_fd 注册到的是哪个 handler,由它的 listen 端点决定,不可中途切换。
  4. 协商响应在两条路径(H1/H2)下的 alt-svc 字段值字节级相同(同源 settings.h3_port_)。
  5. pending_response 一旦填充,必须由 TrySendResponse 在多次 EAGAIN 之间逐字节写完——绝不重新生成(重新生成意味着 :status 之类有状态字段错位)。
  6. UpgradeServer::listeners_ 中每个 ConnectionHandler 的 shared_ptr 必须存活到对应 fd RemoveFd 之后(EventLoop 内部 weak_ptr 假设)。
  7. 析构时 RemoveFd 必须在 Close 之前,否则 EventLoop 可能在 fd 已关闭后再次 dispatch。
  8. ConnectionState 状态迁移单向:INITIAL → DETECTING → NEGOTIATING → (UPGRADED | FAILED),不存在回退路径。
  9. 任何最终态(UPGRADED / FAILED)都必须 socket->Close()——upgrade 模块不保留任何长连接,连接复用是客户端 alt-authority cache 的事。
  10. HTTP/2 路径的 5 帧序列必须在单次 TLS write 内一次性 emit(注释 version_negotiator.cpp:128),保证客户端解析窗口内五帧顺序到达。
  11. UpgradeManager 持久化的状态只有 last_result_(用于日志);连接级状态住在 ConnectionContext 里、归 BaseSmartHandler 管,manager 是无连接状态的纯函数式编排者。
  12. 服务端永远不读 H2 客户端的 SETTINGS 帧(提前 ACK 简化路径),但必须发自己的 SETTINGS(即使为空),否则客户端会因为 RFC 7540 §3.5 违反协议而 GOAWAY。

11.1 与本仓库其他设计文档的分工

Section titled “11.1 与本仓库其他设计文档的分工”
文档职责边界与本文的接口
connection_anatomy.mdUDP/QUIC 连接结构upgrade 在 TCP 端发完 Alt-Svc 后,客户端在此进入
handshake_state_machine.mdQUIC + TLS 握手流程客户端到达 UDP 后跑这套握手;ALPN=h3 即在此协商
crypto_keying.md密钥派生与 Key Updateh3 ALPN 选定后用 RFC 9001 §5 的 secrets 起 1-RTT
h3_connection.mdH3 6 类流的多流协作upgrade 把客户端引到 quic,quic 服务器装配本文档描述的 6 类流
process_model.mdEventLoop 线程模型upgrade 自建私有 EventLoop 与驱动线程,与 quic 侧互不共享
ownership_and_memory.md引用计数与生命周期模式UpgradeServer::listeners_ 强引用 + EventLoop weak_ptr 是该模型的实例
  • RFC 9114 HTTP/3:§3.1 H3 endpoint discovery、§3.3 Connection Establishment(明确说 h3 不走 TCP Upgrade 头)
  • RFC 7838 HTTP Alternative Services:§3 Alt-Svc 字段语法与语义、§3.1 alt-authority 含义、§3.3 Alt-Svc: clear 清除信号
  • RFC 9460 Service Binding via DNS:HTTPS RR 记录(DNS 路径的 Alt-Svc 替代品;不在本模块)
  • RFC 7540 HTTP/2:§3.5 connection preface、§6.5 SETTINGS frame、§6.5.3 SETTINGS ACK、§6.8 GOAWAY frame
  • RFC 9113 HTTP/2 (revised):§3.1 弃用 H2C Upgrade: h2c
  • RFC 7541 HPACK:§6.1 indexed header(:status:200 的 0x88 编码)、§6.2.2 literal without indexing(alt-svc 头的零依赖编码方式)
  • RFC 7230 HTTP/1.1 Message Syntax:§6.7 Upgrade 头(解释为何 h3 不通过此机制)
  • RFC 8470 Using Early Data in HTTP:0-RTT 在 HTTP 上的使用约束(本模块未启用,列入差距)

设计文档完结:docs/zh/design/ 目前共 16 篇正文,覆盖主链路、关键决策、基础设施、协议层细节与可观测性五组,构成 quicX 内部从握手到协议入口的完整说明集。文档地图见 ../README.md §6。