目录
HPACK索引列表
静态表
动态表
静态表和动态表构成完整的HPACK索引列表
HPACK编码
索引Header表示法
增加动态表Header表示法
HPACK解码
验证&总结
首页 后端开发 Golang Go发起HTTP2.0请求流程分析(后篇)——标头压缩

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

Jul 21, 2023 pm 04:30 PM
go http请求

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

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

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

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

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 = 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&#39;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/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系列读完的读者,真诚的希望各位读者能够有所收获。

以上是Go发起HTTP2.0请求流程分析(后篇)——标头压缩的详细内容。更多信息请关注PHP中文网其他相关文章!

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

Video Face Swap

Video Face Swap

使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

热门话题

Java教程
1663
14
CakePHP 教程
1419
52
Laravel 教程
1313
25
PHP教程
1264
29
C# 教程
1237
24
使用 Golang 为 HTTP 请求设置查询参数 使用 Golang 为 HTTP 请求设置查询参数 Jun 02, 2024 pm 03:27 PM

在Go中为HTTP请求设置查询参数,可以使用http.Request.URL.Query().Set()方法,该方法接受查询参数名称和值作为参数。具体步骤包括:创建一个新的HTTP请求。使用Query().Set()方法设置查询参数。对请求进行编码。执行请求。获取查询参数的值(可选)。删除查询参数(可选)。

Go WebSocket 消息如何发送? Go WebSocket 消息如何发送? Jun 03, 2024 pm 04:53 PM

在Go中,可以使用gorilla/websocket包发送WebSocket消息。具体步骤:建立WebSocket连接。发送文本消息:调用WriteMessage(websocket.TextMessage,[]byte("消息"))。发送二进制消息:调用WriteMessage(websocket.BinaryMessage,[]byte{1,2,3})。

Golang 与 Go 语言的区别 Golang 与 Go 语言的区别 May 31, 2024 pm 08:10 PM

Go和Go语言是不同的实体,具有不同的特性。Go(又称Golang)以其并发性、编译速度快、内存管理和跨平台优点而闻名。Go语言的缺点包括生态系统不如其他语言丰富、语法更严格以及缺乏动态类型。

如何在 Go 中使用正则表达式匹配时间戳? 如何在 Go 中使用正则表达式匹配时间戳? Jun 02, 2024 am 09:00 AM

在Go中,可以使用正则表达式匹配时间戳:编译正则表达式字符串,例如用于匹配ISO8601时间戳的表达式:^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-][0-9]{2}:[0-9]{2})$。使用regexp.MatchString函数检查字符串是否与正则表达式匹配。

Golang 技术性能优化中如何避免内存泄漏? Golang 技术性能优化中如何避免内存泄漏? Jun 04, 2024 pm 12:27 PM

内存泄漏会导致Go程序内存不断增加,可通过:关闭不再使用的资源,如文件、网络连接和数据库连接。使用弱引用防止内存泄漏,当对象不再被强引用时将其作为垃圾回收目标。利用go协程,协程栈内存会在退出时自动释放,避免内存泄漏。

Golang 函数接收 map 参数时的注意事项 Golang 函数接收 map 参数时的注意事项 Jun 04, 2024 am 10:31 AM

在Go中传递map给函数时,默认会创建副本,对副本的修改不影响原map。如果需要修改原始map,可通过指针传递。空map需小心处理,因为技术上是nil指针,传递空map给期望非空map的函数会发生错误。

如何使用 Golang 的错误包装器? 如何使用 Golang 的错误包装器? Jun 03, 2024 pm 04:08 PM

在Golang中,错误包装器允许你在原始错误上追加上下文信息,从而创建新错误。这可用于统一不同库或组件抛出的错误类型,简化调试和错误处理。步骤如下:使用errors.Wrap函数将原有错误包装成新错误。新错误包含原始错误的上下文信息。使用fmt.Printf输出包装后的错误,提供更多上下文和可操作性。在处理不同类型的错误时,使用errors.Wrap函数统一错误类型。

如何在 Go 中创建优先级 Goroutine? 如何在 Go 中创建优先级 Goroutine? Jun 04, 2024 pm 12:41 PM

在Go语言中创建优先级Goroutine有两步:注册自定义Goroutine创建函数(步骤1)并指定优先级值(步骤2)。这样,您可以创建不同优先级的Goroutine,优化资源分配并提高执行效率。

See all articles