> 백엔드 개발 > Golang > Go는 HTTP2.0 요청 프로세스 분석을 시작합니다(2부) - 헤더 압축

Go는 HTTP2.0 요청 프로세스 분석을 시작합니다(2부) - 헤더 압축

풀어 주다: 2023-07-21 16:30:54
앞으로
1221명이 탐색했습니다.
HTTP2는 HPACK 압축 형식을 사용하여 요청 및 응답 헤더 메타데이터를 압축합니다. 이 형식은 다음 두 가지 기술을 사용하여 압축됩니다.
  1. 정적 허프만 코드를 통해 전송된 헤더 필드를 인코딩하여 데이터 크기를 줄입니다. 전송.

  2. 단일 연결에서 클라이언트와 서버는 동일한 헤더 필드 인덱스 목록(저자는 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: 현재 색인화된 헤더를 나타내는 항목의 약어 데이터. headerFieldTable에서 각 헤더에는 ents[k] 예를 들어 고유 ID 계산 방법 isk + evictCount + 1. ents:entries的缩写,代表着当前已经索引的Header数据。在headerFieldTable中,每一个Header都有一个唯一的Id,以ents[k]为例,该唯一id的计算方式是k + evictCount + 1

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

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

byNameValue

evictCount : 항목에서 삭제된 항목 수입니다.

byName : 동일한 이름을 가진 헤더의 고유 ID를 저장합니다. 최신 헤더 이름이 이전 고유 ID를 덮어씁니다.

byNameValue : 헤더의 이름과 값을 키로 사용하여 해당 고유 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 요청 프로세스 분석을 시작합니다(2부) - 헤더 압축

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

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 요청 프로세스 분석을 시작합니다(2부) - 헤더 압축의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

관련 라벨:
원천:Go语言进阶学习
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿