每当 HTTP 协议进行重大变革的时候,客户端和服务端总是需要一定时间进行适配和兼容。而从 gQUIC 数十个迭代到 HTTP/3 的十数个草案版本,客户端和服务端之间如何决定用哪个方式连接,本文就来谈谈这个。

HTTP/2 的协商方式

HTTP/1.1 在制定时提出了 Upgrade 机制(你可以在 RFC7230 中找到相关的定义)。一般的,由客户端在请求头中发出申请使用别的协议:

  1. GET http://skk.moe HTTP/1.1
  2. Connection: Upgrade
  3. Upgrade: h2c

其中,Connection: Upgrade 表示客户端协商更换协议,Upgrade 指示了希望切换到的协议(h2 是 HTTP/2 的协议名;而 h2c 是不经过 TLS 加密 HTTP/2.0 的协议名,即 HTTP/2 ClearText)。

如果服务端不支持 HTTP/2,那么会直接忽略 Upgrade,使用 HTTP/1.1 返回:

  1. HTTP/1.1 200 OK
  2. Content-Type: text/html

不过如果服务端支持 HTTP/2,服务端会发回一个 101 状态码和 Upgrade 相关头部,然后响应正文就会以 HTTP/2 二进制编码分帧的方式发送:

  1. HTTP/1.1 101 Switching Protocols
  2. Connection: Upgrade
  3. Upgrade: h2c

这套路是不是很熟悉?在浏览器中发起 WebScoket 连接利用的也是 HTTP Upgrade,客户端通过指定 Upgrade: websocketSec-WebSocket-* 一系列响应头指示服务端切换到 WebSocket,协商成功后返回 101 状态码,WebSocket 连接就建立了,之后的 message 就可以直接在 WebSocket 连接上收发了。

除了客户端与服务端协商,HTTP Upgrade 机制也允许服务端和客户端协商使用的协议。服务端可以通过返回 426 状态码要求客户端必须切换协议,并发送 Accept 的协议列表供客户端选择。

需要注意的是,浏览器要求 HTTP/2 必须使用 TLS、建立连接时需要一次额外的 TLS 握手,所以可以在 TLS 握手中、通过 ALPN(应用层扩展协议)进行 HTTP 协议的协商:

  • 客户端在 TLS 握手的 Client Hello 阶段通过 ALPN Protocol 发送客户端支持的协议列表
  • 服务端从协议列表中进行挑选,在 Server Hello 中通过 ALPN Protocol 返回给客户端,协商完成

虽然也可以在 TLS 握手完成、建立 HTTP/1.1 后再通过 HTTP Upgrade 的方式进行协商,但是这需要额外的一个 RTT,因此实际在生产环境和实践中都没有被采用。

不难发现,两种协商机制都颇有一种服务端迁就客户端的意味:客户端告诉服务端支持的协议、由服务端挑选;虽然在规范中服务端也可以要求客户端使用不同的协议,但是在实践中却鲜有运用的。

HTTP Alternative Services

HTTP Alternative Services 的规范 RFC7838 于 2016 年 4 月发布(比 HTTP/2 晚了一年),在设计上为了解决如下问题:

  • 服务端通过这一方式指明「替代服务地址」,当服务器负载过大时服务端可以指示客户端将请求分流到其它服务器上。
  • 解决兼容性问题。例如有服务端 A 和 B,A 为了兼容老旧客户端没有提供 SNI、而 B 提供了 SNI 支持(因此兼容性较差)。因此可以将 A 用于接收第一个请求,而 A 可以指示支持 SNI 的客户端将后续请求发送给 B。

HTTP Alternative Services 是通过 Alt-Svc 响应头的方式呈现的:

  1. Alt-Svc: h2="backup-server.skk.moe:114514", h2=":1919", h2="another-server.skk.moe"; ma=86400; persist=1

其中,h2=":443 定义了可选替代服务使用的协议、主机名、端口:h2 是可选协议为 HTTP/2,= 后面的内容就是替代服务地址。其中如果和替代服务主机名和当前主机名一致,则主机名可以被忽略;如果替代服务使用的是协议的默认端口号,则端口号可以被忽略(但是两个不能同时忽略)。ma (max-age)则表示服务端已经做好了「心理准备」,在这段时间里如果客户端发送了以新的协议(也包括新的编码方式)的请求,服务端都能够理解和作出响应。而 persist=1 表示客户端发生网络连接方式变化的时候依然允许复用这一 HTTP Alternative Services。

当客户端首次请求是使用 HTTP/1.1 请求服务端时,服务端使用明文发送的响应头进行协商,因此只有客户端只有在第二次请求时才可以使用替代服务;而在 HTTP/2 中新增了专门的 ALTSVC 帧,客户端接收到该帧后,下一帧就可以直接发送给替代服务,因此可以做到收到的第一个响应就来自替代服务。

简单来说,HTTP Alternative Services 就是客户端向服务端发送请求后,服务端响应时说「诶嘿嘿,你看,我还在这些地方通过这些方式提供相同的服务,你要不要来试一试?」。

在大部分浏览器实现中,虽然使用了替代服务,但是地址栏中的主机名、DevTools 中的 Remote Address 都还是原始服务地址。实际上,处于安全考虑,RFC7838 规定:

  • 用于替代的服务必须部署 TLS
  • 如果原始服务使用了 HTTPS,那么替代服务必须使用同一张 SSL 证书。而在 Chrome 中,甚至要求只有 HTTPS 网站才可以使用 HTTP Alternative Services。

HTTP Alternative Services 是如此强大,因此除了 Mozilla 和 Akamai 当初在 RFC7838 中列出的设计用途以外,还已经被用于以下用途:

  • 可选加密(Opportunistic Encryption):对于使用 HTTP 明文(一般都是 HTTP/1.1)的请求,服务端可以通过指定 Alt-Svc 的请求头将后续请求「升级」到 HTTPS。注意!使用这种方式「升级」的 HTTPS 不会校验 TLS 证书!(只有 Firefox 允许这种「升级」,Chrome 处于安全性的考虑不允许 HTTP 网站使用 HTTP Alternative Services)目前 Cloudflare 提供了这一功能。
  • 免备案接入国内主机:浏览器首先和位于国外的服务器进行 TCP 和 TLS 握手,国外的服务器通过 Alt-Svc 指示客户端接下来的请求和连接都可以使用国内主机作为替代服务(替代服务中可以指定特殊端口以绕过机房或云厂商在常规 Web 端口上做的白名单拦截),目前已有 相关的实验
  • 洋葱路由优化(Onion Route):在 Tor Browser 中输入正常网站的域名,服务端识别出这是 Tor Browser 或客户端的网络是 Tor 后,可以使用 Alt-Svc 指示后续请求可以发往网站的 Onion 地址、获得更快的速度。目前这一思路已有 Cloudflare 实现。

使用 Alt-Svc 协商 HTTP/3

截至本文写就,HTTP/3 仍然没有在建立连接时就进行协商的方法(同样的,HTTP/3 的头部压缩规范 QPACK 也仍处于草案阶段)。因此即使服务端支持 HTTP/3,客户端 初次建立连接时依然会使用 HTTP/2(甚至可能是 HTTP/1.1)。因此与客户端通过 ALPN 和服务端协商 HTTP/2 不同,HTTP/3 需要服务端和客户端协商,HTTP Alternative Services 此时就可以发挥作用:

  1. Alt-Svc: h3-27=":443"; ma=86400, h3-25=":443"; ma=86400, h3-24=":443"; ma=86400, h3-23=":443"; ma=86400

这一串响应头表示服务端支持四种版本的 HTTP/3 协议,都可以通过当前主机名下的 443 端口进行服务,客户端未来 24 小时之内都可以直接发送 HTTP/3 版本的请求。

使用 HTTP Alternative Services 协商 HTTP/3 时,虽然理论上可以直接通过 HTTP/2 的 ALTSVC 帧容许客户端后续帧使用 HTTP/3 发送。但是在实际上,考虑到客户端兼容性以及 HTTP 协议请求和响应的特点,即使服务端开始了协商,客户端也仍然需要在第二个甚至第三个请求才开始使用 HTTP/3。希望未来 QUIC 在迭代中,IETF 小组能提供更优雅的协商和升级方式。

本文作者 : Sukka
本文采用 CC BY-NC-SA 4.0 许可协议。转载和引用时请注意遵守协议、注明出处!
本文链接 : https://blog.skk.moe/post/http3-alt-svc/