写点什么

HTTP/2 in GO(二)

  • 2019-11-18
  • 本文字数:5218 字

    阅读完需:约 17 分钟

HTTP/2 in GO(二)

上一篇文章中介绍了 HTTP/2 的二进制分帧和多路复用的特性,这次来介绍下头部压缩和服务端推送。

HTTP/2 新增特性

  • 二进制分帧(HTTP Frames)

  • 多路复用

  • 头部压缩

  • 服务端推送(Server Push)

1 头部压缩

在 HTTP/1.x 中,每次 HTTP 请求都会携带需要的 header 信息,这些信息以纯文本形式传递,所以每次的请求和响应,都会浪费一些带宽,如果 header 信息中包含 cookie 等之类的信息,那么浪费的带宽就更可观了。为了减少带宽开销和提升性能,HTTP/2 使用 HPACK 压缩格式压缩请求和响应标头元数据,这种格式采用两种简单但是强大的技术:


  • 这种格式支持通过静态 Huffman 编码对传输的 header 字段进行编码,从而减小了传输的大小。

  • 这种格式要求客户端和服务器同时维护和更新一个包含之前见过的 header 字段的索引列表(换句话说,它可以建立一个共享的压缩上下文),此列表随后会用作参考,对之前传输的值进行有效编码。


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


客户端和服务端都有一个内置的静态表,部分内容如下:


静态表+-------+-----------------------------+---------------+| 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             |               |...| 58    | user-agent                  |               || 59    | vary                        |               || 60    | via                         |               || 61    | www-authenticate            |               |+-------+-----------------------------+---------------+         
复制代码


可以看到,部分静态表已经包含了 value,比如 Index=2 的 :method = GET,当客户端发起请求时,如果发起的是 GET 请求,那么只需要在 Header 信息中携带一个 Index=2 的索引即可,服务端收到通过静态表即可查出对应的请求头信息。


在静态表中传输的 Header Block 是这种格式的:



0 1 2 3 4 5 6 7 +---+---+---+---+---+---+---+---+ | 1 | Index (7+) | +---+---------------------------+
复制代码


从图中可以看到,只需要 8-bit 即可实现一个 method 的 Header 传输:



with index


对于静态表中不存在 value 的值,或者 value 的值跟想传递的值不一样时,就不能只传递简单的 Index 了;比如对于:path 的头信息,如果要请求的 path 不在静态表里,就需要用到 Huffman 编码 了。


假设:path 的值为/post/20180811-http2_in_go_1.html


     0   1   2   3   4   5   6   7   +---+---+---+---+---+---+---+---+   | 0 | 0 | 0 | 0 |  Index (4+)   |   +---+---+-----------------------+   | H |     Value Length (7+)     |   +---+---------------------------+   | Value String (Length octets)  |   +-------------------------------+   
复制代码


从下图中能看到,占用了 25 个 Byte 来传递:path=/post/20180811-http2_in_go_1.html 的信息,这个数值比传输明文字符串要节省空间;当然是由于这些字符串普遍在 Huffman 编码 的压缩比较高的字典里,经过编码后会占用较小的空间,如果要传输的都是一个比较奇怪的字符,那么也有可能出现编码后占用的空间比之前还要高。



那如果我们应用要传输的就是一些奇怪的字符串,难道我们要每次传输比直接更大的值么,其实不然。除了静态表,HPACK 算法还提供了一个 动态表,双方针对每个 connection 共同维护这个表,这样对于之前未出现过的 Header 信息,只要传输一次,那么下次大家就都了解了。


比如下边这个 user-agent,第一次传输时,Index 是从本地静态表获取,传输给服务端后,会把 Header Name+Header Value 同时更新到本地的动态表里;这样本地和服务端都同时存在一个相同 id 的动态表了,这里大家都追加到了 Index=76 的动态表,再次传递时,只要跟静态表的结构一样即可。占用 1Byte 就 ok。


第一次传输:



第二次传输:



最后总结下,用 Roberto Peon(HPACK 的设计者之一)的话说:


“HPACK 旨在提供一个一致性的实现使信息量的损失尽可能少,使编解码快速而方便,使接收方能控制压缩文本的大小,允许代理重新建立索引(如,通过代理在前后端共享状态),以及对哈夫曼编码串的更快速比较”

2 服务端推送(Server Push)

Server Push 指的是服务端主动向客户端推送数据,相当于对客户端的一次请求,服务端可以主动返回多次结果。这个功能打破了严格的请求—响应的语义,对客户端和服务端双方通信的互动上,开启了一个崭新的可能性。但是这个推送跟 websocket 中的推送功能不是一回事,Server Push 的存在不是为了解决 websocket 推送的这种需求。


对我们的 web 应用来说,举个最简单的例子,有一个 index.html 页面:


<!DOCTYPE html><html><head>  <link rel="stylesheet" href="push-style.css"></head><body>  <h1>hello, server push</h1>  <img src="push-image.png"></body></html>
复制代码


  • 在 HTTP/1.x 里,为了展示这个页面,客户端会先发起一次 GET /index.html 的请求,拿到返回结果进行分析后,再发起两个资源的请求,一共是三次请求, 并且有串行的请求存在。

  • 在 HTTP/2 里,当客户端发起 GET /index.html 的请求后,如果服务端进行了 Server Push 的支持,那么会直接把客户端需要的/index.html 和另外两份文件资源一起返回,避免了串行和多次请求的发送。


大家可以看看 Go 官方给的这个 Server Push 的例子:



这个功能的实现,主要就依赖于上一篇文章提到的 PUSH_PROMISE Frame,所有的推送请求,都是有 PUSH_PROMISE 来发起,服务端通过向客户端在返回正常的 Response 前,优先发送 PUSH_PROMISE,来表达自己即将为客户端推送的资源,当客户端收到请求后,针对这些资源,就不会再向服务端发起请求。


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


PUSH_PROMISE 中,包含了一个 Promised Stream ID,这个是服务端承诺向客户端推送相关数据时使用的 Stream ID,Header Block 中包含资源链接等相关内容。


客户端收到 PUSH_PROMISE 后,可以选择接受服务器推送的资源,如果客户端发现本地缓存已经存在,不需要服务端再推送,也可以向对应的 Stream ID 发送 RST_STREAM 帧,来阻止服务端发送 Push.


下边看几张用 h2c(不是 ClearText 的 h2c,是一个 HTTP/2 Command-Line Client)模拟 http2 请求的图来看效果,还是访问的上边 Go 官方给的 Server Push 的网页:


在发起 GET /serverpush 的请求后,收到了服务端发送的 PUSH_PROMISE,承诺在 2、4、6、8 等 stream ID 的流中给发送相应的资源信息:



然后可以看到,在 Stream Id 为 2、4、6、8 的流上开始给客户端发送 Header 帧和 Data 帧的数据,顺序不定,同时也在向 Get /serverpush 这个 stream ID=1 的流上返回相关的页面信息。



最后,就是各个 Stream 传输自己的 Data 数据,直到数据传输完毕,打上 END_STREAM 的 Flag 表明流传输结束。


Stream 状态机

说完这两个 HTTP/2 的特性,对整体概念应该有所了解了,最后说下 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
复制代码


其中,half closed 状态,就是进入 Server Push 后的状态,有一方,其实就是客户端,进入了半开闭状态,这时候它不能通过这个 Stream 再发送请求的相关数据,只能接受数据,或者选择结束链接。half closed 状态可以由 idle 状态经过两条路径到达:


  • 服务端发送 PUSH_PROMISE 后发送 Header 帧信息,使客户端进入半开闭;

  • 客户端通过 Header 帧向服务端发送数据,并在 Header 标记 END_STREAM 的 Flag,表明自己期望结束 Stream,不再向服务端发送 Header 或 Data 的数据,这个时候服务端还没同意关闭 Stream,所以服务端是可以向客户端发送数据的。


这里边比较奇怪的就是 reserved 和 half closed 两个状态,看起来没有什么区别,通过一个无关紧要的 Header 帧来触发状态转换。


其实,在 Go 语言里,对 HTTP/2 的 Stream State 的实现,就是把 reserved 和 half closed 当做一个状态给合并了。



那么为什么会有这两个状态呢,其实是出于 Stream Concurrency 并发的限制,在并发限制里,reserved 状态不计入活跃状态,不进行限制。这样能达到的一个效果就是,即使 Stream 并发数达到限制以后,服务端仍然是能向客户端发送 PUSH_PROMISE 的,能够一定程度的防止 PUSH_PROMISE 不能发送而导致的客户端竞争请求。


这块我也是简单介绍下,想了解更仔细的话,可以参考这篇文章 RFC7540 笔记(四)——More on Stream States


好了,本次就说这些。下次开始介绍 HTTP/2 在 Go 语言中的一些实现和用法。


本文转载自公众号 360 云计算(ID:hulktalk)。


原文链接:


https://mp.weixin.qq.com/s/5tcqd40by8GBSsnTQEo-OQ


2019-11-18 19:05976

评论

发布
暂无评论
发现更多内容

现代分布式架构设计原则-可靠性

余先生

稳定性 可用性 弹性 可靠性

手把手撸二叉树之叶子相似的树

HelloWorld杰少

面试 大前端 二叉树 数据结构与算法 8月日更

服装生产流程管理在明道云的实现

明道云

Git的基本操作

卢卡多多

git flow git reset 8月日更

02-架构图

Lane

Pandas入门教程-Series类型数据

Peter

Python 数据分析 数据 pandas

缓存使用的一些问题

旺仔大菜包

redis

Convolutional Neural Network (CNN)

毛显新

神经网络 深度学习 tensorflow 图像识别

聊聊实时数仓架构设计

水滴

实时数仓 数仓架构 8月日更 数仓建设思路

网络攻防学习笔记 Day92

穿过生命散发芬芳

网络攻防 8月日更

「SQL数据分析系列」13. 索引和约束

Databri_AI

sql 索引 位图

【Vue2.x 源码学习】第二十二篇 - dep 和 watcher 关联

Brave

源码 vue2 8月日更

【前端 · 面试 】HTTP 总结(一)—— HTTP 概述

编程三昧

面试 大前端 HTTP 8月日更

pyinstaller 打包

橙橙橙橙汁丶

解密NFT,进军元宇宙,区块链与价值实体将如何链接?

CECBC

非典型开发者的形象三变

脑极体

在线短视频缩略图剪切工具

入门小站

工具

具备货币属性的比特币,会成为一种货币吗?

CECBC

菜鸡学习python

Augus

8月日更

架构实战营毕业总结

白发青年

#架构实战营

Python OpenCV 图像处理之傅里叶变换,取经之旅第 52 篇

梦想橡皮擦

8月日更

【LeetCode】矩阵中战斗力最弱的 K 行Java题解

Albert

算法 LeetCode 8月日更

做行业的底层架构者 为区块链+提供更多可能

CECBC

李运华老师(前阿里P9)架构实战营 毕业总结

代廉洁

架构实战营

大数据训练营 -0725 课后作业

cc

毕业设计-秒杀业务

白发青年

架构实战营

架构实战营-毕业设计

泄矢的呼啦圈

架构实战营

Discourse 图片上传的更新

HoneyMoose

Pandas入门教程-开篇之作

Peter

Python pandas 数据分析师 #python

架构师实战营 模块九总结

代廉洁

架构实战营

Linux中Shell重定向

入门小站

Linux

HTTP/2 in GO(二)_文化 & 方法_付坤_InfoQ精选文章