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

浏览倡议

这是HTTP2.0系列的最初一篇,笔者举荐浏览程序如下:

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

回顾

在前篇(*http2ClientConn).roundTrip办法中提到了写入申请header,而在写入申请header之前须要先编码(源码见https://github.com/golang/go/...)。

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

因为标头压缩具备较高的独立性,所以笔者基于下面提到的编/解码局部的源码本人实现了一个能够独立运行的小例子。本篇将基于本人实现的例子进行标头压缩剖析(残缺例子见https://github.com/Isites/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/...。

动静表

动静表构造体如下:

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索引是怎么样的呢?

带着这样的疑难咱们看看上面的构造:

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

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/baiduyun@2x-e0be79e69e.png"},  {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)时,须要应用多个字节来存储索引的值,步骤如下:

  1. 第一个字节的最低n位全为1。
  2. 索引i减去uint64((1 << n) - 1)后,每次取低7位或上0b10000000, 而后i右移7位并和128进行比拟,判断是否进入下一次循环。
  3. 循环完结后将剩下的i值间接放入buf中。

用这种办法示意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/baiduyun@2x-e0be79e69e.png"// 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 = bufif 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 againfmt.Println("try again: ")buf.Reset()henc.WriteField(hpack.HeaderField{Name: "custom-header", Value: "custom-value"}) // 编码曾经编码过后的Headerfmt.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/baiduyun@2x-e0be79e69e.png"// 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...