硬核干货——《中小企业 AI 实战指南》免费下载! 了解详情
写点什么

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

评论

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

实测对比|法国AI独角兽公司发布的“最强OCR”,实测效果如何?

合合技术团队

人工智能 算法 OCR #大模型

吉卜力风图像席卷社交潮流,GPU正在「融化」

PowerVerse

动画 AI‘’ gpu 算力

怎么用AI制作甘特图?7个甘特图生成工具盘点!

职场工具箱

人工智能 项目管理 甘特图 AI软件 AIGC

《Operating System Concepts》阅读笔记:p528-p544

codists

操作系统

基于控制面单元化的 Kubernetes 集群联邦

字节跳动开源

Kubernetes 多集群管理 控制面单元化 资源调度优化 容灾备份策略

EDGNEX与Hyperco建立合作关系

财见

Rust 中的高效视频处理:利用硬件加速应对高分辨率视频

Yeauty

rust ffmpeg Video media audio

相比于DeepSeek,Manus有哪些优势

老张

大模型 DeepSeek Manus

微信分享的CAD图纸在电脑上打不开是怎么回事?

在路上

cad cad看图 CAD看图王

字符串替换研究

京东科技开发者

首发,Higress AI 网关率先支持 QwQ-32B,推理成本可再降 90%

阿里巴巴云原生

人工智能 云计算

Higress 开源 Remote MCP Server 托管方案,并将上线 MCP 市场

阿里巴巴云原生

云计算

AI to B奇点将至,“伙伴+华为”体系如何跨越数智鸿沟?

Alter

拼多多商品详情接口(pdd.item_get)

tbapi

拼多多商品详情接口 拼多多API 拼多多商品数据采集

【灯塔计划】【积微成著】规模化混沌工程体系建设及AI融合探索

京东科技开发者

Nacos 发布 MCP Registry,实现存量应用接口“0改动”升级到 MCP 协议

阿里巴巴云原生

云计算

芯盾时代全线产品接入DeepSeek 让企业数字化业务更安全

芯盾时代

零信任 DeepSeek

支持东方40语种+中国22方言的新SOTA语音大模型Dolphin开源

极客天地

RealClip:轻量级战略能否打开全球小游戏市场的巨额潜力?

xuyinyin

CAD如何进行图形统计并导出表格?

在路上

cad cad看图 CAD看图王

通义灵码 Rules 来了:个性化代码生成,对抗模型幻觉

阿里巴巴云原生

云计算 通义灵码

2025 年 AppSheet 最佳开源替代品

NocoBase

开源 低代码 零代码 无代码 AppSheet

LED小间距:拉动显示屏商显市场的主要动力

Dylan

LED LED display LED显示屏 市场 LED屏幕

如何在 IDE 里使用 DeepSeek?

阿里巴巴云原生

云计算 通义灵码

25年湖北等保测评机构名单汇总

行云管家

等保 堡垒机 等保测评

【LangChain】一文读懂RAG基础以及基于langchain的RAG实战

京东科技开发者

混合APP的性能测试

北京木奇移动技术有限公司

APP开发 混合app开发 软件外包公司

在HarmonyOS NEXT 开发中,如何指定一个号码,拉起系统拨号页面

威哥爱编程

HarmonyOS HarmonyOS NEXT

基于阿里云可观测产品构建企业级告警体系的通用路径与最佳实践

阿里巴巴云原生

云计算

怎样购置有限元分析软件 Abaqus?达索代理商思茂信息为您答疑

思茂信息

abaqus 达索系统 正版软件

基于阿里云可观测产品构建企业级告警体系的通用路径与最佳实践

阿里巴巴云原生

云计算

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