共计 15297 个字符,预计需要花费 39 分钟才能阅读完成。
来自公众号:新世界杂货铺
浏览倡议
这是 HTTP2.0 系列的最初一篇,笔者举荐浏览程序如下:
- Go 中的 HTTP 申请之——HTTP1.1 申请流程剖析
- Go 发动 HTTP2.0 申请流程剖析(前篇)
- 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 压缩格局压缩申请和响应标头元数据,这种格局采纳上面两种技术压缩:
- 通过动态哈夫曼代码对传输的标头字段进行编码,从而减小数据传输的大小。
- 单个连贯中,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,并将其别离存入 byName
和byNameValue
中。最初,将 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 的步骤如下:
- 删除
byName
和byNameValue
的映射。 - 将第 n 位及其之后的 Header 前移。
- 将倒数的 n 个 Header 置空,以不便垃圾回收。
- 扭转 ents 的长度。
- 减少
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.byName
和t.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...Hn
和 H1...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
,:path
,accept-encoding
和user-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.SetMaxDynamicTableSizeLimit
和e.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)
时,须要应用多个字节来存储索引的值,步骤如下:
- 第一个字节的最低 n 位全为 1。
- 索引 i 减去 uint64((1 << n) – 1)后,每次取低 7 位或上
0b10000000
,而后 i 右移 7 位并和 128 进行比拟,判断是否进入下一次循环。 - 循环完结后将剩下的 i 值间接放入 buf 中。
用这种办法示意 Header 时,仅须要大量字节就能够示意一个残缺的 Header 头字段,最好的状况是一个字节就能够示意一个 Header 字段。
减少动静表 Header 表示法
此种表示法对应两种状况:一,Header 的 Name 有匹配索引;二,Header 的 Name 和 Value 均无匹配索引。这两种状况别离对应的处理函数为 appendIndexedName
和appendNewName
。这两种状况均会将 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})
}
上述办法次要有三个步骤:
- 通过
readVarInt
函数读取 HPACK 索引。 - 通过
d.at
办法找到索引列表中实在的 Header 数据。 - 将 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 索引。
- 第一次循环时 m 为 0,b 的低 7 位加上
(1<<uint64(n))-1
并赋值给 i - 后续循环时 m 按 7 递增,b 的低 7 位会逐渐填充到 i 的高位上。
- 当 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/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 数据时能够分为两种状况。
- Header 在 HPACK 索引列表外面,能够不必传输实在的 Header 数据仅需传输 HPACK 索引从而达到标头压缩的目标。
- Header 不在 HPACK 索引列表外面,对大多数 Header 而言也仅需传输 Header 的 Value 以及 Name 的 HPACK 索引,从而缩小 Header 数据的传输。同时,在发送和接管这样的 Header 数据时会更新各自的 HPACK 索引列表,以保障下一个申请传输的 Header 数据尽可能的少。
最初,由衷的感激将 HTTP2.0 系列读完的读者,真挚的心愿各位读者可能有所播种。
如果大家有什么疑难能够在评论区谐和地探讨,笔者看到了也会及时回复,愿大家一起提高。
注:
- 写本文时,笔者所用 go 版本为: go1.14.2
- 索引 Header 表示法和减少动静表 Header 表示法均为笔者自主命名,次要便于读者了解。
参考:
https://developers.google.com…