阅读建议

这是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) + 1if 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索引是怎么样的呢?

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

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

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 = falseif 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] |= 0x80return 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 -= kfor ; 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 byteif 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 = truereturn  }  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 

通过最后一行的输出可以知道确确实实从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 = bufreturn 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 uint64for len(p) > 0 {     b := p[0]        p = p[1:]        i += uint64(b&127) << mif b&128 == 0 {return i, p, nil      }     m += 7if 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<并赋值给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.tablereturn 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 othersdefer 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 // try again:// header field "custom-header" = "custom-value"// 1 

由上面最后一行的输出可知,解码仅用了一个字节,即本例中编码一个已经编码过的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

您的点赞、转发和在看对笔者都是一种支持!

生命不息, 探索不止, 后续将持续更新有关于go的技术探索

创作不易, 求关注收藏二连.

internetreadfile读取数据长度为0_Go发起HTTP2.0请求流程分析(后篇)——标头压缩相关推荐

  1. internetreadfile读取数据长度为0_YOLOV3的TensorFlow2.0实现,支持在自己的数据集上训练...

    GitHub链接: calmisential/YOLOv3_TensorFlow2​github.com 我主要参考了yolov3的一个keras实现版本: qqwweee/keras-yolo3​g ...

  2. internetreadfile读取数据长度为0_Hadoop 读取数据

    MapReduce - 读取数据 通过InputFormat决定读取的数据的类型,然后拆分成一个个InputSplit,每个InputSplit对应一个Map处理,RecordReader读取Inpu ...

  3. internetreadfile读取数据长度为0_Datax3.0的安装和基本使用

    今天安装了下datax,正好有一些心得,感觉可以记录下来,避免下一次在踩坑. DataX 是阿里巴巴集团内被广泛使用的离线数据同步工具/平台,实现包括 MySQL.Oracle.SqlServer.P ...

  4. internetreadfile读取数据长度为0_【完结】TensorFlow2.0 快速上手手册

    大家好,这是专栏<TensorFlow2.0>的第五篇文章,我们对专栏<TensorFlow2.0>进行一个总结. 我们知道全新的TensorFlow2.0 Alpha已经于2 ...

  5. django通过ajax请求接口返回多条数据,并动态生成表格,请求表单后将表格数据并入库

    一.最近在做接口相关的开发,需求是这样的,通过一个接口所需要传递的参数,调用接口后,处理接口响应的参数,返回多条数据,并动态生成表格,请求表单后将表格的数据入库,下面是我改过的代码,跟实际代码有些出入 ...

  6. python 使用 httpx 发送http2.0 请求

    python 使用 httpx 发送http2.0 请求 摘要 安装 http/2 支持 客户端请求 更有效地利用网络资源 额外功能 同步 异步 复杂示例,APNS异步推送到多用户 http/1 支持 ...

  7. android http2.0请求,Android http HttpURLConnection

    /** * Http get 请求 * * @param urlPath * * */ private static String httpConnByGet(RequestUrl ru,Params ...

  8. 进一步封装axios并调用其读取数据(吐槽~在安卓9.0以下或者IOS10.X以下手机端H5页面不支持,在这两种情况下的系统只能使用ajax或者原生js请求后台数据)

    注意!!!(修改于2020年7月18日) 在安卓9.0以下或者IOS10.X以下手机端H5页面不支持,在这两种情况下的系统只能使用ajax或者原生js请求后台数据 报错截图如下 报错内容: {&quo ...

  9. python csv读取数据 去掉标题-Python读csv文件去掉一列后再写入新的文件实例

    用了两种方式解决该问题,都是网上现有的解决方案. 场景说明: 有一个数据文件,以文本方式保存,现在有三列user_id,plan_id,mobile_id.目标是得到新文件只有mobile_id,pl ...

最新文章

  1. Android-源代码分析
  2. java reduce.mdn_reduce高级用法
  3. html绘制头像原样教程,CSS实例教程:创意CSS3头像展示教程
  4. 无人驾驶方面牛人和实验室
  5. Tomcat Firewall JMX RMI
  6. spring-boot注解详解(三)
  7. initialize方法与load方法比较
  8. 汇编语言——第1次上机实验
  9. wamp php 安装redis,wampServer的php安装Redis 扩展
  10. FOUND MODULE 所在的表及刪除不啟作用的INCLUDE
  11. Java中涉及线程和并发相关的内容
  12. 内点法外点法matlab代码,分享:惩罚函数法(内点法、外点法)求解约束优化问题最优值...
  13. 头像怎么画,日系头像绘画教程
  14. 进不了PE,进PE黑屏或点阵屏怎么办
  15. [渝粤教育] 西南科技大学 律师实务 在线考试复习资料2021版(1)
  16. [k8s集群系列-09]Kubernetes 组件 Dashboard
  17. 深度相机原理和优势对比
  18. mac笔记本部分按键失灵
  19. Web前端期末大作业---响应式美女健身教练瑜伽馆网页设计(HTML+CSS+JavaScript+)实现
  20. 头条号nx配置文件mysql_后端开辟必备的MySQL日记文件知识点

热门文章

  1. 运维组如何管理服务器资源,运维服务管理体系方案全套.doc
  2. 顶岗实习周记java方向_前途虽远,扶摇可接 | 商务日语专业顶岗实习动员大会...
  3. ireport怎么套打_方向盘套你选对了吗?老司机告诉你该这样做|酷斯特玩车
  4. java中取系统时间_JAVA中获取当前系统时间(示例代码)
  5. maven仓库的安装与配置
  6. Eureke服务入门就这一篇就够了
  7. linux vim 单引号,单引号和双引号的区别、linux快捷键、zip压缩、lrzsz、vim常见问题...
  8. Catkin工作空间 (重点)
  9. python协同过滤调用包_简单的python协同过滤程序实例代码
  10. 获取页眉值vba_VBA抓取股票历史数据的整体表处理