From the public account: New World Grocery store

Read the advice

This is the final article in the HTTP2.0 series, and I recommend reading it in the following order:

  1. HTTP Requests in Go – HTTP1.1 Request Flow Analysis
  2. Go Initiate HTTP2.0 Request Flow Analysis (Previous Article)
  3. Go Initiates HTTP2.0 Request Flow Analysis (Part) – Data Frames & Flow Control

review

The previous article (*http2ClientConn).roundtrip method mentioned writing request headers, but before writing request headers you need to encode them (source: github.com/golang/go/b…

The ReadFrame() method mentioned in the (*http2ClientConn).readloop method reads data frames. If it is an http2FrameHeaders data frame, It calls (*http2Framer).readmetaFrame to decode the data frame (see github.com/golang/go/b…

Because header compression has a high degree of independence, the author has implemented a small example that can run independently based on the source code of the codec/decode section mentioned above. This article will analyze header compression based on an example of my own implementation (see github.com/Isites/go-c for a complete example).

Cut to the chase

HTTP2 compresses request and response header metadata using the HPACK compression format, which is compressed using two techniques:

  1. The transmission header fields are encoded by static Huffman code to reduce the size of the data transmission.
  2. In a single connection, the client and server jointly maintain the same header field index list (referred to by the authors as the HPACK index list), which is used as a codec reference in subsequent transmissions.

This paper does not elaborate on Huffman coding too much, but mainly analyzes the index list jointly maintained by both ends.

The HPACK compression context contains a static table and a dynamic table: the static table is defined in the specification and provides a list of common HTTP header fields that all connections may use; The dynamic table is initially empty and will be updated based on the values exchanged within a particular connection.

HPACK index list

Understanding static/dynamic tables requires understanding the headerFieldTable structure, on which both dynamic and static tables are implemented.

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
}
Copy the code

The above fields are described below:

Ents: Abbreviation for Entries, which stands for currently indexed Header data. In headerFieldTable, each Header has a unique Id, such as ents[k], which is calculated as k + evictCount + 1.

EvictCount: The number of items that have been removed from ents.

ByName: Stores the unique Id of the Header with the same Name. The Name of the latest Header overrides the old unique Id.

ByNameValue: stores the unique Id corresponding to the Header Name and Value as the key.

Now that you understand the meaning of the fields, I’ll describe some of the more important behaviors of headerFieldTable.

(*headerFieldTable). AddEntry: Adds Header entities to the table

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)
}
Copy the code

First, calculate the Header’s unique Id in the headerFieldTable and store it in byName and byNameValue, respectively. Finally, save the Header to ents.

Because the append function is used, this means ents[0] stores the oldest headers.

(*headerFieldTable). EvictOldest: Removes a specified number of Header entities from the table

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(a); 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)
}
Copy the code

The first for loop starts with a zero subscript, which means that the Header is removed on a first-in, first-out basis. To delete the Header, perform the following steps:

  1. deletebyNameandbyNameValueThe mapping.
  2. Move the NTH bit and the headers after it forward.
  3. Empty the reciprocal n headers to facilitate garbage collection.
  4. Change the length of ents.
  5. increaseevictCountThe number of.

(*headerFieldTable).search: Searches for the specified Header from the current table and returns the Index in the current table.

func (t *headerFieldTable) search(f HeaderField) (i uint64, nameValueMatch bool) {
	if! f.Sensitive {ifid := t.byNameValue[pairNameValue{f.Name, f.Value}]; id ! =0 {
			return t.idToIndex(id), true}}ifid := t.byName[f.Name]; id ! =0 {
		return t.idToIndex(id), false
	}
	return 0.false
}
Copy the code

If the Header Name and Value match, the Index in the current table is returned and nameValueMatch is true.

If only the Header’s Name matches, the Index in the current table is returned with nameValueMatch false.

If the Header Name and Value do not match, 0 is returned and nameValueMatch is false.

(*headerFieldTable).idtoIndex: Calculates the Index of the current table from the unique Id of the current table

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]
	ift ! = staticTable {return uint64(t.len()) - k // dynamic table
	}
	return k + 1
}
Copy the code

Static table: Index starts from 1, and the corresponding element when Index is 1 is t.ents[0].

Dynamic table: Index also starts at 1, but when Index is 1 the corresponding element is t.ents[t.len()-1].

Static table

The static table contains some headers that may be used by each connection. Its implementation is as follows:

var staticTable = newStaticTable()
func newStaticTable(a) *headerFieldTable {
	t := &headerFieldTable{}
	t.init()
	for _, e := range staticTableEntries[:] {
		t.addEntry(e)
	}
	return t
}
varstaticTableEntries = [...] HeaderField{ {Name:":authority"},
	{Name: ":method", Value: "GET"},
	{Name: ":method", Value: "POST"},
  // Omit code here
	{Name: "www-authenticate"}},Copy the code

The t.it function above is only used to initialize t.byname and T.bynamevalue. I only show some of the predefined headers here. For full predefined headers, see github.com/golang/go/b…

Dynamic table

The dynamic table structure is as follows:

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
}
Copy the code

The dynamic table implementation is based on headerFieldTable, which increases the size limit of the table compared to the original basic functionality, while remaining unchanged.

Static and dynamic tables make up the complete HPACK index list

HPACK Index is a static table and a dynamic table. It is a static table and a dynamic table. It is a static table.

With this in mind let’s look at the following structure:

In the figure above, static tables are represented in blue and dynamic tables in yellow.

H1… Hn and H1… Hm represents Header elements stored in static and dynamic tables, respectively.

The static table part of the HPACK index is the same as the internal index of the static table. The dynamic table part of the index is the dynamic table internal index plus the maximum value of the static table index. Client and Server identify unique Header elements through the HPACK index in a connection.

HPACK coding

HTTP2 header compression can reduce the transmission of a lot of data. Let’s use the following example to compare the size of the data before and after encoding:

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
Copy the code

Note: In HTTP2, the definition of the request and response header fields remains the same, with minor differences: all header field names are lowercase, and the request line is now split into: Method, : Scheme, : Authority, and: PATH pseudo-header fields.

In the above example, we see that the header data that was 197 bytes is now only 111 bytes, reducing the amount of data by nearly half!

With a kind of “Oh, my God! The mood starts debugging the henc.WriteField method.

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) // Add to dynamic table
		}

		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
}
Copy the code

In this example, the authority, :path, accept-Encoding, and user-agent branches run appendIndexedName. The: Method and: Scheme branches off appendIndexed; The custom-header goes through the appendNewName branch. Together, these three branches represent two different coding approaches.

Since f. sensitive defaults to false and Encoder gives the dynamic table a default size of 4096, all indexing in this case is true, following the logic of E. shouldindex. (In the go1.14.2 source code I used, The client has not found any code that makes f.sensitive true.

Not to mention the author for the above e. ableSizeUpdate related logic control is the cause of e. ableSizeUpdate method for e.S etMaxDynamicTableSizeLimit and e.S etMaxDynamicTableSize, I found a comment in the source code associated with (*http2Transport).newClientconn (see previous article for the logic for this method) :

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

I see here when the heart excited ah, produced a strong desire to contribute to the code, but their ability is limited can only look at the opportunity but can not catch ah, have to hate hard work (joke ~, after all, a wise man said, write less BUG less 😄).

(*Encoder). SearchTable: Searches the Header from the HPACK index list and returns the corresponding index.

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
}
Copy the code

The static table is searched first. If the static table does not match, the dynamic table is searched.

Index Header notation

The corresponding function of this notation is appendIndexed, and the Header is already in the index list.

This function encodes the Header index in the HPACK index list, and the original Header can be represented in just a few bytes at the end.

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))
}
Copy the code

In appendIndexed header field notation, the first byte must be 0b1XXXXXXX, that is, the 0th bit must be 1, and the lower 7 bits are used to represent the value.

If the index is larger than ((1 << n) -1), multiple bytes are required to store the index value. The procedure is as follows:

  1. The lowest n bits of the first byte are all ones.
  2. Index I minus uint64((1 << n) -1), set the value to 7 bits lower or up each time0b10000000, and then I moves 7 bits to the right and compares with 128 to determine whether it enters the next cycle.
  3. At the end of the loop, the remaining I values are placed directly into buF.

When you represent the Header in this way, you need only a few bytes to represent a complete Header field, or, at best, a single byte to represent a Header field.

Added dynamic table Header notation

This notation corresponds to two cases: first, the Name of the Header has a matching index; Header Name and Value have no matching index. The corresponding handlers in these two cases are appendIndexedName and appendNewName respectively. In both cases, the Header is added to the dynamic table.

AppendIndexedName: Encodes a Header field that matches Name.

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)
}
Copy the code

Here we first look at the encodeTypeByte function:

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

As mentioned earlier in this example, indexing is always true and sensitive is false, so encodeTypeByte always returns 0x40.

At this point back to the appendIndexedName function, we know that the first byte to add the dynamic table Header representation must be in the format 0xB01XXXXXX, that is, the top two bits must be 01 and the bottom six bits must be used to represent the index of Name in the Header.

With appendVarInt encoding the index, let’s see how the appendHpackString function encodes the Value of the Header:

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
}
Copy the code

AppendHpackString is coded in two cases:

When the Huffman encoded length is less than the original Value, the final Huffman encoded length is stored in BUF with appendVarInt, and then the real Huffman encoded length is stored in BUF.

AppendVarInt stores the length of the original Value to BUF and then the original Value to BUF when the Huffman encoded length is greater than or equal to the original Value.

It should be noted here that only the lower 7 bits of the byte are used to store the Value length. The highest bit is 1, indicating that the stored content is Huffman encoding, and the highest bit is 0, indicating that the stored content is the original Value.

AppendNewName: The encoding Name and Value have no matching Header fields.

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)
}
Copy the code

As mentioned earlier, the return value of encodeTypeByte is 0x40, so the first byte we encode at this point is 0b01000000.

AppendHpackString encodes the Name and Value of the Header in sequence after the first byte encoding.

HPACK decoding

The previous reason for the HPACK encoding process, the following we through a decoding example to reason a decoding process.

// The encoding example in HPACK encoding is omitted here
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)
    // }
  }
  ifinvalid ! =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()))
// Output is as follows:
// 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 <nil>
Copy the code

You can see from the output of the last line that 197 bytes of the original Header data were actually decoded out of 111 bytes.

The author will analyze the process of decoding from the hdec.Write method and gradually uncover its mystery.

 func (d *Decoder) Write(p []byte) (n int, err error) {
   // Omit code here
	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 {
			// Omit code here
			d.saveBuf.Write(d.buf)
			return len(p), nil
		}
		// Omit code here
	}
	return len(p), err
}
Copy the code

In the process of debugging, the author found that the core logic of decoding was mainly in d. parseheaderfieldrepr method.

func (d *Decoder) parseHeaderFieldRepr(a) error {
	b := d.buf[0]
	switch {
	case b&128! =0:
		return d.parseFieldIndexed()
	case b&192= =64:
		return d.parseFieldLiteral(6, indexedTrue)
    // Omit code here
	}
	return DecodingError{errors.New("invalid encoding")}}Copy the code

There is only one case where the first byte and 128 are not 0, that is, b is data in 0B1XXXXXXX format. Based on the previous encoding logic, it can be known that the decoding method corresponding to index Header representation is D. parsefieldindexed.

There is only one case where the first byte and 192 are 64, and that is when b is 0b01XXXXXX. The decoding method corresponding to the addition of dynamic table Header notation is D. parsefieldLiteral.

Index Header notation

Indexed by (*Decoder).parsefieldIndexed indexed data already exists in a static or dynamic table, so it can be decoded by finding the corresponding Header using the HPACK index.

func (d *Decoder) parseFieldIndexed(a) error {
	buf := d.buf
	idx, buf, err := readVarInt(7, buf)
	iferr ! =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})
}
Copy the code

The above method mainly consists of three steps:

  1. throughreadVarIntFunction reads the HPACK index.
  2. throughd.atMethod to find the actual Header data in the index list.
  3. Pass the Header to the top layer.d.CallEmitIt will end up callinghdec.SetEmitFuncTo pass the Header to the top layer.

ReadVarInt: Reads the HPACK index

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
}
Copy the code

As we know from the readVarInt function above, if the low n of the first byte is not all 1, then the low n represents the real HPACK index and can be returned directly.

When the low n of the first byte is all 1, more bytes need to be read to calculate the true HPACK index.

  1. For the first loop, m is 0, the lowest 7 bits of B plus (1<

  2. In subsequent cycles, m increases by 7, and the lower 7 bits of B gradually fill in the higher part of I.

  3. When b is less than 128, the complete HPACK index has been read.

The readVarInt logic corresponds to the previous appendVarInt logic.

(*Decoder).at: Get real Header data according to HPACK index.

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
}
Copy the code

If the index is smaller than the static table length, the Header data is fetched directly from the static table.

Dt.len ()-(int(I)- statictable.len ())); dt.len()-(int(I)- statictable.len ()));

Added dynamic table Header notation

When decoding with (*Decoder).parsefieldLiteral, there are two cases to consider. Header Name has an index. Header Name and Value are not indexed. In both cases, the Header does not exist in the dynamic table.

The following is a step-by-step analysis of (*Decoder).parsefieldLiteral methods.

1. Read HPACK index in BUF.

nameIdx, buf, err := readVarInt(n, buf)
Copy the code

2. If the index is not 0, get the Header Name from the HPACK index list.

ihf, ok := d.at(nameIdx)
if! ok {return DecodingError{InvalidIndexError(nameIdx)}
}
hf.Name = ihf.Name
Copy the code

3. If the index is 0, read the Header Name from buf.

hf.Name, buf, err = d.readString(buf, wantStr)
Copy the code

Read the Value of the Header from the BUF and add the complete Header to the dynamic table.

hf.Value, buf, err = d.readString(buf, wantStr)
iferr ! =nil {
  return err
}
d.buf = buf
if it.indexed() {
  d.dynTab.add(hf)
}
Copy the code

(*Decoder). ReadString: Read real Header data from encoded byte data.

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)
	// omit the verification logic
	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)
		iferr := 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
}
Copy the code

First determine if the byte data is Huffman encoded (as in the previous appendHpackString function), then read the length of the data via readVarInt and assign it to strLen.

If the encoding is not Huffman, the strLen length is returned. If it is Huffman encoding, the strLen length of data is read, and the Huffman algorithm is used to decode and return.

Verification & Summary

We’ve looked at the HPACK index list and the codec process based on the HPACK index list.

The following author finally verifies the size of the Header after encoding and decoding.

// Omit the previous HAPACK encoding and HPACK decoding demo here
// try again
fmt.Println("try again: ")
buf.Reset()
henc.WriteField(hpack.HeaderField{Name: "custom-header", Value: "custom-value"}) // Encode the encoded Header
fmt.Println(hdec.Write(buf.Bytes())) / / decoding
/ / output:
// 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 <nil>
// try again:
// header field "custom-header" = "custom-value"
// 1 <nil>
Copy the code

As you can see from the output of the last line above, the decoding takes only one byte, or in this case, one byte to encode an already encoded Header.

To summarize: Client and Server maintain the same HPACK index list on one connection, and multiple requests can be divided into two situations when sending and receiving Header data.

  1. Header in the HPACK index list, you can transfer the HPACK index instead of the actual Header data to achieve Header compression.
  2. Header is not in the HPACK index list. For most headers, only the Value of the Header and the HPACK index of Name need to be transmitted, thus reducing the transmission of Header data. At the same time, the respective HPACK index lists are updated when such Header data is sent and received to ensure that the next request transmits as little Header data as possible.

Finally, a big thank you to those who have finished the HTTP2.0 series, and I sincerely hope you found it rewarding.

If you have any questions, you can discuss them harmoniously in the comments section. I will reply in time when I see them. I hope you can make progress together.

Note:

  1. When writing this article, the author used the GO version as go1.14.2
  2. Index Header notation and add dynamic table Header notation are named by the author themselves, mainly for readers to understand.

Reference:

Developers.google.com/web/fundame…