--- HTTP/2 协议 --- ### 目录 1. HTTP/2 分层 2. 连接 3. 帧 4. 流 ### HTTP/2 分层 HTTP/2 大致可以分为两部分:分帧层,即 h2 多路复用能力的核心部分;数据或 http 层,其中包含传统上被认为是 HTTP 及其关联数据的部分。彻底分开这两层,把它们当成彼此独立的事物。 尽管数据层被设计成可以向后兼容 HTTP/1.1,对于熟悉 h1 并习惯于阅读线上协议的开发者来说,还有些地方需要重新确认。 **二进制协议** h2 的分帧层是基于帧的二进制协议。这方便了机器解析,但是肉眼识别起来非常困难。 **首部压缩** 仅仅使用二进制协议似乎还不够,h2 的首部还会被深度压缩,这将显著减少传输中的冗余字节。 **多路复用** 在你喜爱的调试工具里查看基于 h2 传输的连接的时候,你会发现请求和响应交织在一起。 ##### 加密传输 最重要的是,线上传输的绝大部分数据是加密过的,所以在中途读取会更加困难。 ### 连接 连接是所有 HTTP/2 会话的基础元素,其定义是客户端初始化一个 TCP/IP socket,客户端是指发送 HTTP 请求的实体。这和 h1 是一样的,不过与完全无状态的 h1 不同的是,h2 把它所承载的帧和流共同依赖的连接层元素捆绑在一起,其中既包含连接层设置也包含首部表。 是否支持 h2? 协议发现 --- 识别终端是否支持你想要使用的协议,会比较棘手。HTTP/2 提供两种协议发现的机制。 在连接不加密的情况下,客户端会利用 Upgrade 首部来表明期望使用 h2,如果服务器也可以支持 h2,它会返回一个 “101 Switching Protocols”(协议转换)响应,这增加了一轮完整的请求 - 响应通信。 如果连接基于 TLS,情况就不同了。客户端在 Clienthello 消息中设置 ALPN(应用层协议协商)扩展来表明期望使用 h2 协议,服务器用同样的方式回复。如果使用这种方式,那么 h2 在创建 TLS 握手的过程中完成协商,不需要多余的网络通信。 为了向服务器双重确认客户端支持 h2,客户端会发送一个叫做 connection preface(连接前奏)的魔法字节流,作为连接的第一份数据。这主要是为了应对客户端通过纯文本的 HTTP/1.1 升级上来的情况,该数据的 ASCII 为: ``` PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n ``` 这个字符串的用处是,如果服务器不支持 h2,就会产生一个显式错误。这个消息特意设计成 h1 消息的样式,如果运行良好的 h1 服务器收到这个字符串,它会阻塞这个方法(PRI)或者版本(HTTP/2.0),并返回错误,可以让 h2 客户端明确的知道发生了什么错误。 ### 帧 HTTP/2 是基于帧的协议,采用分帧是为了将重要信息封装起来,让协议的解析方可以轻松阅读、解析并还原信息。相比之下,h1 不是基于帧的,而是以文本分割。 下面是一个 HTTP/2 帧的结构: ![](https://i.loli.net/2019/11/11/dlngaVBcK3xAY6Q.png) 前九个字节对于每个帧是一致的,解析时只需要读取这些字节,就可以准确的知道在整个帧中期望的字节数。其中每个字段的说明,如下表: | 名称 | 长度 | 描述 | | ---------------- | -------- | ---------------------------------------- | | Length | 3 字节 | 表示帧负载的长度 | | Type | 1 字节 | 当前帧类型 | | Flags | 1 字节 | 具体帧类型的标识 | | R | 1 位 | 保留位,不要设置 | | Stream Identifer | 31 位 | 每个流的唯一 ID | | Frame Payload | 长度可变 | 真实的帧内容,长度在 Length 字段中设置的 | HTTP/2 帧类型: | 名称 | ID | 描述 | | ------------- | ---- | -------------------------------------- | | DATA | 0x0 | 传输流的核心内容 | | HEADERS | 0x1 | 包含 HTTP 首部,和可选的优先级参数 | | PRIORITY | 0x2 | 指示或者更改流的优先级和依赖 | | RSY_STREAM | 0x3 | 允许一端停止流 | | SETTINGS | 0x4 | 协商连接时参数 | | PUSH_PROMISE | 0x5 | 提示客户端,服务器要推送些东西 | | PING | 0x6 | 测试连接可用性和往返时延 | | GOAWAY | 0x7 | 告诉另一端,当前端已结束 | | WINDOW_UPDATE | 0x8 | 协商一端将要接收多少字节,用于流量控制 | | CONTINUATION | 0x9 | 用以扩展 HEADER 数据库 | ### 流 HTTP/2 规范对流的定义是:HTTP/2 连接上独立的、双向的帧序列交换。你可以将流看作在连接上的一系列帧,它们构成了单独的 HTTP 请求和响应。如果客户端想要发出请求,它会开启一个新的流。然后,服务器将在这个流上回复。这与 h1 的请求/响应流程类似,重要的区别在于,因为有分帧,所以多个请求和响应可以交错,而不会互相阻塞。流 ID(帧首部的第 6~9 字节)用来标识帧所属的流。 客户端到服务器的 h2 连接建立之后,通过发送 HEADERS 帧来启动新的流,如果首部需要跨多个帧,可能还会发送 CONTINUATION 帧。该 HEADERS 帧可能来自 HTTP 请求,也可能来自响应,具体取决于发送方。后续流启动的时候,会发送一个带有递增流 ID 的新的 HEADERS 帧。 > CONTINUATION 帧 > > HEADERS 帧通过在帧的 Flags 字段中设置 END_HEADERS 标识位来标识首部结束。在单个 HEADERS 帧装不下所有 HTTP 首部的情况下,不会设置 END_HEADERS 标识位,而是在之后跟随一个或多个 CONTIUNATION 帧。我们可以把 CONTINUATION 帧当做特殊的 HEADERS 帧。那么,为什么要使用特殊的帧,而不是再次使用 HEADERS 帧?如果重复使用 HEADERS,那么后续的 HEADERS 帧的负载就得经过特殊处理才能和之前的拼接起来。 > > 需要注意的是,由于 HEADERS 和 CONTINUATION 帧必须是有序的,使用 CONTINUATION 帧会破坏或减损多路复用的益处。CONTINUATION 帧是解决重要场景(大首部)的工具,但只能在必要时使用。 ### 消息