当前位置:首页 > 文章列表 > Golang > Go教程 > goHTTP2的头部压缩算法hpack实现详解

goHTTP2的头部压缩算法hpack实现详解

来源:脚本之家 2022-12-22 19:31:56 0浏览 收藏

本篇文章给大家分享《goHTTP2的头部压缩算法hpack实现详解》,覆盖了Golang的常见基础知识,其实一个语言的全部知识点一篇文章是不可能说完的,但希望通过这些问题,让读者对自己的掌握程度有一定的认识(B 数),从而弥补自己的不足,更好的掌握它。

Hpack 是啥

Hpack 是 HTTP2 的头部压缩算法。在 HTTP1 中,每次传输都会有大量的 Header 携带,我们可以拿一个实际的请求来看,如图一:

图一:请求 header

这里面 Header 很多是请求共性的,比如 method: POST,就是 post 请求的 header,那每个 POST 请求都会携带这个 header;以及同一个页面里可能有很多请求需要带上相同 header,比如 user-agent、鉴权相关 header 等等。那如果 body 很小的话,每次传输利用率就很低了。HTTP2 为了提高传输效率设计了 HPACK 头部压缩算法。

HPACK 原理

HPACK 维护了两张表,静态表和动态表。如果 Header key、value 在表里的话,直接将 Header kv 用 index 编码即可;如果不存在表中的话,则采用 Huffman 编码或者不编码发送。每条连接维护各自的动态表,request 和 response 的动态表是分开的。

静态表存储常见的 Header kv,比如 :method: GET、:method: POST、:schema: http 等一共 61 项,具体的项可以参考 RFC 7541 文档

动态表是一个先进先出的表,先进入的在高索引空间,后进入的在低索引空间(索引空间从0到最后递减)。header 根据一定的规则判断是否加入动态表,有三种规则:

  • 将 header 字段添加到动态表的开头
  • 不将 header 字段添加到动态表
  • 不将 header 添加到动态表,另外规定 header 始终不被动态表编码,常见于有代理或者网关的场景。这是为了保护 header 字段值,比如通过大量尝试判断 header size 可以推断出动态表的内容。

动态表也有一定大小,通过 SETTINGS_HEADER_TABLE_SIZE 来设置。如果新的 Header kv size 超过了这个值,就会逐出动态表,直到能够放下这个 Header kv 或者将所有的逐出。特别的,如果一个 Header kv size 大于了动态表的最大值,那么这个 Header 的作用就是清空动态表。

如何编码

  • 该 Header 已经存在动态表中
  0   1   2   3   4   5   6   7
+---+---+---+---+---+---+---+---+
| 1 |        Index (7+)         |
+---+---------------------------+
  • Key 被索引,value 未索引且允许保存
 0   1   2   3   4   5   6   7
+---+---+---+---+---+---+---+---+
| 0 | 1 |      Index (6+)       |
+---+---+-----------------------+
| H |     Value Length (7+)     |
+---+---------------------------+
| Value String (Length octets)  |
+-------------------------------+

01 后的 index 表示 Header Key 的索引

这个 Header 会被加在 server 和 client 的动态表中。

  • Key 被索引,value 未索引且不允许保存
 0   1   2   3   4   5   6   7
+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | 0 |  Index (4+)   |
+---+---+-----------------------+
| H |     Value Length (7+)     |
+---+---------------------------+
| Value String (Length octets)  |
+-------------------------------+
  • Key、value 均未索引且允许保存
  0   1   2   3   4   5   6   7
+---+---+---+---+---+---+---+---+
| 0 | 1 |           0           |
+---+---+-----------------------+
| H |     Name Length (7+)      |
+---+---------------------------+
|  Name String (Length octets)  |
+---+---------------------------+
| H |     Value Length (7+)     |
+---+---------------------------+
| Value String (Length octets)  |
+-------------------------------+
  • Key、value 均未索引且不允许保存
    0   1   2   3   4   5   6   7
+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | 0 |       0       |
+---+---+-----------------------+
| H |     Name Length (7+)      |
+---+---------------------------+
|  Name String (Length octets)  |
+---+---------------------------+
| H |     Value Length (7+)     |
+---+---------------------------+
| Value String (Length octets)  |
+-------------------------------+
  • Key 被索引,value 未索引且绝对不允许保存
0   1   2   3   4   5   6   7
+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | 1 |  Index (4+)   |
+---+---+-----------------------+
| H |     Value Length (7+)     |
+---+---------------------------+
| Value String (Length octets)  |
+-------------------------------+
  • Key、value 均未索引且绝对不允许保存
 0   1   2   3   4   5   6   7
+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | 1 |       0       |
+---+---+-----------------------+
| H |     Name Length (7+)      |
+---+---------------------------+
|  Name String (Length octets)  |
+---+---------------------------+
| H |     Value Length (7+)     |
+---+---------------------------+
| Value String (Length octets)  |
+-------------------------------+

举个编码?

:method: GET
:scheme: http
:path: /
:authority: www.example.com

编码后的 16 进制如下

8286 8441 8cf1 e3c2 e5f2 3a6b a0ab 90f4 ff

82 = 10000010 -> 8 表示 kv 均被索引,表项为静态表第 2 项-> :method: GET

86 = 10000110 -> 8 表示 kv 均被索引,表项为静态表第 6 项-> :scheme: http

84 = 10000100 -> 8 表示 kv 均被索引,表项为静态表第 4 项 -> :path: /

41 = 01000001 -> 4 表示 Key 被索引,value 未索引且允许保存,name 为静态表第1项,即 :authority。接下来表示这个 header对应的 value。

8c = 10001100 -> 第一个 bit 为1,表示 huffman 编码,字符串的长度为 1100b = 12。接着解析12个字节为 huffman 编码后的字符 f1e3 c2e5 f23a 6ba0 ab90 f4ff, 解码为www.example.com

所以得到最后一个头部 :authority: www.example.com

HPACK 实现

我们可以先想一下,如果要做到索引的复杂度尽可能小,同时又要有序方便逐出,那应该采用什么数据结构呢?

那应该很容易想到,我们需要用一个 slice 存下来所有的数据,也方便逐出;如果一个 Header 来了,我们也需要两个 map 存下这个这个 Header 对应的在 slice 中的 index。

Golang 中 HPACK 的实现在 hpack 文件夹中,动态表的数据结构和我们想的一样。

动态表的实现在 tables.go 当中

 type headerFieldTable struct {
        // 用 slice 存储具体的表项,同时也方便逐出
        ents       []HeaderField
        // 逐出数量,可以理解为偏移修正量。如果一个 header 被逐出后,那其他 header 的
        // 索引就会升高。在 map 中修改需要 O(n) 的开销,所以计算 id 时在这里统一加
        // 一个修正量即可。
        evictCount uint64
        // 只根据 header 找对应的 id。
        byName map[string]uint64
        // 根据 header kv 找对应的 id。
        byNameValue map[pairNameValue]uint64
}
type pairNameValue struct {
        name, value string
}
func (t *headerFieldTable) addEntry(f HeaderField) {
        // 计算唯一 id,同时保证不和已经在表中的 id 重复
        id := uint64(t.len()) + t.evictCount + 1
        // 在两个 map 中存下索引
        t.byName[f.Name] = id
        t.byNameValue[pairNameValue{f.Name, f.Value}] = id
        // 保存索引
        t.ents = append(t.ents, f) 
}
// 逐出 n 个
func (t *headerFieldTable) evictOldest(n int) {
        ...
        for k := 0; k 

其他部分的实现就很简单了,基本上就是照着上面的流程写就可以了。其中有一个解析当前 header 是哪种类型的实现还挺有意思的。

func (d *Decoder) parseHeaderFieldRepr() error {
        b := d.buf[0]
        switch {
        case b&128 != 0:
                // 128 => 10000000
                // 设置了最高位,对应上面的第 1 种 kv 均在的情况
                // https://httpwg.org/specs/rfc7541.html#rfc.section.6.1
                return d.parseFieldIndexed()
        case b&192 == 64:
                // 192 => 11000000
                // 对应前三位为 010 的情况,即允许保存的情况
                // https://httpwg.org/specs/rfc7541.html#rfc.section.6.2.1
                return d.parseFieldLiteral(6, indexedTrue)
        case b&240 == 0:
                // 240 => 11110000
                // 对应前四位都是0的情况,即不允许保存的情况
                // https://httpwg.org/specs/rfc7541.html#rfc.section.6.2.2
                return d.parseFieldLiteral(4, indexedFalse)
        case b&240 == 16:
                // 240 => 11110000
                // 对应前四位是0001的情况,即绝对不允许保存的情况
                // https://httpwg.org/specs/rfc7541.html#rfc.section.6.2.3
                return d.parseFieldLiteral(4, indexedNever)
        case b&224 == 32:
                // 224 => 11100000
                // 对应前三位是001的情况,即动态表大小更新的情况
                // https://httpwg.org/specs/rfc7541.html#rfc.section.6.3
                return d.parseDynamicTableSizeUpdate()
        }
        return DecodingError{errors.New("invalid encoding")}
}

遇到的坑

写这篇文章是因为 hertz 在接入 h3 的时候会偶发的 panic,原因是在动态表存表项的时候,存入了一个 unsafe string,后面这一项给变了,导致双重校验的时候没有删掉,从而引发了 panic。

参考文档

www.rfc-editor.org/rfc/rfc7541

理论要掌握,实操不能落!以上关于《goHTTP2的头部压缩算法hpack实现详解》的详细介绍,大家都掌握了吧!如果想要继续提升自己的能力,那么就来关注golang学习网公众号吧!

版本声明
本文转载于:脚本之家 如有侵犯,请联系study_golang@163.com删除
详解Golang中字符串的使用详解Golang中字符串的使用
上一篇
详解Golang中字符串的使用
golang进程内存控制避免docker内oom
下一篇
golang进程内存控制避免docker内oom
评论列表
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • ljg-skills -
    ljg-skills
    ljg-skills 是李继刚开源的 AI 技能与提示词集合,面向大模型使用者整理了一批可复用的 prompt、角色设定和任务技能模板,适合用于学习提示词设计、搭建个人 AI 工作流和沉淀团队常用智能体能力。
    86次使用
  • MELO音乐 - AI 音乐生成平台,支持多模态创作能力
    MELO音乐
    MELO音乐是一站式AI视频与音乐制作助手,对标suno, udio的高品质体验。提供伴奏生成、原创写词、无损导出、哼唱识曲、混音变声等全套音频与短视频编辑工具。无论是流行Kpop、电音说唱、民谣古风、摇滚儿歌还是商用轻音乐,MELO为你免费谱曲,轻松做同款!
    107次使用
  • UniScribe - AI 免费在线音视频转文字平台
    UniScribe
    UniScribe 是一款 AI 音视频转文字与内容整理工具,支持上传音频、视频文件或粘贴 YouTube 链接,自动生成转写文本、摘要、思维导图和关键问题,并支持多格式导出,适合会议记录、课程学习、访谈整理和内容创作复盘。
    99次使用
  • 剧云 - 免费 AI 智能中文剧本创作平台
    剧云
    剧云是专业中文剧本创作平台,安全稳定运行十余年,集成AI编剧、剧本医生审核、人物小传、剧情关系图、大纲编写、多人协作、Word导入导出、版权管控功能,数据安全防护,轻松高效创作剧本。
    243次使用
  • 万象有声 - AI 一站式有声内容创作平台
    万象有声
    万象有声,一个专为有声创作者打造的新一代智能有声内容创作平台。平台提供专业的智能拆章、智能画本编辑、AI配音、AI生成音效、后期制作、智能对轨、智能审听等有声创作全流程工具,可以帮助创作者高效、低成本创作出引人入胜的有声作品。立即体验,让有声书制作更简单!
    249次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码