HTTP/2 学习笔记

知乎 · · 2748 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

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 均保留),其改变了数据是如何传输的。

  1. 多路复用: 为了更高效地利用多路复用,另外实现了 Flow Control 和 Prioritization
  2. Server Push
  3. 头部压缩

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 里面,因此必须在 idlehalf 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

本文来自:知乎

感谢作者:知乎

查看原文:HTTP/2 学习笔记

2748 次点击  
加入收藏 微博
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传