HTTP/2

Table of Contents

1. HTTP/2 简介

HTTP/2 于 2015 年被正式标准化(RFC 7540),目前已经被主流浏览器实现。和 HTTP 1.1 相比,它带来了二进制分帧、服务器推送、头部压缩等等特性。

2. 概念

在开始介绍前,先了解一下 HTTP/2 中的几个概念,流(Stream)、消息(Message)和帧(Frame):

  1. 流:已建立的连接内的双向字节流,可以承载一条或多条消息;每个流都有一个唯一的整数标识符;
  2. 消息:是指逻辑上的 HTTP 消息,比如请求、响应等, 消息由一或多个帧组成。
  3. 帧: HTTP/2 通信的最小单位 ,每个帧包含帧头,至少也会标识出当前帧所属的流,承载着特定类型的数据,如 HTTP 首部、负荷,等等。

流、消息和帧的关系如图 1 所示。

http2_stream_message_frame.svg

Figure 1: 流、消息和帧的关系

3. 帧(Frame)

3.1. 帧(Frame)格式

建立 HTTP/2 连接后,客户端与服务器会通过交换帧来通信,“帧”是 HTTP/2 通信的最小单位。帧(Frame)的格式如下:

+-----------------------------------------------+
|                 Length (24)                   |
+---------------+---------------+---------------+
|   Type (8)    |   Flags (8)   |
+-+-------------+---------------+-------------------------------+
|R|                 Stream Identifier (31)                      |
+=+=============================================================+
|                   Frame Payload (0...)                      ...
+---------------------------------------------------------------+

帧头一共有 9 字节,其中包含长度(3 字节)、类型(1 字节)、标志(1 字节),还有一个保留位和一个 31 位的流标识符(保留位和流标识符共占 4 字节)。下面分别介绍每个字段。

1、长度是有效载荷(Frame Payload)的长度,不包含帧头的 9 字节。

2、类型目前一共有 10 种,其含义如表 1 所示。

Table 1: 帧类型
Frame Type Code
DATA 0x0
HEADERS 0x1
PRIORITY 0x2
RST_STREAM 0x3
SETTINGS 0x4
PUSH_PROMISE 0x5
PING 0x6
GOAWAY 0x7
WINDOW_UPDATE 0x8
CONTINUATION 0x9

3、Flags 目前一共有 5 种,END_STREAM/ACK/END_HEADERS/PADDED/PRIORITY,并不是每个类型都适应这 5 种 Flags。比如,Type 为 DATA 的帧,只有 2 种 Flags: END_STREAM/PADDED。每种 Type 的帧其适应的 Flags 如下(摘自https://metacpan.org/pod/release/CRUX/Protocol-HTTP2-0.14/lib/Protocol/HTTP2/Frame.pm )所示:

                    +-END_STREAM 0x1
                    |   +-ACK 0x1
                    |   |   +-END_HEADERS 0x4
                    |   |   |   +-PADDED 0x8
                    |   |   |   |   +-PRIORITY 0x20
                    |   |   |   |   |        +-stream id (value)
                    |   |   |   |   |        |
| frame type\flag | V | V | V | V | V |   |  V  |
| --------------- |:-:|:-:|:-:|:-:|:-:| - |:---:|
| DATA            | x |   |   | x |   |   |  x  |
| HEADERS         | x |   | x | x | x |   |  x  |
| PRIORITY        |   |   |   |   |   |   |  x  |
| RST_STREAM      |   |   |   |   |   |   |  x  |
| SETTINGS        |   | x |   |   |   |   |  0  |
| PUSH_PROMISE    |   |   | x | x |   |   |  x  |
| PING            |   | x |   |   |   |   |  0  |
| GOAWAY          |   |   |   |   |   |   |  0  |
| WINDOW_UPDATE   |   |   |   |   |   |   | 0/x |
| CONTINUATION    |   |   | x | x |   |   |  x  |

4、Reserved 字段是保留位,暂时未用,目前必须设置为 0x0

5、Stream Identifier 是流的唯一标识符。

3.2. Header 压缩和解压

在 HTTP/1.x 中,Header 始终以纯文本形式,通常会给每个传输增加 500–800 字节的开销。如果使用 HTTP Cookie,增加的开销有时会达到上千字节。

为了减少此开销和提升性能,HTTP/2 使用 HPACK(定义在 RFC7541 中)压缩请求和响应的 Header 元数据,这种格式采用两种简单但是强大的技术:

  1. 支持通过静态 Huffman 编码对传输的标头字段进行编码,从而减小了各个传输的大小。
  2. 要求客户端和服务器同时维护和更新一个包含之前见过的标头字段的索引列表(换句话说,它可以建立一个共享的压缩上下文),此列表随后会用作参考,对之前传输的值进行有效编码。

利用霍夫曼编码,可以在传输时对各个值进行压缩,而利用之前传输值的索引列表,我们可以通过传输索引值的方式对重复值进行编码,索引值可用于有效查询和重构完整的标头键值对。

http2_header_compression.svg

Figure 2: Header Compression

作为一种进一步优化方式,HPACK 压缩上下文包含一个“静态表”和一个“动态表”:静态表在规范中定义,并提供了一个包含所有连接都可能使用的常用 HTTP 标头字段的列表(如表 2 所示);动态表最初为空,将根据在特定连接内交换的值进行更新。因此,为之前未见过的值采用静态 Huffman 编码,并替换每一侧静态表或动态表中已存在值的索引,可以减小每个请求的大小。

Table 2: Static Table Definition
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
9 :status 204
10 :status 206
11 :status 304
12 :status 400
13 :status 404
14 :status 500
15 accept-charset  
16 accept-encoding gzip, deflate
17 accept-language  
18 accept-ranges  
19 accept  
20 access-control-allow-origin  
21 age  
22 allow  
23 authorization  
24 cache-control  
25 content-disposition  
26 content-encoding  
27 content-language  
28 content-length  
29 content-location  
30 content-range  
31 content-type  
32 cookie  
33 date  
34 etag  
35 expect  
36 expires  
37 from  
38 host  
39 if-match  
40 if-modified-since  
41 if-none-match  
42 if-range  
43 if-unmodified-since  
44 last-modified  
45 link  
46 location  
47 max-forwards  
48 proxy-authenticate  
49 proxy-authorization  
50 range  
51 referer  
52 refresh  
53 retry-after  
54 server  
55 set-cookie  
56 strict-transport-security  
57 transfer-encoding  
58 user-agent  
59 vary  
60 via  
61 www-authenticate  

HTTP/2 中的 Header 又称为 Header Block Fragment,它可以出现在 HEADERS/PUSH_PROMISE/CONTINUATION 这三种类型的帧中。

3.2.1. 为什么不使用 gzip 压缩

在标准化前,HTTP/2 的早期实现中,Header 的压缩直接使用带有一个自定义字典的 gzip 压缩算法。但后来发现它容易遭受 CRIME (Compression Ratio Info-leak Made Easy) 攻击。于是,gzip 压缩算法被新设计的 HPACK 算法替代。

4. 流(Stream)

流(Stream)是 HTTP/2 连接内的双向字节流,一个连接中可以同时包含多个打开的流,每个流都有一个唯一的整数标识符。

4.1. 流状态

流是有状态的,如下所示:

                                +--------+
                        send PP |        | recv PP
                       ,--------|  idle  |--------.
                      /         |        |         \
                     v          +--------+          v
              +----------+          |           +----------+
              |          |          | send H /  |          |
       ,------| reserved |          | recv H    | reserved |------.
       |      | (local)  |          |           | (remote) |      |
       |      +----------+          v           +----------+      |
       |          |             +--------+             |          |
       |          |     recv ES |        | send ES     |          |
       |   send H |     ,-------|  open  |-------.     | recv H   |
       |          |    /        |        |        \    |          |
       |          v   v         +--------+         v   v          |
       |      +----------+          |           +----------+      |
       |      |   half   |          |           |   half   |      |
       |      |  closed  |          | send R /  |  closed  |      |
       |      | (remote) |          | recv R    | (local)  |      |
       |      +----------+          |           +----------+      |
       |           |                |                 |           |
       |           | send ES /      |       recv ES / |           |
       |           | send R /       v        send R / |           |
       |           | recv R     +--------+   recv R   |           |
       | send R /  `----------->|        |<-----------'  send R / |
       | recv R                 | closed |               recv R   |
       `----------------------->|        |<----------------------'
                                +--------+

          send:   endpoint sends this frame
          recv:   endpoint receives this frame

          H:  HEADERS frame (with implied CONTINUATIONs)
          PP: PUSH_PROMISE frame (with implied CONTINUATIONs)
          ES: END_STREAM flag
          R:  RST_STREAM frame

参考:https://tools.ietf.org/html/rfc7540#section-5.1

4.2. 流的优先级

将 HTTP 消息分解为很多独立的帧(Frame)之后,我们就可以复用多个数据流中的帧,客户端和服务器交错发送和传输这些帧的顺序就成为关键的性能决定因素。为了做到这一点,HTTP/2 标准允许每个数据流都有一个关联的“权重”和“依赖关系”:

  1. 可以向每个数据流分配一个介于 1 至 256 之间的整数作为权重。
  2. 每个数据流与其他数据流之间可以存在显式依赖关系。

数据流“依赖关系”和“权重”的组合让客户端可以构建和传递“优先级树”,表明它倾向于如何接收响应。 反过来,服务器可以使用此信息通过控制 CPU、内存和其他资源的分配设定数据流处理的优先级,在资源数据可用之后,带宽分配可以确保将高优先级响应以最优方式传输至客户端。

http2_stream_prioritization.svg

Figure 3: Stream Weight and Dependency

我们来看一下图 3 中的几个示例。从左到右依次为:

  1. 数据流 A 和数据流 B 都没有指定父依赖项,依赖于隐式“根数据流”;A 的权重为 12,B 的权重为 4。因此,根据比例权重:数据流 B 获得的资源是 A 所获资源的三分之一。
  2. 数据流 D 依赖于根数据流;C 依赖于 D。因此,D 应先于 C 获得完整资源分配。 权重不重要,因为 C 的依赖关系拥有更高的优先级。
  3. 数据流 D 应先于 C 获得完整资源分配;C 应先于 A 和 B 获得完整资源分配;数据流 B 获得的资源是 A 所获资源的三分之一。
  4. 数据流 D 应先于 E 和 C 获得完整资源分配;E 和 C 应先于 A 和 B 获得相同的资源分配;A 和 B 应基于其权重获得比例分配。

4.3. 流控制

流控制是一种阻止发送方向接收方发送大量数据的机制,以免超出后者的需求或处理能力:发送方可能非常繁忙、处于较高的负载之下,也可能仅仅希望为特定数据流分配固定量的资源。 例如,客户端可能请求了一个具有较高优先级的大型视频流,但是用户已经暂停视频,客户端现在希望暂停或限制从服务器的传输,以免提取和缓冲不必要的数据。 再比如,一个代理服务器可能具有较快的下游连接和较慢的上游连接,并且也希望调节下游连接传输数据的速度以匹配上游连接的速度来控制其资源利用率;等等。

上述要求会让您想到 TCP 流控制吗?您应当想到这一点;因为问题基本相同。不过,由于 HTTP/2 数据流在一个 TCP 连接内复用,TCP 流控制既不够精细,也无法提供必要的应用级 API 来调节各个数据流的传输。为了解决这一问题,HTTP/2 提供了一组简单的构建块,这些构建块允许客户端和服务器实现其自己的数据流和连接级流控制:

  1. 流控制具有方向性。每个接收方都可以根据自身需要选择为每个数据流和整个连接设置任意的窗口大小。
  2. 流控制基于信用。每个接收方都可以公布其初始连接和数据流流控制窗口(以字节为单位),每当发送方发出 DATA 帧时都会减小,在接收方发出 WINDOW_UPDATE 帧时增大。
  3. 流控制无法停用。建立 HTTP/2 连接后,客户端将与服务器交换 SETTINGS 帧,这会在两个方向上设置流控制窗口。流控制窗口的默认值设为 65,535 字节,但是接收方可以设置一个较大的最大窗口大小(2^31-1 字节),并在接收到任意数据时通过发送 WINDOW_UPDATE 帧来维持这一大小。
  4. 流控制为逐跃点控制,而非端到端控制。即,可信中介可以使用它来控制资源使用,以及基于自身条件和启发式算法实现资源分配机制。

5. 各种类型的帧

5.1. DATA

DATA 类型的帧可以传输任意数据。DATA 帧的 Payload 如下所示:

+---------------+
|Pad Length? (8)|
+---------------+-----------------------------------------------+
|                            Data (*)                         ...
+---------------------------------------------------------------+
|                           Padding (*)                       ...
+---------------------------------------------------------------+

仅当 Flag PADDED 设置上后,才有 Pad Length,也就是说 Flag PADDED 没有设置时,Pad Length 不存在。

5.2. HEADERS

HEADERS 类型的帧有两个作用:

  1. 打开一个流(Stream);
  2. 传输 Header Block Fragment。

HEADERS 帧的 Payload 如下所示:

+---------------+
|Pad Length? (8)|
+-+-------------+-----------------------------------------------+
|E|                 Stream Dependency? (31)                     |
+-+-------------+-----------------------------------------------+
|  Weight? (8)  |
+-+-------------+-----------------------------------------------+
|                   Header Block Fragment (*)                 ...
+---------------------------------------------------------------+
|                           Padding (*)                       ...
+---------------------------------------------------------------+

Dependency 和 Weight 仅当 Flag PRIORITY 设置为 1 时才存在,给它们分别表示流的“依赖关系”和“权重”,参考节 4.2

Header Block Fragment 除了可以出现在 HEADERS 帧中,还可以出现在 PUSH_PROMISE/CONTINUATION 类型的帧中。

5.3. PRIORITY

PRIORITY 帧用于发送者告诉对方自己对流优先级的建议设置。

PRIORITY 帧的 Payload 如下所示:

+-+-------------------------------------------------------------+
|E|                  Stream Dependency (31)                     |
+-+-------------+-----------------------------------------------+
|   Weight (8)  |
+-+-------------+

5.4. RST_STREAM

RST_STREAM 帧用于终止一个流,或者表明遇到了某种错误。

RST_STREAM 帧的 Payload 如下所示:

+---------------------------------------------------------------+
|                        Error Code (32)                        |
+---------------------------------------------------------------+

Error Code 如表 3 所示。

Table 3: Error Code (用于 RST_STREAM 帧和 GOAWAY 帧)
Error Code Meaning
NO_ERROR (0x0) Not an error. For example, a GOAWAY might include this code to indicate graceful shutdown of a connection.
PROTOCOL_ERROR (0x1) The endpoint detected an unspecific protocol error.
INTERNAL_ERROR (0x2) The endpoint encountered an unexpected internal error.
FLOW_CONTROL_ERROR (0x3) The endpoint detected that its peer violated the flow-control protocol.
SETTINGS_TIMEOUT (0x4) The endpoint sent a SETTINGS frame but did not receive a response in a timely manner.
STREAM_CLOSED(0x5) The endpoint received a frame after a stream was half-closed.
FRAME_SIZE_ERROR (0x6) The endpoint received a frame with an invalid size.
REFUSED_STREAM (0x7) The endpoint refused the stream prior to performing any application processing
CANCEL (0x8) Used by the endpoint to indicate that the stream is no longer needed.
COMPRESSION_ERROR (0x9) The endpoint is unable to maintain the header compression context for the connection.
CONNECT_ERROR (0xa) The connection established in response to a CONNECT request was reset or abnormally closed.
ENHANCE_YOUR_CALM (0xb) The endpoint detected that its peer is exhibiting a behavior that might be generating excessive load.
INADEQUATE_SECURITY (0xc) The underlying transport has properties that do not meet minimum security requirements.
HTTP_1_1_REQUIRED (0xd) The endpoint requires that HTTP/1.1 be used instead of HTTP/2.

5.5. SETTINGS

SETTINGS 帧用于指定参数。SETTINGS 参数不协商,它们描述了发送端的特征,被接收端使用。

SETTINGS 帧必须在连接开始时由两端发送,并且可以在连接的整个生命周期内由任一端点在任何其他时间发送。SETTINGS 帧始终适用于连接,而不针对单个流。

SETTINGS 帧的有效载荷由 0 个或多个参数组成,每个参数由一个无符号的 16 位设置标识符和一个无符号的 32 位值组成。每个参数格式如下所示:

+-------------------------------+
|       Identifier (16)         |
+-------------------------------+-------------------------------+
|                        Value (32)                             |
+---------------------------------------------------------------+

5.5.1. 设置参数

目前标准中定义了 6 个设置参数。如表 4 所示。

Table 4: SETTINGS 帧的设置参数
SETTINGS Parameter Meaning
SETTINGS_HEADER_TABLE_SIZE (0x01) 通知对方端点用于解码 header 块的 header 压缩表的最大尺寸,初始为 4096 字节。
SETTINGS_ENABLE_PUSH (0x02) 可用于禁用服务器推送。初始值为 1(启用服务器推送)。
SETTINGS_MAX_CONCURRENT_STREAMS (0x3) 指定最大并发流数。这个限制是有方向性的:它适用于发送者允许接收者创建的数据流。初始值是没有限制。
SETTINGS_INITIAL_WINDOW_SIZE (0x4) 流控制的初始窗口大小。初始值是 2^16-1(65535)字节。
SETTINGS_MAX_FRAME_SIZE (0x5) 最大帧有效载荷的大小。初始值是 2^14 (16384) 字节。
SETTINGS_MAX_HEADER_LIST_SIZE (0x6) 通知对方发送方可以接受的 header 列表的最大大小。初始值是没有限制。

5.5.2. 设置同步

当收到对方发来的 SETTINGS 帧,自己在处理完后,需要及时地进行确认。也就是回复对方一个 SETTINGS 帧,并把 Flag ACK 设置上。

如果发送了一个普通的 SETTINGS 帧给对方,有一定时间内没有收到对方的 ACK(也是一个 SETTINGS 帧),则给对方发送一个 GOAWAY ,并标明原因是 SETTINGS_TIMEOUT。

5.6. PUSH_PROMISE(服务器推送)

所有服务器推送数据流都由 PUSH_PROMISE 帧发起,表明了服务器向客户端推送所述资源的意图, 并且需要早于请求推送资源的响应数据传输。这种传输顺序非常重要:客户端需要了解服务器打算推送哪些资源,以免为这些资源创建重复请求。满足此要求的最简单策略是 先于父响应(即,DATA 帧)发送所有 PUSH_PROMISE 帧,其中包含所承诺资源的 HTTP 头。

在客户端接收到 PUSH_PROMISE 帧后,它可以根据自身情况选择拒绝数据流(通过 RST_STREAM 帧)。例如,如果资源已经位于缓存中,客户端就可以选择拒绝数据流。

使用 HTTP/2,客户端仍然完全掌控服务器推送的使用方式。客户端可以限制并行推送的数据流数量;调整初始的流控制窗口以控制在数据流首次打开时推送的数据量;或完全停用服务器推送。这些参数可以通过在 HTTP/2 连接开始时传输 SETTINGS 帧来控制,而且以后也可以随时更新。

PUSH_PROMISE 帧的 Payload 如下所示:

+---------------+
|Pad Length? (8)|
+-+-------------+-----------------------------------------------+
|R|                  Promised Stream ID (31)                    |
+-+-----------------------------+-------------------------------+
|                   Header Block Fragment (*)                 ...
+---------------------------------------------------------------+
|                           Padding (*)                       ...
+---------------------------------------------------------------+

5.7. PING

PING 类型的帧有两个作用:

  1. 测量来自发送方的最小往返时间(round-trip time);
  2. 以及确定空闲连接是否仍然有效。

PING 帧的 Payload 如下所示:

+---------------------------------------------------------------+
|                                                               |
|                      Opaque Data (64)                         |
|                                                               |
+---------------------------------------------------------------+

5.8. GOAWAY

GOAWAY 帧用于开始连接关闭过程或发出严重错误信号。GOAWAY 允许端点正常停止接受新的流,同时仍然完成对先前建立的流的处理。这可以实现管理操作,例如服务器维护。

GOAWAY 帧适用于连接,而不针对特定的流。

GOAWAY 帧的 Payload 如下所示:

+-+-------------------------------------------------------------+
|R|                  Last-Stream-ID (31)                        |
+-+-------------------------------------------------------------+
|                      Error Code (32)                          |
+---------------------------------------------------------------+
|                  Additional Debug Data (*)                    |
+---------------------------------------------------------------+

Error Code 和 RST_STREAM 帧的 Error Code 一样,可参见节 5.4

5.9. WINDOW_UPDATE(流控制)

WINDOW_UPDATE 帧用于实现流控制(Flow control),参考节 4.3

WINDOW_UPDATE 帧的 Payload 如下所示:

+-+-------------------------------------------------------------+
|R|              Window Size Increment (31)                     |
+-+-------------------------------------------------------------+

5.10. CONTINUATION

CONTINUATION 帧用于继续发送 Header Block Fragment 的序列。如果相同流上的前导帧是没有设置 END_HEADERS 标记的 HEADERS,PUSH_PROMISE,或 CONTINUATION 帧,就可以发送任意数量的 CONTINUATION 帧。

CONTINUATION 帧的 Payload 如下所示:

+---------------------------------------------------------------+
|                   Header Block Fragment (*)                 ...
+---------------------------------------------------------------+

CONTINUATION 帧必须与某个流相关联。

6. 参考

Author: cig01

Created: <2020-02-15 Sat>

Last updated: <2020-07-04 Sat>

Creator: Emacs 27.1 (Org mode 9.4)