24

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

 3 years ago
source link: https://studygolang.com/articles/30801
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

来自公众号:新世界杂货铺

阅读建议

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

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

回顾

在前篇 (*http2ClientConn).roundTrip 方法中提到了写入请求header,而在写入请求header之前需要先编码(源码见 https://github.com/golang/go/blob/master/src/net/http/h2_bundle.go#L7947 )。

在中篇 (*http2ClientConn).readLoop 方法中提到了 ReadFrame() 方法,该方法会读取数据帧,如果是 http2FrameHeaders 数据帧,会调用 (*http2Framer).readMetaFrame 对读取到的数据帧解码(源码见 https://github.com/golang/go/blob/master/src/net/http/h2_bundle.go#L2725 )。

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

开门见山

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均匹配,则返回当前表中的Index且 nameValueMatch 为true。

如果仅有Header的Name匹配,则返回当前表中的Index且 nameValueMatch 为false。

如果Header的Name和Value均不匹配,则返回0且 nameValueMatch 为false。

(*headerFieldTable).idToIndex:通过当前表中的唯一Id计算出当前表对应的Index

func (t *headerFieldTable) idToIndex(id uint64) uint64 {
    if id <= t.evictCount {
        panic(fmt.Sprintf("id (%v) <= evictCount (%v)", id, t.evictCount))
    }
    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
}

静态表: Index 从1开始,且Index为1时对应的元素为 t.ents[0]

动态表: Index 也从1开始,但是Index为1时对应的元素为 t.ents[t.len()-1]

静态表

静态表中包含了一些每个连接都可能使用到的Header。其实现如下:

var staticTable = newStaticTable()
func newStaticTable() *headerFieldTable {
    t := &headerFieldTable{}
    t.init()
    for _, e := range staticTableEntries[:] {
        t.addEntry(e)
    }
    return t
}
var staticTableEntries = [...]HeaderField{
    {Name: ":authority"},
    {Name: ":method", Value: "GET"},
    {Name: ":method", Value: "POST"},
  // 此处省略代码
    {Name: "www-authenticate"},
}

上面的 t.init 函数仅做初始化 t.byNamet.byNameValue 用。笔者在这里仅展示了部分预定义的Header,完整预定义Header参见 https://github.com/golang/go/blob/master/src/vendor/golang.org/x/net/http2/hpack/tables.go#L130

动态表

动态表结构体如下:

type dynamicTable struct {
    // http://http2.github.io/http2-spec/compression.html#rfc.section.2.3.2
    table          headerFieldTable
    size           uint32 // in bytes
    maxSize        uint32 // current maxSize
    allowedMaxSize uint32 // maxSize may go up to this, inclusive
}

动态表的实现是基于 headerFieldTable ,相比原先的基础功能增加了表的大小限制,其他功能保持不变。

静态表和动态表构成完整的HPACK索引列表

前面介绍了动/静态表中内部的Index和内部的唯一Id,而在一次连接中HPACK索引列表是由静态表和动态表一起构成,那此时在连接中的HPACK索引是怎么样的呢?

带着这样的疑问我们看看下面的结构:

VJZN7v3.jpg!mobile

image

上图中蓝色部分表示静态表,黄色部分表示动态表。

H1...HnH1...Hm 分别表示存储在静态表和动态表中的Header元素。

在HPACK索引中静态表部分的索引和静态表的内部索引保持一致,动态表部分的索引为动态表内部索引加上静态表索引的最大值。在一次连接中Client和Server通过HPACK索引标识唯一的Header元素。

HPACK编码

众所周知HTTP2的标头压缩能够减少很多数据的传输,接下来我们通过下面的例子,对比一下编码前后的数据大小:

var (
  buf     bytes.Buffer
  oriSize int
)
henc := hpack.NewEncoder(&buf)
headers := []hpack.HeaderField{
  {Name: ":authority", Value: "dss0.bdstatic.com"},
  {Name: ":method", Value: "GET"},
  {Name: ":path", Value: "/5aV1bjqh_Q23odCf/static/superman/img/topnav/[email protected]"},
  {Name: ":scheme", Value: "https"},
  {Name: "accept-encoding", Value: "gzip"},
  {Name: "user-agent", Value: "Go-http-client/2.0"},
  {Name: "custom-header", Value: "custom-value"},
}
for _, header := range headers {
  oriSize += len(header.Name) + len(header.Value)
  henc.WriteField(header)
}
fmt.Printf("ori size: %v, encoded size: %v\n", oriSize, buf.Len())
//输出为:ori size: 197, encoded size: 111

注:在 HTTP2 中,请求和响应标头字段的定义保持不变,仅有一些微小的差异:所有标头字段名称均为小写,请求行现在拆分成各个 :method:scheme:authority:path 伪标头字段。

在上面的例子中,我们看到原来为197字节的标头数据现在只有111字节,减少了近一半的数据量!

带着一种 “卧槽,牛逼!”的心情开始对 henc.WriteField 方法调试。

func (e *Encoder) WriteField(f HeaderField) error {
    e.buf = e.buf[:0]

    if e.tableSizeUpdate {
        e.tableSizeUpdate = false
        if e.minSize < e.dynTab.maxSize {
            e.buf = appendTableSize(e.buf, e.minSize)
        }
        e.minSize = uint32Max
        e.buf = appendTableSize(e.buf, e.dynTab.maxSize)
    }

    idx, nameValueMatch := e.searchTable(f)
    if nameValueMatch {
        e.buf = appendIndexed(e.buf, idx)
    } else {
        indexing := e.shouldIndex(f)
        if indexing {
            e.dynTab.add(f) // 加入动态表中
        }

        if idx == 0 {
            e.buf = appendNewName(e.buf, f, indexing)
        } else {
            e.buf = appendIndexedName(e.buf, f, idx, indexing)
        }
    }
    n, err := e.w.Write(e.buf)
    if err == nil && n != len(e.buf) {
        err = io.ErrShortWrite
    }
    return err
}

经调试发现,本例中 :authority:pathaccept-encodinguser-agent 走了 appendIndexedName 分支; :method:scheme 走了 appendIndexed 分支; custom-header 走了 appendNewName 分支。这三种分支总共代表了两种不同的编码方法。

由于本例中 f.Sensitive 默认值为false且Encoder给动态表的默认大小为4096,按照 e.shouldIndex 的逻辑本例中 indexing 一直为true(在笔者所使用的go1.14.2源码中,client端尚未发现有使 f.Sensitive 为true的代码)。

笔者对上面 e.tableSizeUpdate 相关的逻辑不提的原因是控制 e.tableSizeUpdate 的方法为 e.SetMaxDynamicTableSizeLimite.SetMaxDynamicTableSize ,而笔者在 (*http2Transport).newClientConn (此方法相关逻辑参见前篇)相关的源码中发现了这样的注释:

// TODO: SetMaxDynamicTableSize, SetMaxDynamicTableSizeLimit on
// henc in response to SETTINGS frames?

笔者看到这里的时候内心激动不已呀,产生了一种强烈的想贡献代码的欲望,奈何自己能力有限只能看着机会却抓不住呀,只好含恨埋头苦学(开个玩笑~,毕竟某位智者说过,写的越少BUG越少)。

(*Encoder).searchTable:从HPACK索引列表中搜索Header,并返回对应的索引。

func (e *Encoder) searchTable(f HeaderField) (i uint64, nameValueMatch bool) {
    i, nameValueMatch = staticTable.search(f)
    if nameValueMatch {
        return i, true
    }

    j, nameValueMatch := e.dynTab.table.search(f)
    if nameValueMatch || (i == 0 && j != 0) {
        return j + uint64(staticTable.len()), nameValueMatch
    }

    return i, false
}

搜索顺序为,先搜索静态表,如果静态表不匹配,则搜索动态表,最后返回。

索引Header表示法

此表示法对应的函数为 appendIndexed ,且该Header已经在索引列表中。

该函数将Header在HPACK索引列表中的索引编码,原先的Header最后仅用少量的几个字节就可以表示。

func appendIndexed(dst []byte, i uint64) []byte {
    first := len(dst)
    dst = appendVarInt(dst, 7, i)
    dst[first] |= 0x80
    return dst
}
func appendVarInt(dst []byte, n byte, i uint64) []byte {
    k := uint64((1 << n) - 1)
    if i < k {
        return append(dst, byte(i))
    }
    dst = append(dst, byte(k))
    i -= k
    for ; i >= 128; i >>= 7 {
        dst = append(dst, byte(0x80|(i&0x7f)))
    }
    return append(dst, byte(i))
}

appendIndexed 知,用索引头字段表示法时,第一个字节的格式必须是 0b1xxxxxxx ,即第0位必须为 1 ,低7位用来表示值。

如果索引大于 uint64((1 << n) - 1) 时,需要使用多个字节来存储索引的值,步骤如下:

0b10000000

用这种方法表示Header时,仅需要少量字节就可以表示一个完整的Header头字段,最好的情况是一个字节就可以表示一个Header字段。

增加动态表Header表示法

此种表示法对应两种情况:一,Header的Name有匹配索引;二,Header的Name和Value均无匹配索引。这两种情况分别对应的处理函数为 appendIndexedNameappendNewName 。这两种情况均会将Header添加到动态表中。

appendIndexedName:编码有Name匹配的Header字段。

func appendIndexedName(dst []byte, f HeaderField, i uint64, indexing bool) []byte {
    first := len(dst)
    var n byte
    if indexing {
        n = 6
    } else {
        n = 4
    }
    dst = appendVarInt(dst, n, i)
    dst[first] |= encodeTypeByte(indexing, f.Sensitive)
    return appendHpackString(dst, f.Value)
}

在这里我们先看看 encodeTypeByte 函数:

func encodeTypeByte(indexing, sensitive bool) byte {
    if sensitive {
        return 0x10
    }
    if indexing {
        return 0x40
    }
    return 0
}

前面提到本例中indexing一直为true,sensitive为false,所以encodeTypeByte的返回值一直为 0x40

此时回到appendIndexedName函数,我们知道增加动态表Header表示法的第一个字节格式必须是 0xb01xxxxxx ,即最高两位必须是 01 ,低6位用于表示Header中Name的索引。

通过 appendVarInt 对索引编码后,下面我们看看 appendHpackString 函数如何对Header的Value进行编码:

func appendHpackString(dst []byte, s string) []byte {
    huffmanLength := HuffmanEncodeLength(s)
    if huffmanLength < uint64(len(s)) {
        first := len(dst)
        dst = appendVarInt(dst, 7, huffmanLength)
        dst = AppendHuffmanString(dst, s)
        dst[first] |= 0x80
    } else {
        dst = appendVarInt(dst, 7, uint64(len(s)))
        dst = append(dst, s...)
    }
    return dst
}

appendHpackString 编码时分为两种情况:

哈夫曼编码后的长度小于原Value的长度时,先用 appendVarInt 将哈夫曼编码后的最终长度存入buf,然后再将真实的哈夫曼编码存入buf。

哈夫曼编码后的长度大于等于原Value的长度时,先用 appendVarInt 将原Value的长度存入buf,然后再将原Value存入buf。

在这里需要注意的是存储Value长度时仅用了字节的低7位,最高位为1表示存储的内容为哈夫曼编码,最高位为0表示存储的内容为原Value。

appendNewName:编码Name和Value均无匹配的Header字段。

func appendNewName(dst []byte, f HeaderField, indexing bool) []byte {
    dst = append(dst, encodeTypeByte(indexing, f.Sensitive))
    dst = appendHpackString(dst, f.Name)
    return appendHpackString(dst, f.Value)
}

前面提到 encodeTypeByte 的返回值为 0x40 ,所以我们此时编码的第一个字节为 0b01000000

第一个字节编码结束后通过 appendHpackString 先后对Header的Name和Value进行编码。

HPACK解码

前面理了一遍HPACK的编码过程,下面我们通过一个解码的例子来理一遍解码的过程。

// 此处省略HPACK编码中的编码例子
var (
  invalid    error
  sawRegular bool
  // 16 << 20 from fr.maxHeaderListSize() from
  remainSize uint32 = 16 << 20
)
hdec := hpack.NewDecoder(4096, nil)
// 16 << 20 from fr.maxHeaderStringLen() from fr.maxHeaderListSize()
hdec.SetMaxStringLength(int(remainSize))
hdec.SetEmitFunc(func(hf hpack.HeaderField) {
  if !httpguts.ValidHeaderFieldValue(hf.Value) {
    invalid = fmt.Errorf("invalid header field value %q", hf.Value)
  }
  isPseudo := strings.HasPrefix(hf.Name, ":")
  if isPseudo {
    if sawRegular {
      invalid = errors.New("pseudo header field after regular")
    }
  } else {
    sawRegular = true
    // if !http2validWireHeaderFieldName(hf.Name) {
    //  invliad = fmt.Sprintf("invalid header field name %q", hf.Name)
    // }
  }
  if invalid != nil {
    fmt.Println(invalid)
    hdec.SetEmitEnabled(false)
    return
  }
  size := hf.Size()
  if size > remainSize {
    hdec.SetEmitEnabled(false)
    // mh.Truncated = true
    return
  }
  remainSize -= size
  fmt.Printf("%+v\n", hf)
  // mh.Fields = append(mh.Fields, hf)
})
defer hdec.SetEmitFunc(func(hf hpack.HeaderField) {})
fmt.Println(hdec.Write(buf.Bytes()))
// 输出如下:
// ori size: 197, encoded size: 111
// header field ":authority" = "dss0.bdstatic.com"
// header field ":method" = "GET"
// header field ":path" = "/5aV1bjqh_Q23odCf/static/superman/img/topnav/[email protected]"
// header field ":scheme" = "https"
// header field "accept-encoding" = "gzip"
// header field "user-agent" = "Go-http-client/2.0"
// header field "custom-header" = "custom-value"
// 111 <nil>

通过最后一行的输出可以知道确确实实从111个字节中解码出了197个字节的原Header数据。

而这解码的过程笔者将从 hdec.Write 方法开始分析,逐步揭开它的神秘面纱。

func (d *Decoder) Write(p []byte) (n int, err error) {
   // 此处省略代码
    if d.saveBuf.Len() == 0 {
        d.buf = p
    } else {
        d.saveBuf.Write(p)
        d.buf = d.saveBuf.Bytes()
        d.saveBuf.Reset()
    }

    for len(d.buf) > 0 {
        err = d.parseHeaderFieldRepr()
        if err == errNeedMore {
            // 此处省略代码
            d.saveBuf.Write(d.buf)
            return len(p), nil
        }
        // 此处省略代码
    }
    return len(p), err
}

在笔者debug的过程中发现解码的核心逻辑主要在 d.parseHeaderFieldRepr 方法里。

func (d *Decoder) parseHeaderFieldRepr() error {
    b := d.buf[0]
    switch {
    case b&128 != 0:
        return d.parseFieldIndexed()
    case b&192 == 64:
        return d.parseFieldLiteral(6, indexedTrue)
    // 此处省略代码
    }
    return DecodingError{errors.New("invalid encoding")}
}

第一个字节与上128不为0只有一种情况,那就是b为 0b1xxxxxxx 格式的数据,综合前面的编码逻辑可以知道索引Header表示法对应的解码方法为 d.parseFieldIndexed

第一个字节与上192为64也只有一种情况,那就是b为 0b01xxxxxx 格式的数据,综合前面的编码逻辑可以知道增加动态表Header表示法对应的解码方法为 d.parseFieldLiteral

索引Header表示法

通过 (*Decoder).parseFieldIndexed 解码时,真实的Header数据已经在静态表或者动态表中了,只要通过HPACK索引找到对应的Header就解码成功了。

func (d *Decoder) parseFieldIndexed() error {
    buf := d.buf
    idx, buf, err := readVarInt(7, buf)
    if err != nil {
        return err
    }
    hf, ok := d.at(idx)
    if !ok {
        return DecodingError{InvalidIndexError(idx)}
    }
    d.buf = buf
    return d.callEmit(HeaderField{Name: hf.Name, Value: hf.Value})
}

上述方法主要有三个步骤:

  1. 通过 readVarInt 函数读取HPACK索引。
  2. 通过 d.at 方法找到索引列表中真实的Header数据。
  3. 将Header传递给最上层。 d.CallEmit 最终会调用 hdec.SetEmitFunc 设置的闭包,从而将Header传递给最上层。

readVarInt:读取HPACK索引

func readVarInt(n byte, p []byte) (i uint64, remain []byte, err error) {
    if n < 1 || n > 8 {
        panic("bad n")
    }
    if len(p) == 0 {
        return 0, p, errNeedMore
    }
    i = uint64(p[0])
    if n < 8 {
        i &= (1 << uint64(n)) - 1
    }
    if i < (1<<uint64(n))-1 {
        return i, p[1:], nil
    }

    origP := p
    p = p[1:]
    var m uint64
    for len(p) > 0 {
        b := p[0]
        p = p[1:]
        i += uint64(b&127) << m
        if b&128 == 0 {
            return i, p, nil
        }
        m += 7
        if m >= 63 { // TODO: proper overflow check. making this up.
            return 0, origP, errVarintOverflow
        }
    }
    return 0, origP, errNeedMore
}

由上述的readVarInt函数知,当第一个字节的低n为不全为1时,则低n为代表真实的HPACK索引,可以直接返回。

当第一个字节的低n为全为1时,需要读取更多的字节数来计算真正的HPACK索引。

  1. 第一次循环时m为0,b的低7位加上 (1<<uint64(n))-1 并赋值给i

  2. 后续循环时m按7递增,b的低7位会逐步填充到i的高位上。

  3. 当b小于128时结速循环,此时已经读取完整的HPACK索引。

readVarInt 函数逻辑和前面 appendVarInt 函数逻辑相对应。

(*Decoder).at:根据HPACK的索引获取真实的Header数据。

func (d *Decoder) at(i uint64) (hf HeaderField, ok bool) {
    if i == 0 {
        return
    }
    if i <= uint64(staticTable.len()) {
        return staticTable.ents[i-1], true
    }
    if i > uint64(d.maxTableIndex()) {
        return
    }
    dt := d.dynTab.table
    return dt.ents[dt.len()-(int(i)-staticTable.len())], true
}

索引小于静态表长度时,直接从静态表中获取Header数据。

索引长度大于静态表时,根据前面介绍的 HPACK索引列表 ,可以通过 dt.len()-(int(i)-staticTable.len()) 计算出i在动态表 ents 的真实下标,从而获取Header数据。

增加动态表Header表示法

通过 (*Decoder).parseFieldLiteral 解码时,需要考虑两种情况。一、Header的Name有索引。二、Header的Name和Value均无索引。这两种情况下,该Header都不存在于动态表中。

下面分步骤分析 (*Decoder).parseFieldLiteral 方法。

1、读取buf中的HPACK索引。

nameIdx, buf, err := readVarInt(n, buf)

2、 如果索引不为0,则从HPACK索引列表中获取Header的Name。

ihf, ok := d.at(nameIdx)
if !ok {
  return DecodingError{InvalidIndexError(nameIdx)}
}
hf.Name = ihf.Name

3、如果索引为0,则从buf中读取Header的Name。

hf.Name, buf, err = d.readString(buf, wantStr)

4、从buf中读取Header的Value,并将完整的Header添加到动态表中。

hf.Value, buf, err = d.readString(buf, wantStr)
if err != nil {
  return err
}
d.buf = buf
if it.indexed() {
  d.dynTab.add(hf)
}

(*Decoder).readString:从编码的字节数据中读取真实的Header数据。

func (d *Decoder) readString(p []byte, wantStr bool) (s string, remain []byte, err error) {
    if len(p) == 0 {
        return "", p, errNeedMore
    }
    isHuff := p[0]&128 != 0
    strLen, p, err := readVarInt(7, p)
    // 省略校验逻辑
    if !isHuff {
        if wantStr {
            s = string(p[:strLen])
        }
        return s, p[strLen:], nil
    }

    if wantStr {
        buf := bufPool.Get().(*bytes.Buffer)
        buf.Reset() // don't trust others
        defer bufPool.Put(buf)
        if err := huffmanDecode(buf, d.maxStrLen, p[:strLen]); err != nil {
            buf.Reset()
            return "", nil, err
        }
        s = buf.String()
        buf.Reset() // be nice to GC
    }
    return s, p[strLen:], nil
}

首先判断字节数据是否是哈夫曼编码(和前面的 appendHpackString 函数对应),然后通过 readVarInt 读取数据的长度并赋值给 strLen

如果不是哈夫曼编码,则直接返回 strLen 长度的数据。如果是哈夫曼编码,读取 strLen 长度的数据,并用哈夫曼算法解码后再返回。

验证&总结

在前面我们已经了解了HPACK索引列表,以及基于HPACK索引列表的编/解码流程。

下面笔者最后验证一下已经编解码过后的Header,再次编解码时的大小。

// 此处省略前面HAPACK编码和HPACK解码的demo
// try again
fmt.Println("try again: ")
buf.Reset()
henc.WriteField(hpack.HeaderField{Name: "custom-header", Value: "custom-value"}) // 编码已经编码过后的Header
fmt.Println(hdec.Write(buf.Bytes())) // 解码
// 输出:
// ori size: 197, encoded size: 111
// header field ":authority" = "dss0.bdstatic.com"
// header field ":method" = "GET"
// header field ":path" = "/5aV1bjqh_Q23odCf/static/superman/img/topnav/[email protected]"
// header field ":scheme" = "https"
// header field "accept-encoding" = "gzip"
// header field "user-agent" = "Go-http-client/2.0"
// header field "custom-header" = "custom-value"
// 111 <nil>
// try again:
// header field "custom-header" = "custom-value"
// 1 <nil>

由上面最后一行的输出可知,解码仅用了一个字节,即本例中编码一个已经编码过的Header也仅需一个字节。

综上:在一个连接上,client和server维护一个相同的HPACK索引列表,多个请求在发送和接收Header数据时可以分为两种情况。

  1. Header在HPACK索引列表里面,可以不用传输真实的Header数据仅需传输HPACK索引从而达到标头压缩的目的。
  2. Header不在HPACK索引列表里面,对大多数Header而言也仅需传输Header的Value以及Name的HPACK索引,从而减少Header数据的传输。同时,在发送和接收这样的Header数据时会更新各自的HPACK索引列表,以保证下一个请求传输的Header数据尽可能的少。

最后,由衷的感谢将HTTP2.0系列读完的读者,真诚的希望各位读者能够有所收获。

如果大家有什么疑问可以在评论区和谐地讨论,笔者看到了也会及时回复,愿大家一起进步。

注:

  1. 写本文时, 笔者所用go版本为: go1.14.2
  2. 索引Header表示法和增加动态表Header表示法均为笔者自主命名,主要便于读者理解。

参考:

https://developers.google.com/web/fundamentals/performance/http2?hl=zh-cn

有疑问加站长微信联系

iiUfA3j.png!mobile

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK