写点什么

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

评论

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

「架构实战营」毕业总结

DaiChen

「架构实战营」

重学架构之拆分电商系统为微服务

陈华英

架构实战营

模块六作业-拆分电商系统为微服务

浪飞

ABAP 简易弹出输出/输入框

Jasen Ye

Input abap decide CONFIRM

表数据都删了一半,可我的表文件咋还是那么大

华为云开发者联盟

MySQL 文件 innodb 数据页

模块六作业

Geek_ec866b

架构训练营

模块九作业-设计电商秒杀系统

CH

架构实战营

【模块六】拆分电商系统为微服务

yhjhero

#架构训练营

什么是分支模式 ? 各(类)分支正确的使用方式

阿里云云效

云计算 阿里云 云原生 Feature 分支模式

Ant Design Landing

云原生

SaaS React Ant Design Landing page

Vue

wudaxue

AppCube低代码快速开发健康打卡应用

DS小龙哥

3月月更

一文了解MySQL的Buffer Pool

华为云开发者联盟

MySQL 缓存 缓冲池 Buffer Pool 脏页

架构训练营第一期作业

Geek_bc9c8d

「架构实战营」模块六 电商微服务框架设计

hxb

「架构实战营」

「架构实战营」模块九《十万级到亿万级 IM 架构实战》作业

DaiChen

作业 「架构实战营」 模块九

浏览器原理

wudaxue

常见的反爬措施:UA反爬和Cookie反爬

华为云开发者联盟

Python 爬虫 Python爬虫 反爬虫 Cookie反爬虫

适合 Kubernetes 初学者的一些实战练习(一)

汪子熙

云原生 #Kubernetes# Kubernetes 集群 Serverless Kubernetes 3月月更

模块六作业

Leo

架构实战营

模块六作业

blazar

「架构实战营」

Java中的序列化安全漏洞梳理

陈德伟

Java 安全 编程语言、 序列化机制

关于技术团队的考核(24/100)

hackstoic

技术管理

一文带你了解 Python 中的装饰器

踏雪痕

Python 装饰器 3月程序媛福利 3月月更

时间轮原理及其在框架中的应用

vivo互联网技术

服务器 时间轮

【前端架构必备】手摸手带你搭建一个属于自己的脚手架

战场小包

前端 脚手架 3月月更

电商系统微服务拆分实践

IT屠狗辈

微服务 架构实战营 电商系统架构 架构拆分

电商系统微服务拆分

tom

DDD实战(6):战略设计之技术决策

深清秋

DDD 软件架构 生鲜电商系统 3月月更

云原生-模块十二

hunk

css

wudaxue

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