TT Lab
开始
学习 学习路径 课程

实时通信 — WebSocket、gRPC 流式调用与 WebRTC

HTTP/2 — 一个连接承载多个请求

在 TT Lab 中继续学习

一句话总结

HTTP/2 在一个连接上以流的形式叠加承载多个请求,并压缩头部,但作为代价,所有流要共享这一个连接的流量控制窗口和 TCP 字节流。

为什么需要它

HTTP/1.1 的连接一次只处理一个请求。规范中虽然有不等待响应、连续发送请求的管线化(pipelining),但响应必须按请求顺序返回,所以前面慢的响应会把后面的全部拖住。这就是应用层面的队头阻塞(head-of-line blocking)。因此浏览器和客户端库放弃了管线化,转而选择打开多个连接,每个连接都要各自付出 TCP 握手、TLS 握手以及拥塞窗口慢启动的代价。每个请求中同样重复的 Cookie 和认证头部,也每次都以明文重新发送。

RFC 9113 的 HTTP/2 针对的就是这两种浪费。连接只保留一个,在其上打开多个相互独立的流,头部则用带有连接级状态的压缩(RFC 7541 HPACK)来缩减。gRPC 就运行在它之上,而流式响应突然停住的故障,有相当一部分就出在这里所说明的流量控制窗口上。

工作原理

HTTP/2 中的一切都是帧。第 4.1 节规定的帧头部固定为 9 字节。

字段 大小 含义
Length 24 位 不含头部的正文长度。默认上限为 16,384(SETTINGS_MAX_FRAME_SIZE)
Type 8 位 DATA 0x0、HEADERS 0x1、RST_STREAM 0x3、SETTINGS 0x4、PING 0x6、GOAWAY 0x7、WINDOW_UPDATE 0x8 ……
Flags 8 位 像 END_STREAM、END_HEADERS 这样,含义因类型而异
R + Stream Identifier 1 + 31 位 保留位发送时为 0,接收时忽略

由客户端打开的流使用奇数编号,由服务器打开的流使用偶数编号,0 号用于整个连接的控制(SETTINGS、PING、GOAWAY)。一个请求以 HEADERS 帧开始,以 END_STREAM 标志结束,不同流的帧在一个连接内可以任意交错地流动。连接总是以客户端的连接前言(以 PRI * HTTP/2.0 开头的 24 字节)和双方的 SETTINGS 开始,而决定使用 HTTP/2 的方法,在 TLS 中是 ALPN 的 h2,在明文中是第 3.3 节的先验知识(prior knowledge)。

可以同时打开的流的数量,由接收一方通过 SETTINGS_MAX_CONCURRENT_STREAMS 告知。第 6.5.2 节说明,这个值的初始值是不限,并建议不要设得小于 100。这里有一个常见的陷阱:在对方的第一个 SETTINGS 到达之前,并不知道上限,所以一发出前言就把请求集中发送,就可能超出上限。

流量控制(第 5.2 节、第 6.9 节)只作用于 DATA 帧。窗口是每个流一个,整个连接一个,两者都从 65,535 字节开始。发送方只能按两个窗口中较小的那个发送,接收方通过 WINDOW_UPDATE 归还窗口,发送方才能继续发送。通过 SETTINGS_INITIAL_WINDOW_SIZE 能改变的只有流窗口的初始值,连接窗口只能通过 WINDOW_UPDATE 扩大。所以即使把流窗口告知为 100,000,第一个响应也会停在 65,535 字节。只要有一个不归还窗口的客户端,一个大响应就会用光连接窗口,连同一个连接上 3 字节的响应也得等待。

HPACK 使用包含 61 个条目的静态表,以及每个连接各自累积的动态表(默认 4,096 字节)。在同一个连接上第二次发送相同的头部时,会缩减为一个表编号。如果重新打开连接,表也是空的,需要从头重新增长。压缩的收益来自长时间使用同一个连接。

在现场相遇的样子

常有这样的报告:改用 HTTP/2 之后,慢的页面变快了,可在移动网络上反而变差了。HTTP/1.1 的六个连接相互独立,所以其中一个丢了数据包,其余五个仍然在流动。HTTP/2 的一个连接,无论有多少个流,在 TCP 看来都只是一条字节流,所以在重发一个报文段期间,所有流都会停住。下一个模块会接着讲这个话题。

另一个是 gRPC 流每次停住几秒的故障。如果接收一侧的应用程序取出消息很慢,库就会很晚才归还窗口,发送一侧则因窗口关闭而等待。表现为网络很空闲,吞吐量却跌到谷底。

下一项实验要做什么

使用 h2 库读取帧头部、测量 HPACK 的大小,并构建一个在一个连接上叠加承载多个请求的客户端。然后把它改为遵守并发流上限,并在 16,384、任意值、100,000 这三种情形下测量窗口关闭时的字节数,用数字确认连接窗口的存在。最后设置一个让 TCP 停顿一次的中继器,比较 HTTP/2 和 HTTP/1.1 的小响应各在什么时候结束。