Why HTTP/2
- HTTP/1.1 是文本协议
- 对用户友好,但对计算机不友好 (parse 不高效)
- TCP 连接管理
- TCP 慢启动,因此尽可能的复用连接 (keep-alive)
- 一个 TCP 连接上只能有一个 request/response,可以使用 pipeline 实现并发请求,但会有头部阻塞问题,现代浏览器默认不启用 pipeline,而是通过对一个域名同时建立多个连接 workaround
- 头部 field 重复,造成资源浪费
- 资源没有优先级
HTTP/2 特性:
HTTP/2 没有改变 HTTP/1.1 的语义 (method, status code, URI, header 均保留),其改变了数据是如何传输的。
- 多路复用: 为了更高效地利用多路复用,另外实现了 Flow Control 和 Prioritization
- Server Push
- 头部压缩
HTTP2 基本概念
HTTP/1.1 是文本协议,而 HTTP/2 是二进制协议,其通过发送不同的二进制帧(frame)来进行传输
先来看看 HTTP/2 frame 的格式
+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+=+=============================================================+
| Frame Payload (0...) ...
+---------------------------------------------------------------+
所有的 frame 都有一个 9 byte 的 header 和不定长的 payload 组成。header 中的 Length 指 payload 的长度,因为可以很方便的 parse。stream identifier 是每个 stream 唯一的标志,client 发起的 stream stream identifier 为奇数,server 发起的 stream stream identifier ,stream identifier 为 0x0 表示这个 frame 针对整个 connection 而不是单个 stream。
上面的 Frame 是一个很容易理解的物理概念,而 HTTP/2 在 Frame 的基础上抽象出了 Stream。Stream 其实就是一段包含客户端和服务端交换 frame 的序列。
HTTP/2 中的多路复用其实指的就是一个 HTTP/2 连接上可以同时存在多个 stream。
Stream 状态机
+--------+
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
理解 HTTP/2 必须理解 stream 的状态以及如何通过发送不同的 frame 使 stream 状态发生转换。上面的图比较复杂,接下来会逐个来看。
首先来介绍最基本的两种 frame,HEADER 和 DATA。可以简单的看做:HEADER 用来传输 HTTP/1.1 的 header,DATA 用来传输 HTTP/1.1 的 body。这两种 frame 用来传递真正对上层应用有用的信息,其他 frame 都是用来控制 HTTP/2 stream / connection。
+---------------+
|Pad Length? (8)|
+-+-------------+-----------------------------------------------+
|E| Stream Dependency? (31) |
+-+-------------+-----------------------------------------------+
| Weight? (8) |
+-+-------------+-----------------------------------------------+
| Header Block Fragment (*) ...
+---------------------------------------------------------------+
| Padding (*) ...
+---------------------------------------------------------------+
+---------------+
|Pad Length? (8)|
+---------------+-----------------------------------------------+
| Data (*) ...
+---------------------------------------------------------------+
| Padding (*) ...
注意上面的图都只是对应 frame 的 payload 部分。
HEADER 和 DATA 有个 flag END_STREAM 值得提一下,当这个 flag 被设置后,说明这是最后一个 HEADER/DATA,数据都已经传完了。
另外还有个 frame 先提下,RST_STREAM 用来关闭 stream,常常用在当 error 发生的时候。
+---------------------------------------------------------------+
| Error Code (32) |
+---------------------------------------------------------------+
有了这几种 Stream,我们可以很容易的画出最基本的 stream 状态机。
+--------+
| |
| idle |
| |
+--------+
|
| send H
| recv H
|
v
+--------+
| |
| open |
| |
+--------+
|
|
| send R
| recv R
|
|
|
v
+--------+
| |
| closed |
| |
+--------+
首先每个 stream 开始都处于 idle 状态。当 client 发起 request 时,client 首先发送 一个Header frame ,client 进入 open 状态。当 server 收到这个 Header frame 后,server 也进入 open 状态。client / server 进入 open 状态后,就可以发送/接受数据了。server 发起 response 也是同理,只不过是把 client 和 server 的位置互换罢了。
当任何一方认为这个 stream 需要终止时,可以发送 RST_STREAM frame 给对方,发送后发送方的状态变为 closed,接收方接收到后的状态也变为 closed。在 closed 的状态下,双方不能交换 frame,这个 stream 的生命周期也就结束了。
上面的流程有一个很大的问题,当某一方认为自己的数据已经发完,可以关闭 stream 的时候,但他不知道对方的数据是否发完没有,如果这时候贸然的关闭 stream 会导致接下来的数据收不到。在 HTTP/2 中,更常规的做法(上面的 RST_STREAM 只有发生错误的情况下才会使用,属于非常规做法)是在发送数据最后的一个 frame 中,设置 END_STREAM flag。发送带 END_STREAM flag frame 端的状态变为 half closed(local), 表明数据已经发完了,不会再发数据,但可以接受对方的数据。收到带 END_STREAM flag frame 端的状态变为 half closed(remote),表明还可以发数据,对方会接受,但对方已经不会再发数据了。
+--------+
| |
| idle |
| |
+--------+
|
| send H
| recv H
|
v
+--------+
recv ES | | send ES
,-------| open |-------.
/ | | \
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 |
`----------->| |<-----------'
| closed |
| |
+--------+
Server Push
常见的web请求是先拿一个 html,然后 client 在去请求这个 html 中包括的 css,js。但是server 在返回 html 给 client 的时候是知道 html 中内容的,如果支持 server push,可以直接把需要的资源推给 client,减少了 client 解析再请求的时间。
HTTP/2 的 Server Push 是通过在上一个 stream 中插入 PUSH_PROMISE frame 实现的。
+---------------+
|Pad Length? (8)|
+-+-------------+-----------------------------------------------+
|R| Promised Stream ID (31) |
+-+-----------------------------+-------------------------------+
| Header Block Fragment (*) ...
+---------------------------------------------------------------+
| Padding (*) ...
+---------------------------------------------------------------+
上一个 stream 是指 client 请求导致 server push 的 stream。按上面的例子来说,就是请求你 html 的那个 stream。具体把这个 PUSH_PROMISE 插入在哪里是个比较 trick 的地方,假设我们放在上一个 stream 的最后,client 先收到 html(此时 server push 的内容还没收到),然后发起对 css,js 的请求,这样 server push 就没有意义了。因此,HTTP/2 规定:
The server SHOULD send PUSH_PROMISE frames prior to sending any frames that reference the promised responses. This avoids a race where clients issue requests prior to receiving any PUSH_PROMISE frames.
发送 PUSH_PROMISE 会把一个 idle stream (PUSH_PROMISE 中 Promised Stream ID 对应的 Stream)的状态置为 reversed(local) (对server而言,对 client 而言是 reversed(remote))。开始 server push 后,会先发送 HEADER frame, 然后状态置为 half closed(remote) (对 client 而言是 half closed(local))
+--------+
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 |
`----------------------->| |<----------------------'
+--------+
这里有个商榷的地方,为何需要把带推送的 stream 的状态设置为 reversed(local) 而不是 half closed(remote),毕竟从语义上来讲,Server Push 时客户端不会发送 frame,整个 stream 就应该是 half closed 状态。在RFC 5.1.2 节中,HTTP 规定了一个 HTTP/2 连接中的并发 stream 数目,而只有能够发送 DATA frame 的 stream 才算做并发 stream 里面,而在 reversed 下,由于还没有发送 HEADER,这个 STREAM 不能发送 DATA,自然也就不能算作并发 stream 里面,因此必须在 idle 和 half closed 之间显式的另外引入一个状态来表明某个 stream 是之后要进行 server push,但还没有开始 (即没有发送 HEADER),而这个状态就是 reversed。虽然 RFC 里面是这么规定的,但实现上貌似并不是特别遵守,Golang 的 HTTP/2 实现就是没有 reversed 状态的。
Flow Control
因为引入了 Stream Multiplex,各个 stream 之间有了资源竞争,为了更好的分配资源,需要在应用层实现 Flow Control (TCP 的 flow control 是针对单个 connection,没法对 stream 这个更上层的抽象做)
HTTP/2 只提供了一种机制来实现流量控制,而没有具体指定使用哪种算法(由实现者自己决定)。而这种机制是通过发送 WINDOW_UPDATE 这种 frame 来实现的。
+-+-------------------------------------------------------------+
|R| Window Size Increment (31) |
+-+-------------------------------------------------------------+
具体实现是 connection 和 每个 stream 都会有一个 flow-control window, 发送的 DATA payload 大小不能超过这个 window。每发送一个 DATA,connection window 和对应的 stream window 都会减去 DATA payload 长度大小,当收到 connection / stream WINDOW_UPDATE ,connection / stream window 会增大对应 Window Size Increment 大小。
Reference