<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Go發(fā)起HTTP2.0請求流程分析(后篇)——標(biāo)頭壓縮

          共 3729字,需瀏覽 8分鐘

           ·

          2022-04-20 20:35

          點(diǎn)擊上方“Go語言進(jìn)階學(xué)習(xí)”,進(jìn)行關(guān)注

          回復(fù)“Go語言”即可獲贈(zèng)從入門到進(jìn)階共10本電子書

          為我一揮手,如聽萬壑松。

          閱讀建議

          這是HTTP2.0系列的最后一篇,筆者推薦閱讀順序如下:

          1. Go中的HTTP請求之——HTTP1.1請求流程分析

          2. Go發(fā)起HTTP2.0請求流程分析(前篇)

          3. Go發(fā)起HTTP2.0請求流程分析(中篇)——數(shù)據(jù)幀&流控制

          回顧

          在前篇(*http2ClientConn).roundTrip方法中提到了寫入請求header,而在寫入請求header之前需要先編碼(源碼見https://github.com/golang/go/blob/master/src/net/http/h2_bundle.go#L7947)。

          在中篇(*http2ClientConn).readLoop方法中提到了ReadFrame()方法,該方法會(huì)讀取數(shù)據(jù)幀,如果是http2FrameHeaders數(shù)據(jù)幀,會(huì)調(diào)用(*http2Framer).readMetaFrame對讀取到的數(shù)據(jù)幀解碼(源碼見https://github.com/golang/go/blob/master/src/net/http/h2_bundle.go#L2725)。

          因?yàn)闃?biāo)頭壓縮具有較高的獨(dú)立性,所以筆者基于上面提到的編/解碼部分的源碼自己實(shí)現(xiàn)了一個(gè)可以獨(dú)立運(yùn)行的小例子。本篇將基于自己實(shí)現(xiàn)的例子進(jìn)行標(biāo)頭壓縮分析(完整例子見https://github.com/Isites/go-coder/blob/master/http2/hpack-example/main.go)。

          開門見山

          HTTP2使用 HPACK 壓縮格式壓縮請求和響應(yīng)標(biāo)頭元數(shù)據(jù),這種格式采用下面兩種技術(shù)壓縮:

          1. 通過靜態(tài)哈夫曼代碼對傳輸?shù)臉?biāo)頭字段進(jìn)行編碼,從而減小數(shù)據(jù)傳輸?shù)拇笮 ?/p>

          2. 單個(gè)連接中,client和server共同維護(hù)一個(gè)相同的標(biāo)頭字段索引列表(筆者稱為HPACK索引列表),此列表在之后的傳輸中用作編解碼的參考。

          本篇不對哈夫曼編碼做過多的闡述,主要對雙端共同維護(hù)的索引列表進(jìn)行分析。

          HPACK 壓縮上下文包含一個(gè)靜態(tài)表和一個(gè)動(dòng)態(tài)表:靜態(tài)表在規(guī)范中定義,并提供了一個(gè)包含所有連接都可能使用的常用 HTTP 標(biāo)頭字段的列表;動(dòng)態(tài)表最初為空,將根據(jù)在特定連接內(nèi)交換的值進(jìn)行更新。

          HPACK索引列表

          認(rèn)識(shí)靜/動(dòng)態(tài)表需要先認(rèn)識(shí)headerFieldTable結(jié)構(gòu)體,動(dòng)態(tài)表和靜態(tài)表都是基于它實(shí)現(xiàn)的。

          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
          }

          下面將對上述的字段分別進(jìn)行描述:

          ents:entries的縮寫,代表著當(dāng)前已經(jīng)索引的Header數(shù)據(jù)。在headerFieldTable中,每一個(gè)Header都有一個(gè)唯一的Id,以ents[k]為例,該唯一id的計(jì)算方式是k + evictCount + 1。

          evictCount:已經(jīng)從ents中刪除的條目數(shù)。

          byName:存儲(chǔ)具有相同Name的Header的唯一Id,最新Header的Name會(huì)覆蓋老的唯一Id。

          byNameValue:以Header的Name和Value為key存儲(chǔ)對應(yīng)的唯一Id。

          對字段的含義有所了解后,接下來對headerFieldTable幾個(gè)比較重要的行為進(jìn)行描述。

          (*headerFieldTable).addEntry:添加Header實(shí)體到表中

          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)
          }

          首先,計(jì)算出Header在headerFieldTable中的唯一Id,并將其分別存入byNamebyNameValue中。最后,將Header存入ents

          因?yàn)槭褂昧薬ppend函數(shù),這意味著ents[0]存儲(chǔ)的是存活最久的Header。

          (*headerFieldTable).evictOldest:從表中刪除指定個(gè)數(shù)的Header實(shí)體

          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)
          }

          第一個(gè)for循環(huán)的下標(biāo)是從0開始的,也就是說刪除Header時(shí)遵循先進(jìn)先出的原則。刪除Header的步驟如下:

          1. 刪除byNamebyNameValue的映射。

          2. 將第n位及其之后的Header前移。

          3. 將倒數(shù)的n個(gè)Header置空,以方便垃圾回收。

          4. 改變ents的長度。

          5. 增加evictCount的數(shù)量。

          (*headerFieldTable).search:從當(dāng)前表中搜索指定Header并返回在當(dāng)前表中的Index(此處的Index和切片中的下標(biāo)含義是不一樣的)

          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均匹配,則返回當(dāng)前表中的Index且nameValueMatch為true。

          如果僅有Header的Name匹配,則返回當(dāng)前表中的Index且nameValueMatch為false。

          如果Header的Name和Value均不匹配,則返回0且nameValueMatch為false。

          (*headerFieldTable).idToIndex:通過當(dāng)前表中的唯一Id計(jì)算出當(dāng)前表對應(yīng)的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
          }

          靜態(tài)表:Index從1開始,且Index為1時(shí)對應(yīng)的元素為t.ents[0]

          動(dòng)態(tài)表:?Index也從1開始,但是Index為1時(shí)對應(yīng)的元素為t.ents[t.len()-1]。

          靜態(tài)表

          靜態(tài)表中包含了一些每個(gè)連接都可能使用到的Header。其實(shí)現(xiàn)如下:

          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函數(shù)僅做初始化t.byNamet.byNameValue用。筆者在這里僅展示了部分預(yù)定義的Header,完整預(yù)定義Header參見https://github.com/golang/go/blob/master/src/vendor/golang.org/x/net/http2/hpack/tables.go#L130。

          動(dòng)態(tài)表

          動(dòng)態(tài)表結(jié)構(gòu)體如下:

          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
          }

          動(dòng)態(tài)表的實(shí)現(xiàn)是基于headerFieldTable,相比原先的基礎(chǔ)功能增加了表的大小限制,其他功能保持不變。

          靜態(tài)表和動(dòng)態(tài)表構(gòu)成完整的HPACK索引列表

          前面介紹了動(dòng)/靜態(tài)表中內(nèi)部的Index和內(nèi)部的唯一Id,而在一次連接中HPACK索引列表是由靜態(tài)表和動(dòng)態(tài)表一起構(gòu)成,那此時(shí)在連接中的HPACK索引是怎么樣的呢?

          帶著這樣的疑問我們看看下面的結(jié)構(gòu):

          上圖中藍(lán)色部分表示靜態(tài)表,黃色部分表示動(dòng)態(tài)表。

          H1...HnH1...Hm分別表示存儲(chǔ)在靜態(tài)表和動(dòng)態(tài)表中的Header元素。

          在HPACK索引中靜態(tài)表部分的索引和靜態(tài)表的內(nèi)部索引保持一致,動(dòng)態(tài)表部分的索引為動(dòng)態(tài)表內(nèi)部索引加上靜態(tài)表索引的最大值。在一次連接中Client和Server通過HPACK索引標(biāo)識(shí)唯一的Header元素。

          HPACK編碼

          眾所周知HTTP2的標(biāo)頭壓縮能夠減少很多數(shù)據(jù)的傳輸,接下來我們通過下面的例子,對比一下編碼前后的數(shù)據(jù)大小:

          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/[email protected]"},
          {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 中,請求和響應(yīng)標(biāo)頭字段的定義保持不變,僅有一些微小的差異:所有標(biāo)頭字段名稱均為小寫,請求行現(xiàn)在拆分成各個(gè)?:method:scheme、:authority?和?:path?偽標(biāo)頭字段。

          在上面的例子中,我們看到原來為197字節(jié)的標(biāo)頭數(shù)據(jù)現(xiàn)在只有111字節(jié),減少了近一半的數(shù)據(jù)量!

          帶著一種 “臥槽,牛逼!”的心情開始對henc.WriteField方法調(diào)試。

          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) // 加入動(dòng)態(tài)表中
          }

          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
          }

          經(jīng)調(diào)試發(fā)現(xiàn),本例中:authority:path,accept-encodinguser-agent走了appendIndexedName分支;:method:scheme走了appendIndexed分支;custom-header走了appendNewName分支。這三種分支總共代表了兩種不同的編碼方法。

          由于本例中f.Sensitive默認(rèn)值為false且Encoder給動(dòng)態(tài)表的默認(rèn)大小為4096,按照e.shouldIndex的邏輯本例中indexing一直為true(在筆者所使用的go1.14.2源碼中,client端尚未發(fā)現(xiàn)有使f.Sensitive為true的代碼)。

          筆者對上面e.tableSizeUpdate相關(guān)的邏輯不提的原因是控制e.tableSizeUpdate的方法為e.SetMaxDynamicTableSizeLimite.SetMaxDynamicTableSize,而筆者在(*http2Transport).newClientConn(此方法相關(guān)邏輯參見前篇)相關(guān)的源碼中發(fā)現(xiàn)了這樣的注釋:

          // TODO: SetMaxDynamicTableSize, SetMaxDynamicTableSizeLimit on
          // henc in response to SETTINGS frames?

          筆者看到這里的時(shí)候內(nèi)心激動(dòng)不已呀,產(chǎn)生了一種強(qiáng)烈的想貢獻(xiàn)代碼的欲望,奈何自己能力有限只能看著機(jī)會(huì)卻抓不住呀,只好含恨埋頭苦學(xué)(開個(gè)玩笑~,畢竟某位智者說過,寫的越少BUG越少??)。

          (*Encoder).searchTable:從HPACK索引列表中搜索Header,并返回對應(yīng)的索引。

          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
          }

          搜索順序?yàn)?,先搜索靜態(tài)表,如果靜態(tài)表不匹配,則搜索動(dòng)態(tài)表,最后返回。

          索引Header表示法

          此表示法對應(yīng)的函數(shù)為appendIndexed,且該Header已經(jīng)在索引列表中。

          該函數(shù)將Header在HPACK索引列表中的索引編碼,原先的Header最后僅用少量的幾個(gè)字節(jié)就可以表示。

          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知,用索引頭字段表示法時(shí),第一個(gè)字節(jié)的格式必須是0b1xxxxxxx,即第0位必須為1,低7位用來表示值。

          如果索引大于uint64((1 << n) - 1)時(shí),需要使用多個(gè)字節(jié)來存儲(chǔ)索引的值,步驟如下:

          1. 第一個(gè)字節(jié)的最低n位全為1。

          2. 索引i減去uint64((1 << n) - 1)后,每次取低7位或上0b10000000, 然后i右移7位并和128進(jìn)行比較,判斷是否進(jìn)入下一次循環(huán)。

          3. 循環(huán)結(jié)束后將剩下的i值直接放入buf中。

          用這種方法表示Header時(shí),僅需要少量字節(jié)就可以表示一個(gè)完整的Header頭字段,最好的情況是一個(gè)字節(jié)就可以表示一個(gè)Header字段。

          增加動(dòng)態(tài)表Header表示法

          此種表示法對應(yīng)兩種情況:一,Header的Name有匹配索引;二,Header的Name和Value均無匹配索引。這兩種情況分別對應(yīng)的處理函數(shù)為appendIndexedNameappendNewName。這兩種情況均會(huì)將Header添加到動(dòng)態(tài)表中。

          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函數(shù):

          func encodeTypeByte(indexing, sensitive bool) byte {
          if sensitive {
          return 0x10
          }
          if indexing {
          return 0x40
          }
          return 0
          }

          前面提到本例中indexing一直為true,sensitive為false,所以encodeTypeByte的返回值一直為0x40。

          此時(shí)回到appendIndexedName函數(shù),我們知道增加動(dòng)態(tài)表Header表示法的第一個(gè)字節(jié)格式必須是0xb01xxxxxx,即最高兩位必須是01,低6位用于表示Header中Name的索引。

          通過appendVarInt對索引編碼后,下面我們看看appendHpackString函數(shù)如何對Header的Value進(jìn)行編碼:

          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編碼時(shí)分為兩種情況:

          哈夫曼編碼后的長度小于原Value的長度時(shí),先用appendVarInt將哈夫曼編碼后的最終長度存入buf,然后再將真實(shí)的哈夫曼編碼存入buf。

          哈夫曼編碼后的長度大于等于原Value的長度時(shí),先用appendVarInt將原Value的長度存入buf,然后再將原Value存入buf。

          在這里需要注意的是存儲(chǔ)Value長度時(shí)僅用了字節(jié)的低7位,最高位為1表示存儲(chǔ)的內(nèi)容為哈夫曼編碼,最高位為0表示存儲(chǔ)的內(nèi)容為原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,所以我們此時(shí)編碼的第一個(gè)字節(jié)為0b01000000

          第一個(gè)字節(jié)編碼結(jié)束后通過appendHpackString先后對Header的Name和Value進(jìn)行編碼。

          HPACK解碼

          前面理了一遍HPACK的編碼過程,下面我們通過一個(gè)解碼的例子來理一遍解碼的過程。

          // 此處省略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/[email protected]"
          // 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

          通過最后一行的輸出可以知道確確實(shí)實(shí)從111個(gè)字節(jié)中解碼出了197個(gè)字節(jié)的原Header數(shù)據(jù)。

          而這解碼的過程筆者將從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的過程中發(fā)現(xiàn)解碼的核心邏輯主要在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")}
          }

          第一個(gè)字節(jié)與上128不為0只有一種情況,那就是b為0b1xxxxxxx格式的數(shù)據(jù),綜合前面的編碼邏輯可以知道索引Header表示法對應(yīng)的解碼方法為d.parseFieldIndexed。

          第一個(gè)字節(jié)與上192為64也只有一種情況,那就是b為0b01xxxxxx格式的數(shù)據(jù),綜合前面的編碼邏輯可以知道增加動(dòng)態(tài)表Header表示法對應(yīng)的解碼方法為d.parseFieldLiteral

          索引Header表示法

          通過(*Decoder).parseFieldIndexed解碼時(shí),真實(shí)的Header數(shù)據(jù)已經(jīng)在靜態(tài)表或者動(dòng)態(tài)表中了,只要通過HPACK索引找到對應(yīng)的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})
          }

          上述方法主要有三個(gè)步驟:

          1. 通過readVarInt函數(shù)讀取HPACK索引。

          2. 通過d.at方法找到索引列表中真實(shí)的Header數(shù)據(jù)。

          3. 將Header傳遞給最上層。d.CallEmit最終會(huì)調(diào)用hdec.SetEmitFunc設(shè)置的閉包,從而將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函數(shù)知,當(dāng)?shù)谝粋€(gè)字節(jié)的低n為不全為1時(shí),則低n為代表真實(shí)的HPACK索引,可以直接返回。

          當(dāng)?shù)谝粋€(gè)字節(jié)的低n為全為1時(shí),需要讀取更多的字節(jié)數(shù)來計(jì)算真正的HPACK索引。

          1. 第一次循環(huán)時(shí)m為0,b的低7位加上(1<并賦值給i

          2. 后續(xù)循環(huán)時(shí)m按7遞增,b的低7位會(huì)逐步填充到i的高位上。

          3. 當(dāng)b小于128時(shí)結(jié)速循環(huán),此時(shí)已經(jīng)讀取完整的HPACK索引。

          readVarInt函數(shù)邏輯和前面appendVarInt函數(shù)邏輯相對應(yīng)。

          (*Decoder).at:根據(jù)HPACK的索引獲取真實(shí)的Header數(shù)據(jù)。

          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
          }

          索引小于靜態(tài)表長度時(shí),直接從靜態(tài)表中獲取Header數(shù)據(jù)。

          索引長度大于靜態(tài)表時(shí),根據(jù)前面介紹的HPACK索引列表,可以通過dt.len()-(int(i)-staticTable.len())計(jì)算出i在動(dòng)態(tài)表ents的真實(shí)下標(biāo),從而獲取Header數(shù)據(jù)。

          增加動(dòng)態(tài)表Header表示法

          通過(*Decoder).parseFieldLiteral解碼時(shí),需要考慮兩種情況。一、Header的Name有索引。二、Header的Name和Value均無索引。這兩種情況下,該Header都不存在于動(dòng)態(tài)表中。

          下面分步驟分析(*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添加到動(dòng)態(tài)表中。

          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:?從編碼的字節(jié)數(shù)據(jù)中讀取真實(shí)的Header數(shù)據(jù)。

          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)
          // 省略校驗(yàn)邏輯
          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
          }

          首先判斷字節(jié)數(shù)據(jù)是否是哈夫曼編碼(和前面的appendHpackString函數(shù)對應(yīng)),然后通過readVarInt讀取數(shù)據(jù)的長度并賦值給strLen

          如果不是哈夫曼編碼,則直接返回strLen長度的數(shù)據(jù)。如果是哈夫曼編碼,讀取strLen長度的數(shù)據(jù),并用哈夫曼算法解碼后再返回。

          驗(yàn)證&總結(jié)

          在前面我們已經(jīng)了解了HPACK索引列表,以及基于HPACK索引列表的編/解碼流程。

          下面筆者最后驗(yàn)證一下已經(jīng)編解碼過后的Header,再次編解碼時(shí)的大小。

          // 此處省略前面HAPACK編碼和HPACK解碼的demo
          // try again
          fmt.Println("try again: ")
          buf.Reset()
          henc.WriteField(hpack.HeaderField{Name: "custom-header", Value: "custom-value"}) // 編碼已經(jīng)編碼過后的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/[email protected]"
          // 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

          由上面最后一行的輸出可知,解碼僅用了一個(gè)字節(jié),即本例中編碼一個(gè)已經(jīng)編碼過的Header也僅需一個(gè)字節(jié)。

          綜上:在一個(gè)連接上,client和server維護(hù)一個(gè)相同的HPACK索引列表,多個(gè)請求在發(fā)送和接收Header數(shù)據(jù)時(shí)可以分為兩種情況。

          1. Header在HPACK索引列表里面,可以不用傳輸真實(shí)的Header數(shù)據(jù)僅需傳輸HPACK索引從而達(dá)到標(biāo)頭壓縮的目的。

          2. Header不在HPACK索引列表里面,對大多數(shù)Header而言也僅需傳輸Header的Value以及Name的HPACK索引,從而減少Header數(shù)據(jù)的傳輸。同時(shí),在發(fā)送和接收這樣的Header數(shù)據(jù)時(shí)會(huì)更新各自的HPACK索引列表,以保證下一個(gè)請求傳輸?shù)腍eader數(shù)據(jù)盡可能的少。

          最后,由衷的感謝將HTTP2.0系列讀完的讀者,真誠的希望各位讀者能夠有所收獲。

          如果大家有什么疑問可以在讀者討論里和諧地討論,筆者看到了也會(huì)及時(shí)回復(fù),愿大家一起進(jìn)步。

          1. 寫本文時(shí), 筆者所用go版本為: go1.14.2

          2. 索引Header表示法和增加動(dòng)態(tài)表Header表示法均為筆者自主命名,主要便于讀者理解。

          參考:

          https://developers.google.com/web/fundamentals/performance/http2?hl=zh-cn

          -------------------?End?-------------------

          往期精彩文章推薦:

          歡迎大家點(diǎn)贊,留言,轉(zhuǎn)發(fā),轉(zhuǎn)載,感謝大家的相伴與支持

          想加入Go學(xué)習(xí)群請?jiān)诤笈_(tái)回復(fù)【入群

          萬水千山總是情,點(diǎn)個(gè)【在看】行不行

          瀏覽 14
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  99精品无码视频 | 亚洲A片一区二区三区电影网 | 成人三级网站在线观看 | 亚洲无码大片 | 免费手机在线看A片 |