<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請(qǐng)求流程分析(后篇)——標(biāo)頭壓縮

          共 3763字,需瀏覽 8分鐘

           ·

          2020-09-21 14:21

          閱讀建議

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

          1. Go中的HTTP請(qǐng)求之——HTTP1.1請(qǐng)求流程分析

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

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

          回顧

          在前篇(*http2ClientConn).roundTrip方法中提到了寫(xiě)入請(qǐng)求header,而在寫(xiě)入請(qǐng)求header之前需要先編碼(源碼見(jiàn)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對(duì)讀取到的數(shù)據(jù)幀解碼(源碼見(jiàn)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)頭壓縮分析(完整例子見(jiàn)https://github.com/Isites/go-coder/blob/master/http2/hpack-example/main.go)。

          開(kāi)門(mén)見(jiàn)山

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

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

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

          本篇不對(duì)哈夫曼編碼做過(guò)多的闡述,主要對(duì)雙端共同維護(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
          }

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

          ents:entries的縮寫(xiě),代表著當(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ǔ)對(duì)應(yīng)的唯一Id。

          對(duì)字段的含義有所了解后,接下來(lái)對(duì)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開(kāi)始的,也就是說(shuō)刪除Header時(shí)遵循先進(jìn)先出的原則。刪除Header的步驟如下:

          1. 刪除byNamebyNameValue的映射。

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

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

          4. 改變ents的長(zhǎng)度。

          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:通過(guò)當(dāng)前表中的唯一Id計(jì)算出當(dāng)前表對(duì)應(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開(kāi)始,且Index為1時(shí)對(duì)應(yīng)的元素為t.ents[0]

          動(dòng)態(tài)表:?Index也從1開(kāi)始,但是Index為1時(shí)對(duì)應(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參見(jiàn)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索引是怎么樣的呢?

          帶著這樣的疑問(wèn)我們看看下面的結(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通過(guò)HPACK索引標(biāo)識(shí)唯一的Header元素。

          HPACK編碼

          眾所周知HTTP2的標(biāo)頭壓縮能夠減少很多數(shù)據(jù)的傳輸,接下來(lái)我們通過(guò)下面的例子,對(duì)比一下編碼前后的數(shù)據(jù)大?。?/p>

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

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

          帶著一種 “臥槽,牛逼!”的心情開(kāi)始對(duì)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,:pathaccept-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的代碼)。

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

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

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

          (*Encoder).searchTable:從HPACK索引列表中搜索Header,并返回對(duì)應(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表示法

          此表示法對(duì)應(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位用來(lái)表示值。

          如果索引大于uint64((1 << n) - 1)時(shí),需要使用多個(gè)字節(jié)來(lái)存儲(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表示法

          此種表示法對(duì)應(yīng)兩種情況:一,Header的Name有匹配索引;二,Header的Name和Value均無(wú)匹配索引。這兩種情況分別對(duì)應(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的索引。

          通過(guò)appendVarInt對(duì)索引編碼后,下面我們看看appendHpackString函數(shù)如何對(duì)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í)分為兩種情況:

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

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

          在這里需要注意的是存儲(chǔ)Value長(zhǎng)度時(shí)僅用了字節(jié)的低7位,最高位為1表示存儲(chǔ)的內(nèi)容為哈夫曼編碼,最高位為0表示存儲(chǔ)的內(nèi)容為原Value。

          appendNewName:?編碼Name和Value均無(wú)匹配的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é)束后通過(guò)appendHpackString先后對(duì)Header的Name和Value進(jìn)行編碼。

          HPACK解碼

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

          // 此處省略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

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

          而這解碼的過(guò)程筆者將從hdec.Write方法開(kāi)始分析,逐步揭開(kāi)它的神秘面紗。

           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的過(guò)程中發(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表示法對(duì)應(yīng)的解碼方法為d.parseFieldIndexed

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

          索引Header表示法

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

          2. 通過(guò)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ù)來(lái)計(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ù)邏輯相對(duì)應(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)表長(zhǎng)度時(shí),直接從靜態(tài)表中獲取Header數(shù)據(jù)。

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

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

          通過(guò)(*Decoder).parseFieldLiteral解碼時(shí),需要考慮兩種情況。一、Header的Name有索引。二、Header的Name和Value均無(wú)索引。這兩種情況下,該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ù)對(duì)應(yīng)),然后通過(guò)readVarInt讀取數(shù)據(jù)的長(zhǎng)度并賦值給strLen。

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

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

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

          下面筆者最后驗(yàn)證一下已經(jīng)編解碼過(guò)后的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)編碼過(guò)后的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)編碼過(guò)的Header也僅需一個(gè)字節(jié)。

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

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

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

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

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

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

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

          參考:

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



          推薦閱讀



          學(xué)習(xí)交流 Go 語(yǔ)言,掃碼回復(fù)「進(jìn)群」即可


          站長(zhǎng) polarisxu

          自己的原創(chuàng)文章

          不限于 Go 技術(shù)

          職場(chǎng)和創(chuàng)業(yè)經(jīng)驗(yàn)


          Go語(yǔ)言中文網(wǎng)

          每天為你

          分享 Go 知識(shí)

          Go愛(ài)好者值得關(guān)注



          瀏覽 45
          點(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>
                  国产 日韩 欧美视频在线 | 操美少妇母亲aV | 美日韩一级黄色片 | 九九九成人网 | 国产成人精品一级毛片 |