HTTP/2 大致可以分为两部分: 分帧层 ,即 多路复用能力的核心部分; 数据 http 层,其中包含传统上被认为是 Hπ 及其关联数据的部分。彻底分开这两层,把它们当成彼此 独立的事物,这是非常诱人的想曲。仔细阅读 /2 规范的读者会发现,分帧层究竟是 被设计成完全通用的、可重用的数据结构,还是用来传输 TI 内容, 者是有些分歧 的。比如,规范起初泛泛地讨论了终端和双向通信一一这对许多消息系统来讲是完美无缺的 一一 然后 舌锋 转,讨论起了客户端、服务器、请求和响应。读到分帧层的时候,不要 忘记这 事实:它的目的是传输 HT ,而不是其他。
- 二进制协议
旧的分帧层是基于帧的 进制协议。这方便了机器解析,但是肉眼识别起来比较困难。
- 首部压缩
仅仅使用 进制协议似乎还不够, h2 的首部还会被深度压缩。这将显著减少传输中的 元余字节。
- 多路复用
在你喜爱的调试工具里查看基于 h2 传输的连接的时候,你会发现请求和晌应交织在 起。
- 加密传输
最重要的是,线上传输的绝大部分数据是加密过的,所以在中途读取会更加困难。 现在,我们来展开这些话题。
这和 hl 是一样的,不过与完全无状态的 hl 不同的是, h2 把 它所承载的帧(仕arne )和流(stream )共同依赖的连接层元素捆绑在 起,其中既包含连 接层设置也包含首部表(稍后有对两者更详细的描述)。
为了向服务器双重确认客户端支持 h2 ,客户端会发送 个叫作 connection preface (连接 前奏)的魔法字节流,作为连接的第一份数据。这主要是为了应对客户端通过纯文本的 HπP/1.1 升级上来的情况。该字节流用十六进制表示如下:
0x505249202a204854545 2f322e300d0a0d0a534d0d0a0d0a
解码为 ASCII 是:
PRI * HTTP/2.0 \r\n\r\nSM\r\n\r\n
这个魔撞字符串会有一个 SETTINGS 帧紧随其后。
之前说过, HπP/2 是基于帧(frame )的协议。采用分帧是为了将重要信息都封装起来, 让协议的解析方可以轻松阅读、解析井还原信息。 相比之下, hl 不是基于帧的,而是以 文本分隅。
HTTP 帧结构:
h2 协议中有 10 种不同的帧类型。
HTTP/2 规范对流( strea )的定义 HTTP/2 连接上独 、双向的帧序列交换。”你 可以将流看作在连接上的一系列帧,它们构成了单独的 HTTP 请求和响应。如果客户端想 要发出请求,它会开启 个新的流。然后,服务器将在这个流上回 复。
客户端到服务器的 h2 连接建立之后,通过发送 HEADERS 帧来启动新的流,如果肯 部需要跨多个帧,可能还发会送 CONTINUATION 帧(更 多信息参见下面 的附 注栏 CONTINUATIONS ”)。该 HEADERS 帧可能来自 请求,也可能来自响应,具体 取决于发送方。后续流启动的时候,会发送 个带有递增流 ID 的新 HEADERS 帧。
HTTP 肖息泛指 HTTP 请求或响应。流是用来传输一对请求/响应消息 的。。 个悄息至少囱 HEADERS 帧(它初始化流)组成,井且可以另外包含 CONTINUATION DATA 帧,以及其他的 HEADE 帧。 5-2 是普通 GET 请求的示 例流程。
- 一切都是header
旧的新特性之 是基于流的流量控制。不同于 hl 的世界,只要客户端可以处理,服务端 就会尽可能快地发送数据, h2 提供了客户端调整传输速度的能力 。(并且,由于在 h2 中, 一切几乎都是对称的,服务端也可以调整传输的速度。) WINDOW_UPDATE 帧用来指示 流量控制信息。每个帧告诉对方,发送方想要接收多少字节。当 端接收井消费被发送的 数据时,它将发出 WINDOW_UPDATE 帧以指示其更新后的处理字节的能力。(许多 早期的 HπP/2 实现者花了大量时间调试窗口更新机制,来回答“为什么我没有取到数据” 的问题。)发送方有责任遵守这些限制。
流的最后 个重要特性是依赖关系。现代浏览器都经过了精心设计,首先请求网页上最重 要的元素,以最优的顺序获取资橱,由此来优化页面性能。拿到了 HTML 之后,在渲染页 面之前,浏览器通常还需要 css 和关键 Java cri pt 这样的东西。在没有多路复用的时候, 在它可以发出对新对象的请求之前,需要等待前 个响应完成。有了 ,客户端就可以 一次发出所有资惊的请求,服务端也可以立即着手处理这些请求。由此带来的问题是,浏 览器失去了在 hl 时代默认的资掘请求优先级策略。假设服务器同时接收到了 100 个请求, 也没有标识哪个更重要,那么它将几乎同时发送每个资橱,次要元素就会影响到关键元 的传输
h2 通过流的依赖关系来解决这个问题。通过 HEADE 帧和 PRIO TY 帧,客户揣可以 明确地和服务端沟通它需要什么,以及它需要这些资掘的顺序。
提升单个对象性能的最佳方式,就是在它被用到之前就放到浏览器的缓存里面。这正是 HπP/2 的服务端推送的目的 推送使服务器能够主动将对象发给客户端,这可能是因为 它知道客户端不久将用到底对象。
如果服务器决定要推送 一 个对象(RFC 中称为“推送响应勺,会构造一个 PUSH_ PROMISE 帧。
- PUSH_PROMISE 帧首部中的流 ID 用来响应相关联的请求。
- PUSH_PROMISE 帧的首部块与客户端请求推送对象时发送的首部块是相似的。所以客 户端有办出放心检查将要发送的请求。
- 被发送的对象必须确保是可缓存的。
- method 首都的值必须确保安全。安全的方怯就是署等的那些方怯,这是 种不改变 任何状态的好办峰。
- 理想情况下, PUSH_PROMISE 应该更早发送,应当早于客户端接收到可能承载着推 送对象的 DATA 帧。
- PUSH_PROMISE 帧会指示将要发送的响应所使用的流 ID
如果客户端对 USH_p OMISE 的任何元素不楠意,就可以按照拒收原因选择重置这个流 (使用 RST_STREAM ),或者发送 PROTOCOL_ERROR (在 GOAWAY 帧中)。
根据应用的不同,选择推送哪些资源的逻辑可能非常简单,也可能异常复杂。
- 资掘已经在浏览器缓存中的概率
- 从客户端看来,这些资顿的优先级(参见 5.4 节)
- 可用的带宽,以及其他类似的会影响客户端接收推送的资源
首部压缩 (旧ACK )将会是 凹的关键元素之一。但是首部应该怎么压缩?经过多次创新性的思考和讨论,人们提出了回ACK CK 是种表查找压缩方案,它来用霍夫曼编码获得接近 GZIP 压缩率。
下面是 个专门设计的简化的例子 来帮助你理解 HPACK 到底做了些什么
假设客户端按顺序发送如下请求首部: Header1 foo Header2: ba Heade 3: bat
当客户端发送请求时, 可以在首部数据块中指示特定首部及其应该被索引的值。它会创建一张表:
如果服务端读到了这些请求首部,它会照样创建 张表。客户端发送下 个请求的时候, 如果首部相同,它可以直接发送这样的首部块:
62 63 64