Go 發起 HTTP2.0 請求流程分析 (後篇)——標頭壓縮
來自公眾號:新世界雜貨鋪
閱讀建議
這是 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 壓縮格式壓縮請求和響應標頭後設資料,這種格式採用下面兩種技術壓縮:
- 通過靜態哈夫曼程式碼對傳輸的標頭欄位進行編碼,從而減小資料傳輸的大小。
- 單個連線中,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/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...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/web/fundamentals/performance/http2?hl=zh-cn
- 加微信實戰群請加微信(註明:實戰群):gocnio
相關文章
- go http請求流程分析GoHTTP
- 如何壓縮 HTTP 請求正文HTTP
- 原始碼分析Retrofit請求流程原始碼
- vue 發起get請求和post請求Vue
- 使用go對NTP發起請求獲取當前時間Go
- POST發起下載請求
- 微信小程式-發起請求微信小程式
- CCHttpClient發起https請求HTTPclient
- 發起GPRS撥號請求
- OkHttp 原始碼分析(一)—— 請求流程HTTP原始碼
- SpringMVC請求流程原始碼分析SpringMVC原始碼
- axios原始碼分析——請求流程iOS原始碼
- MDN新增“HTTP有條件請求”標頭HTTP
- HTTP協議如何發起請求HTTP協議
- golang使用fasthttp 發起http請求GolangASTHTTP
- TCP 請求頭TCP
- http請求頭HTTP
- 請求基本流程
- 有趣的請求引數/請求頭
- 當使用者發起資料請求後,ADAMoracle如何操作Oracle
- ajax中設定請求頭和自定義請求頭
- axios.pacth () 請求如何發起?iOS
- nodejs HTTPS發起POST請求NodeJSHTTP
- mark java發起http請求的方式JavaHTTP
- windows中使用cmd發起http請求WindowsHTTP
- ThinkPHP6 原始碼分析之請求流程PHP原始碼
- go http請求GoHTTP
- ASP.NET使用HttpModule壓縮並刪除空白Html請求ASP.NETHTTPHTML
- 跟我一起動手實現Tomcat(三):解析Request請求引數、請求頭、cookieTomcatCookie
- Tomcat 第四篇:請求處理流程(上)Tomcat
- ASIHttpRequest:建立佇列、下載請求、斷點續傳、解壓縮HTTP佇列斷點
- Nginx轉發導致請求頭丟失Nginx
- HTTP請求頭與響應頭HTTP
- 前端獲取不到後端新增的請求頭資訊前端後端
- 使用者發起資料請求到得到喂價資料的整套流程中
- 原始碼分析Gateway請求轉發原始碼Gateway
- Spring MVC 處理一個請求的流程分析SpringMVC
- DRF之請求執行流程和APIView原始碼分析APIView原始碼