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

目录
  • Hpack 是啥
  • HPACK 原理
    • 如何编码
    • 举个编码
  • HPACK 实现
  • 遇到的坑

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 < n; k++ {
                f := t.ents[k]
                // 根据 index 算出在 map 中的 id
                id := t.evictCount + uint64(k) + 1
                // 双重校验,如果校验通过就删除表项
                if t.byName[f.Name] == id {
                        delete(t.byName, f.Name)
                }
                if p := (pairNameValue{f.Name, f.Value}); t.byNameValue[p] == id {
                        delete(t.byNameValue, p)
                }
        }
        // 用后 n 个表项覆盖前面的表项实现逐出
        copy(t.ents, t.ents[n:])
        for k := t.len() - n; k < t.len(); k++ {
                t.ents[k] = HeaderField{} // so strings can be garbage collected
        }
        t.ents = t.ents[:t.len()-n]
        // 逐出数量 +n
        // 表项迁移带来的索引减小会通过 evictCount 的增加补回来,所以 id 并不会变
        t.evictCount += uint64(n)
}
// 在表项中寻找,如果没有匹配的 i 就是 0.如果 kv 都匹配上了就返回 index, true;
// 如果只有 k 匹配上了就返回 index, false。
func (t *headerFieldTable) search(f HeaderField) (i uint64, nameValueMatch bool) {
        if !f.Sensitive {
                if id := t.byNameValue[pairNameValue{f.Name, f.Value}]; id != 0 {
                        return t.idToIndex(id), true
                }
        }
        if id := t.byName[f.Name]; id != 0 {
                return t.idToIndex(id), false
        }
        return 0, false
}
func (t *headerFieldTable) idToIndex(id uint64) uint64 {
        // 校验。不在这里 panic,下面根据 index 索引的时候,slice 也会 panic
        if id <= t.evictCount {
                panic(fmt.Sprintf("id (%v) <= evictCount (%v)", id, t.evictCount))
        }
        // 将 id 转换为 slice 中的 index
        k := id - t.evictCount - 1 // convert id to an index t.ents[k]
        // 如果是动态表,需要减去静态表的长度
        if t != staticTable {
                return uint64(t.len()) - k // dynamic table
        }
        return k + 1
}

其他部分的实现就很简单了,基本上就是照着上面的流程写就可以了。其中有一个解析当前 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

以上就是go HTTP2 的头部压缩算法hpack实现详解的详细内容,更多关于go HTTP2 头部压缩算法hpack的资料请关注我们其它相关文章!

(0)

相关推荐

  • Golang gRPC HTTP协议转换示例

    gRPC HTTP协议转换 正当有这个需求的时候,就看到了这个实现姿势.源自coreos的一篇博客,转载到了grpc官方博客gRPC with REST and Open APIs. etcd3改用grpc后为了兼容原来的api,同时要提供http/json方式的API,为了满足这个需求,要么开发两套API,要么实现一种转换机制,他们选择了后者,而我们选择跟随他们的脚步. 他们实现了一个协议转换的网关,对应github上的项目grpc-gateway,这个网关负责接收客户端请求,然后决定直接转发

  • Go类型安全的HTTP请求示例详解

    目录 前言 Go 原生写法 httpc 实现 更多能力 前言 对 Gopher 来说,虽然我们基本都是在写代码让别人来请求,但是有时候,我们也需要去请求第三方提供的 RESTful 接口,这个时候,我们才能感受到前端同学拼接 HTTP 请求参数的痛苦. 比如,我们要发起类似这样一个请求,看起来很简单,实际写起来还是比较繁琐的. POST /articles/5/update?device=ios HTTP/1.1 Host: go-zero.dev Authorization: Bearer <

  • Go语言resty http包调用jenkins api实例

    目录 前言 Resty特色 演示例子 简单get请求 增强get请求 灵活post请求 多文件上传 文件下载 实战例子 构造一个jenkins客户端 获取jenkins job信息 无参构建job 查看构建日志 job开关(启用禁用job) 小结 前言 在前两篇文章中都使用HttpRequest这个http包来做api的请求 然后github上面还有一个更有名,星星更多,社区也更活跃的http包Resty 最近11月3号又发布了一个新版本,项目参与者多达75个,标星有5.2k Resty特色 支

  • Go Grpc Gateway兼容HTTP协议文档自动生成网关

    目录 前言 一,grpc-gateway介绍 二,grpc-gateway环境准备 二,编写grpc-gateway服务 四,使用gateway生成swagger文档 五,性能对比 http -> go -> grpc -> go http -> go -> http -> grpc_gateway -> grpc -> go 六,总结 前言 调用,让客户端可以更具自身情况自由选择,服务端工作只需要做一份呢?还别说真还有一个准备好的轮子那就是今天的主角<

  • Go http请求排队处理实战示例

    目录 一.http请求的顺序处理方式 二.http请求的异步处理方式--排队处理 工作单元 队列 消费者协程 完整代码 总结 一.http请求的顺序处理方式 在高并发场景下,为了降低系统压力,都会使用一种让请求排队处理的机制.本文就介绍在Go中是如何实现的. 首先,我们看下正常的请求处理逻辑. 客户端发送请求,web server接收请求,然后就是处理请求,最后响应给客户端这样一个顺序的逻辑.如下图所示: 代码实现如下: package main import ( "fmt" &quo

  • Go gRPC进阶教程gRPC转换HTTP

    目录 前言 gRPC转成HTTP 编写和编译proto 服务端代码修改 使用postman测试 生成swagger文档 把swagger-ui转成Go代码,备用 对外提供swagger-ui 在swagger中配置bearer token 验证测试 总结 前言 我们通常把RPC用作内部通信,而使用Restful Api进行外部通信.为了避免写两套应用,我们使用grpc-gateway把gRPC转成HTTP.服务接收到HTTP请求后,grpc-gateway把它转成gRPC进行处理,然后以JSON

  • go HTTP2 的头部压缩算法hpack实现详解

    目录 Hpack 是啥 HPACK 原理 如何编码 举个编码 HPACK 实现 遇到的坑 Hpack 是啥 Hpack 是 HTTP2 的头部压缩算法.在 HTTP1 中,每次传输都会有大量的 Header 携带,我们可以拿一个实际的请求来看,如图一: 图一:请求 header 这里面 Header 很多是请求共性的,比如 method: POST,就是 post 请求的 header,那每个 POST 请求都会携带这个 header:以及同一个页面里可能有很多请求需要带上相同 header,比

  • Android RecyclerView添加头部和底部实例详解

    Android RecyclerView添加头部和底部实例详解 如果只是想添加头部,可是使用GitHub里面这个项目,它可以为LinearLayoutManager,GridLayoutManager ,StaggeredGridLayoutManager布局的RecyclerView添加header.使用起来也十分简单: 只需将RecyclerViewHeader布局放在RecyclerView的上层. <FrameLayout android:layout_width="match_p

  • 对Pycharm创建py文件时自定义头部模板的方法详解

    如下所示: # -*- coding: utf-8 -*- """ ------------------------------------------------- File Name: ${NAME} Description : Author : ${USER} date: ${DATE} ------------------------------------------------- Change Activity: ${DATE}: ----------------

  • Vue创建头部组件示例代码详解

    Vue.js 组件 组件(Component)是 Vue.js 最强大的功能之一. 组件可以扩展 HTML 元素,封装可重用的代码. 具体代码如下所示: <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title

  • Nginx开启Brotli压缩算法实现过程详解

    前言 在web应用中,为了节省流量,降低传输数据大小,提高传输效率,常用的压缩方式一般都是gzip,今天我们来介绍另外一种更高效的压缩方式brotli. Brotli 是基于LZ77算法的一个现代变体.霍夫曼编码和二阶上下文建模.Google软件工程师在2015年9月发布了包含通用无损数据压缩的Brotli增强版本,特别侧重于HTTP压缩. 注意:使用算法的前提是启用了 https,因为 http 请求中 request header 里的 Accept-Encoding: gzip, defl

  • scrapy头部修改的方法详解

    被Scrapy自动添加的头部 在没有任何配置的情况下,scrapy会对请求默认加上一些头部信息 Scrapy会通过配置文件中的USER_AGENT配置,自动为头部添加User-Agent,这条配置会被任何包含User-Agent的配置覆盖 当请求经过下载器后,会被自动添加头部Accept-Encoding: gzip,deflate, 会被任意包含Accept-Encoding的头部配置覆盖 配置settings.py文件中默认的头部 #DEFAULT_REQUEST_HEADERS = { #

  • Spring 配置文件XML头部文件模板实例详解

    普通spring配置文件模板: <?xml version="1.0" encoding="UTF-8" ?> <beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/beans" xsi:schemaLocation="http://www.s

  • 详解WordPress开发中get_header()获取头部函数的用法

    函数意义详解 从当前主题调用header.php文件.是不是很简单?好吧,如果你是新手的话这里要提醒一下,这里的get和get_children().get_category中的get略有不同之处. get_header函数声明(定义) 之前写文章很少会写到函数定义的代码,后来自己翻看的时候发现这个习惯不太好,所以决定,只要篇幅允许,就会把函数主题贴出来,方便自己翻看. get_header 函数,声明(定义)的位置,是在 wp=include/general-template.php 文件的第

  • Java垃圾回收之标记压缩算法详解

    之前写过的一篇Java垃圾回收之标记清除算法详解 ,这个算法有个缺点就是造成内存碎片,存在不连续的空间,这样会导致申请较大空间的时候,又需要进行垃圾回收.下面介绍一下标记压缩算法,可以避免内存碎片. 空白部分是不连续的. 概述 这个算法的标记清除阶段,跟Java垃圾回收之标记清除算法详解  中的是一样的,而对于压缩阶段,它的工作就是移动所有的可达对象到堆内存的同一个区域中,使他们紧凑的排列在一起,从而将所有非可达对象释放出来的空闲内存都集中在一起,通过这样的方式来达到减少内存碎片的目的.如下图:

  • HTTP中header头部信息详解

    HTTP Request的Header信息 1.HTTP请求方式 如下表: GET 向Web服务器请求一个文件 POST 向Web服务器发送数据让Web服务器进行处理 PUT 向Web服务器发送数据并存储在Web服务器内部 HEAD 检查一个对象是否存在 DELETE 从Web服务器上删除一个文件 CONNECT 对通道提供支持 TRACE 跟踪到服务器的路径 OPTIONS 查询Web服务器的性能 说明: 主要使用到"GET"和"POST". 实例: POST /

随机推荐