写点什么

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:05991

评论

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

Qt|控件的事件过滤使用与总结

中国好公民st

c++ qt 10月月更

自学java培训之间学习方法有哪些

小谷哥

JavaScript——数据类型

胖虎不秃头

前端 js 10月月更

Docker可视化工具Portainer的安装和使用

琦彦

Docker Portainer 10月月更 可视化管理工具

Python应用之寻求两个数对之间的最大乘积

芯动大师

Python语法 10月月更 split函数

JavaScript——JS事件

胖虎不秃头

前端 js 10月月更

2022 Kubernetes 批处理和HPC发展一览

琦彦

HPC 批处理 KubeCON 10月月更

二分查找

掘金安东尼

算法 10月月更

基于BuildKit优化Dockerfile的构建

琦彦

Dockerfile 10月月更

docker-maven-plugin:自动构建Docker镜像,并推送到Docker Registry或阿里云

琦彦

Docker maven 10月月更 docker-maven-plugin

Docker层和虚悬镜像(dangling image)介绍

琦彦

Docker 10月月更

ESP32-C3 学习测试 蓝牙 篇(七、GATT 数据通信 — 发送自定义数据)

矜辰所致

蓝牙 ESP32-C3 10月月更

大数据培训学习如何选择?

小谷哥

前端培训学习比较实用的方法

小谷哥

用纯CSS实现优惠券剪卡风格

茶无味的一天

CSS 前端

国庆perfetto学习

留白的艺术

利用Vue自定义指令让你的开发变得更优雅

茶无味的一天

Vue 前端 vue指令

构建Java镜像的10个最佳实践

琦彦

Java应用 Docker 镜像 10月月更

前端培训机构选择哪些建议

小谷哥

如何零基础学习大数据培训

小谷哥

Python应用之丑数的判断

芯动大师

Python 10月月更 丑数

JVM——垃圾回收算法

琦彦

Java JVM 垃圾回收 10月月更

JavaScript——关于JavaScript、在HTML中嵌入JS代码的三种方式、变量

胖虎不秃头

前端 js 10月月更

陈宗绵|关于研发效能的理想与现实

laofo

DevOps cicd 研发效能 持续集成 持续交付

开发者有话说|以码为梦,心向远方,路在脚下

乌龟哥哥

个人成长 10月月更

vue快速入门---高速版

楠羽

笔记 VUE 3.0 源码 10月月更

如何以非root用户运行Docker容器

琦彦

Docker 10月月更 root用户

Timestone: Netflix的高吞吐、低延迟优先级队列系统

俞凡

架构

【C语言难点突破】指针和数组名的爱恨情仇

Geek_65222d

10月月更

浅谈前端工程化的发展以及相关工具介绍

茶无味的一天

Python应用之拉力赛求最短时间

芯动大师

10月月更 Python语法应用 函数构建

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