Go发起HTTP2.0请求流程分析(后篇)——标头压缩

本文深入探讨Go中HTTP2.0请求的标头压缩,重点分析HPACK索引列表、编码与解码过程。通过实例解析静态表和动态表的运作,揭示如何利用HPACK减少数据传输,提升效率。
摘要由CSDN通过智能技术生成

阅读建议

这是HTTP2.0系列的最后一篇,笔者推荐阅读顺序如下:

  1. Go中的HTTP请求之——HTTP1.1请求流程分析
  2. Go发起HTTP2.0请求流程分析(前篇)
  3. Go发起HTTP2.0请求流程分析(中篇)——数据帧&流控制

回顾

在前篇(*http2ClientConn).roundTrip方法中提到了写入请求header,而在写入请求header之前需要先编码(源码见github.com/golang/go/b…

在中篇(*http2ClientConn).readLoop方法中提到了ReadFrame()方法,该方法会读取数据帧,如果是http2FrameHeaders数据帧,会调用(*http2Framer).readMetaFrame对读取到的数据帧解码(源码见github.com/golang/go/b…

因为标头压缩具有较高的独立性,所以笔者基于上面提到的编/解码部分的源码自己实现了一个可以独立运行的小例子。本篇将基于自己实现的例子进行标头压缩分析(完整例子见github.com/Isites/go-c…

开门见山

HTTP2使用 HPACK 压缩格式压缩请求和响应标头元数据,这种格式采用下面两种技术压缩:

  1. 通过静态哈夫曼代码对传输的标头字段进行编码,从而减小数据传输的大小。
  2. 单个连接中,client和server共同维护一个相同的标头字段索引列表(笔者称为HPACK索引列表),此列表在之后的传输中用作编解码的参考。

本篇不对哈夫曼编码做过多的阐述,主要对双端共同维护的索引列表进行分析。

HPACK 压缩上下文包含一个静态表和一个动态表:静态表在规范中定义,并提供了一个包含所有连接都可能使用的常用 HTTP 标头字段的列表;动态表最初为空,将根据在特定连接内交换的值进行更新。

HPACK索引列表

认识静/动态表需要先认识headerFieldTable结构体,动态表和静态表都是基于它实现的。

type headerFieldTable struct {
    // As in hpack, unique ids  are 1-based. The unique id for ents[k] is k + evictCount + 1.
    ents       []HeaderField
    evictCount uint64

    // byName maps a HeaderField name to the unique id of the newest entry with the same name.
    byName map[string]uint64

    // byNameValue maps a HeaderField name/value pair to the unique id of the newest
    byNameValue map[pairNameValue]uint64
}
复制代码

下面将对上述的字段分别进行描述:

ents:entries的缩写,代表着当前已经索引的Header数据。在headerFieldTable中,每一个Header都有一个唯一的Id,以ents[k]为例,该唯一id的计算方式是k + evictCount + 1

evictCount:已经从ents中删除的条目数。

byName:存储具有相同Name的Header的唯一Id,最新Header的Name会覆盖老的唯一Id。

byNameValue:以Header的Name和Value为key存储对应的唯一Id。

对字段的含义有所了解后,接下来对headerFieldTable几个比较重要的行为进行描述。

(*headerFieldTable).addEntry:添加Header实体到表中

func (t *headerFieldTable) addEntry(f HeaderField) {
    id := uint64(t.len()) + t.evictCount + 1
    t.byName[f.Name] = id
    t.byNameValue[pairNameValue{f.Name, f.Value}] = id
    t.ents = append(t.ents, f)
}
复制代码

首先,计算出Header在headerFieldTable中的唯一Id,并将其分别存入byNamebyNameValue中。最后,将Header存入ents

因为使用了append函数,这意味着ents[0]存储的是存活最久的Header。

(*headerFieldTable).evictOldest:从表中删除指定个数的Header实体

func (t *headerFieldTable) evictOldest(n int) {
    if n > t.len() {
        panic(fmt.Sprintf("evictOldest(%v) on table with %v entries", n, t.len()))
    }
    for k := 0; k < n; k++ {
        f := t.ents[k]
        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)
        }
    }
    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]
    if t.evictCount+uint64(n) < t.evictCount {
        panic("evictCount overflow")
    }
    t.evictCount += uint64(n)
}
复制代码

第一个for循环的下标是从0开始的,也就是说删除Header时遵循先进先出的原则。删除Header的步骤如下:

  1. 删除byNamebyNameValue的映射。
  2. 将第n位及其之后的Header前移。
  3. 将倒数的n个Header置空,以方便垃圾回收。
  4. 改变ents的长度。
  5. 增加evictCount的数量。

(*headerFieldTable).search:从当前表中搜索指定Header并返回在当前表中的Index(此处的Index和切片中的下标含义是不一样的)

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
}
复制代码

如果Header的Name和Value均匹配,则返回

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值