前言

最近被问到 HTTP/2 有什么特点,我只答了个多路复用,当时脑子还想到了二进制分帧和头部压缩,我以为是 HTTP/3 的就没答上来。

由于 HTTP/2 普及率不高,而且最近 IETF 将 QUIC 发布为 RFC9000,这意味着 HTTP/3 即将到来。所以很有可能会跳过 HTTP/2 直接进入 HTTP/3 ,所以对 HTTP/2 没有了解太多。

不过既然被问到了那就好好学一下 HTTP/2

HTTP/2 名字的变化

HTTP/2 与之前的 HTTP/1.0HTTP/1.1 不一样,它去掉了小版本号,只保留了大版本号,这么改的原因是 HTTP/2 工作组认为 1.0、1.1 造成了很多混乱和误解,在使用的过程中难以区分,所以就不再使用小版本号。

所以从 HTTP/2 开始,就只有 HTTP/2、HTTP/3 ,不会出现类似 1.0、1.1 的情况。

HTTP/2 改进的方向

我们知道在 HTTP/1.x 上存在者一些问题,比如队头阻塞,基于请求-响应方式通讯一次只能处理一个请求,后序的请求需要排队。安全问题,这个问题被 HTTPS 解决了,所以不是 HTTP/2 主要关注的地方。头部字段没有进行压缩,许多数据会重复传输,浪费了带宽。

知道了问题,就可以着手开始对这些地方进行改进,所以 HTTP/2 主要在性能上作出改进,通过 HPACK 算法进行头部压缩,使用二进制流传输数据达到多路复用的目的,完美的解决了队头阻塞的问题。

头部压缩

HTTP/2 中一个重要的特性就是对头部进行压缩,采用 HPACK 算法进行压缩。

HPACK 算法是专门为压缩 HTTP 头部定制的算法,与 gzip、zlib 等压缩算法不同,它是一个 有状态 的算法,需要客户端和服务器各自维护一份 索引表 ,也可以说是 字典 ,压缩和解压缩就是查表和更新表的操作。

为了方便管理和压缩,HTTP/2 废除了原有的起始行概念,把起始行里面的请求方法、URI、状态码等统一转换成了头字段的形式,并且给这些“不是头字段的头字段”起了个特别的名字——“伪头字段”(pseudo-header fields)。而起始行里的版本号和错误原因短语因为没什么大用,顺便也给废除了。

为了与“真头字段”区分开来,这些“伪头字段”会在名字前加一个“:”,比如“:authority” “:method” “:status”,分别表示的是域名、请求方法和状态码。为了方便 HTTP/2 就为一些最常用的头字段定义了一个只读的“静态表”(Static Table)。

Index Header Name Header Value
1 :authority
2 :method GET
3 :method POST
4 :path /
5 :path /index.html
6 :scheme http
7 :scheme https
8 :status 200
59 vary
60 via
61 www-authenticate

在表中有些只有 Key 没有 Value 或者这个字段不在表中,这时候就需要使用到动态表了,它被添加在静态表后,在编码和解码的时候进行更新。

随着报文发送和接收越来越多,这个表会越来越丰富,压缩效果就会越来越好,原本上千字节的报文现在只需要发送一个编号,压缩效率非常高。

更多更详细的可以参考 HPACK: Header Compression for HTTP/2

二进制帧

在 HTTP/1.x 版本中,协议的内容是纯文本,人们非常容易看懂,调试起来也很方便。

但是从 HTTP/2 开始,把纯文本改成了二进制格式,这样改的好处是便于计算机解析,不容易出现歧义。比如空格、大小写、空白字符等,这些在不同的人看来是有可能出现歧义的,有些空白字符肉眼还识别不出来。

所以改成二进制后,实现变简单了,体积更小,速度更快。

我们来看下二进制的结构,如下所示,图片来自 31 | 时代之风(下):HTTP/2内核剖析 这里的 HEADERHTTP/1 中的不是同一个, HTTP/2 把头字段放到 DATA 帧中,通过标志位来区分。而 HTTP/1 中是通过换行符来区分头部和实体。

从上面的图中可以看出,已经完全没有了 Header+Body 的结构,有的都是一个个二进制帧。

由于这里也有 Header ,容易和 HTTP/1.xHeader 搞混,需要注意一下。它们不是一个东西, HTTP/2 是放在二进制帧中,它不会保存请求头的东西,请求头的内容被放在 Payload 中。

  • 帧长度:代表整个帧的长度,不包括头9个字节,用24位无符号数表示。默认上限是16K(2^14),最大是16M(2^24)。
  • 帧类型:用于区分那种帧,常见的有数据帧和控制帧, HEADERSDATA 都是数据帧。而 SETTINGSPINGPRIORITY 则是控制帧,用于管理流的。
  • 标志位:这是一个非常重要的信息,可以保存8个标志位,常用的有 END_HEADERS 表示数据头结束,可以理解位 HTTP/1 中的换行(\r\n)。 END_STREAM 表示单方向数据发送结束。
  • R:保留的比特位,没有定义它的语义,发送时必须被设置为 0x0 ,接收时需要忽略。
  • 流标识符:用于区分这帧属于哪个流,从而可以从乱序收发的流中找出特定请求和响应消息。需要注意这里的最高位保留不使用,所以这里真正有效的只有31位,所以一个TCP连接中流的上限大约是21亿(2^31),也就是21亿个请求响应。

虚拟流

上面我们说道 HTTP/2 采用二进制帧进行传输数据,这些帧到达服务器之后怎么知道它们属于哪个请求呢?

HTTP/2 提出了流的概念,用来表示一次请求和响应,也就是说同一次请求和响应它们的流ID是一样的。

你也可以理解为它是二进制帧的双向传输序列,通过一个唯一的流ID来区分不同的流,同一个消息往返的帧会在同一个流里。

所以每一次请求和响应都能够通过流ID来区分,所以只要流ID不同就可以发送不同的请求。

也就是在同一个TCP连接上可以发送多个请求,而不用每次都创建一个新的连接,这就是我们通常说的多路复用。

这些流在请求和响应之间没有顺序关系,不会要求排队等待,所以就不会出现队头阻塞的问题了。

画个图,方便理解,图片来自 30 | 时代之风(上):HTTP/2特性概览

HTTP/2 中服务器不再是完全的被动接受请求,还可以主动向客户端发送消息,这被成为服务器推送。

怎么标识HTTP/2

之前提到 HTTP/2 为了兼容 HTTP/1.x 在协议名上还是使用的 http ,这样如何确认双方需要使用 HTTP/2 通信呢,不像 https 有一个明确 s

HTTP/2 连接成功之前使用的还会 HTTP/1 发送一个请求报文,用来确认建立 HTTP/2 连接。这个被叫做连接前言。

它的内容如下

1
PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n

服务器收到这个字符串就知道客户端想要的是 HTTP/2 协议。

安全性

安全性并不是 HTTP/2 主要考虑的问题, HTTP/2 出于兼容性的考虑没有强制使用 HTTPS ,但是由于 HTTP/2 使用二进制使得人类难以阅读,即便你得到流数据,在不借助工具的情况下也很难看懂,所以在一定程度上有一定的安全性,至少比 HTTP/1.x 要好。

虽然不强制使用加密,但是主流的浏览器都只支持加密的 HTTP/2 ,所以事实上 HTTP/2 是加密的。

为了区分加密和明文这两个版本, HTTP/2 协议规定流两个字符串用来标识是否加密,也就是 h2 表示的是加密的 HTTP/2 ,而 h2c 表示的是明文的 HTTP/2cclear 的意思。

HTTP/2 还要求 TLS 的版本必须是 1.2 以上的,必须支持前向安全和 SNI 。去掉了一些弱密码套件,比如 DESRC4CBC 等。

协议栈

通过一张图看看 HTTP/1HTTPSHTTP/2 协议栈的区别,图片来自30 | 时代之风(上):HTTP/2特性概览

参考