AICon 深圳站聚焦 Agent 技术、应用与生态,大咖分享实战干货 了解详情
写点什么

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

评论

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

构建高效Presubmit卡点,落地测试左移最佳实践

大卡尔

ci 测试左移 Presubmit

Flutter 命令本质之 Flutter tools 机制源码深入分析

工匠若水

flutter android dart Gradle

网络攻防学习笔记 Day85

穿过生命散发芬芳

网络攻防 7月日更

基于 Blazor 打造一款实时字幕

newbe36524

ASP.NET Core dotnet blazor

架构实战营 模块8 作业

Geek_8c5f9c

#架构实战营

web自动化测试(1):再谈UI发展史与UI、功能自动化测试

zhoulujun

大前端 自动化测试 UI自动化测试 web测试

高基数数据特性是什么意思

HoneyMoose

Fact Table 数据表什么意思

HoneyMoose

大数据训练营一期0718作业

朱磊

在线QuartzCron定时任务表达式在线生成

入门小站

工具

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

cc

在线诉讼区块链证据规则的理论逻辑与制度体系

CECBC

架构实战营 - 模块三作业

思梦乐

架构实战营模块三作业

老猎人

架构实战营

web自动化测试(3):web功能自动化测试selenium基础课

zhoulujun

自动化测试 selenium UI测试 界面测试

技术上的过度医疗

superman

过度设计 完美方案

究竟有没有世界上最好的编程语言?

escray

学习 极客时间 朱赟的技术管理课 7月日更

使用MLlib进行机器学习(十-上)

Databri_AI

机器学习 spark 线性回归

Vite 搭建 Vue2 项目(Vue2 + vue-router + vuex)

德育处主任

JavaScript Vue 大前端 vite

没有隐私计算,区块链这个美丽的梦想就不能落地

CECBC

禁止在构造函数里调用虚函数

喵叔

7月日更

OGC标准WMTS服务概念与地图商的瓦片编号流派-web地图切片加载

zhoulujun

GIS 瓦片地图 地图瓦片服务 WMTS

架构实战营 - 模块三作业

李东旭

「架构实战营」

Scrum Master的职责——《Scrum指南》重读有感(5)

Bruce Talk

Scrum 敏捷 随笔 Agile

OpenCV 形态学操作之腐蚀与膨胀,开运算与闭运算,顶帽与黑帽,图像梯度运算相关知识点回顾

梦想橡皮擦

python从入门到精通 7月日更

赶紧收藏!花了1万多买的软件测试教程全套,包含所有软件测试工程师全栈知识点(功能测试理论基础+接口测试+Python自动化+持续集成+性能测试+测试开发+面试简历)软件测试项目实战+训练营学习教程持

程序员阿沐

Python 软件测试 自动化测试 接口测试 测试用例

Linux之wc命令

入门小站

Linux

web自动化测试(2):选择selenium优势?与PhantomJS/QTP/Monkey对比

zhoulujun

自动化测试 web测试 UI测试 界面测试 页面测试

挑选TOP10关键时刻的九大原则

石云升

读书笔记 用户体验 商业洞察 7月日更 体验设计

MySQL事务初始

卢卡多多

MySQL 事务 7月日更

我不会写代码,但我能做系统

明道云

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