詳解http-2頭部壓縮算法

準備工做

使用wireshark抓取http2.0請求 html

點擊查看算法

正式內容

問題背景

  1. HTTP1.x的header中的字段不少時候都是重複的,例如method:getstatus:200等等,隨着網頁增加到須要數十到數百個請求,這些請求中的冗餘標頭字段沒必要要地消耗帶寬,從而顯著增長了延遲,所以,Hpack技術應時而生。

Hpack思想簡介

首先介紹下壓縮的概念(比較簡單,熟悉的能夠跳過):segmentfault

  1. 通訊的雙方各擁有一本字典,記錄着某些字符對應的文本內容,例如x表明危險,y表明撤退,z表明進攻等;
  2. 消息發送方根據字典生成消息文本好比'x,y'
  3. 接收方接收到消息後,根據字典還原內容:「危險,撤退」

這個例子已經簡單介紹了壓縮的好處:能夠在傳輸的過程,簡化消息內容,從而下降消息的大小cookie

官方文檔裏的對Hpack的主要思想說明:工具

  1. 將header裏的字段列表視爲可包括重複對的name-value鍵值對的有序集合,分別使用8位字節表示name和value
  2. 當字段被編碼/解碼時,對應的字典會不斷擴充
  3. 在編碼形式中,header字段能夠直接表示,也可使用header field tables 中對應的引用。所以,可使用引用和文字值的混合來header字段列表。
  4. 文字值要麼直接編碼,要麼使用靜態huffman代碼
  5. 編碼器負責決定在標題字段表中插入哪些標題字段做爲新條目。解碼器執行對編碼器規定的報頭字段表的修改,重建處理中的報頭字段列表

以上摘自RFC 7541協議使用翻譯工具直接翻譯-_-,因此看起來有點艱澀,不要緊,先往下看編碼

引子-壓縮效果比對

因爲理論內容比較枯燥,因此先來幾張圖看一下效果,這裏使用wireshark來抓取對同一個頁面的兩次請求,查看對比。spa

初次請求

  • 頭部長度412,解壓完 690 壓縮完大概是原來的60%

圖片描述

二次請求

  • 頭部長度172,解壓完 690 壓縮完大概是原來的25%

圖片描述

簡單分析

cookie這個字段爲例,在上述兩次請求中:翻譯

  • 第一次請求時cookie所佔的字符長度爲36

圖片描述

  • 第二次請求時cookie所佔的字符長度爲1

圖片描述

因此經過簡單觀察,咱們能夠簡單得出如下結論:code

  1. 頭部壓縮能夠減少請求的頭部大小(顯而易見)
  2. 二次壓縮的壓縮率會更高,(後面會解釋爲何)

過程簡述

簡單描述一下Hpack算法的過程:htm

  • 消息發送端和消息接受端共同維護一份靜態表和一份動態表(這兩個合起來充當字典的角色),
  • 每次請求時,發送方根據字典的內容以及一些特定指定,編碼壓縮消息頭部,
  • 接收方根據字典進行解碼,而且根據指令來判斷是否須要更新動態表

技術細節

基本概念

首先介紹一下前面在說明「壓縮過程」時,提到的字典。在Hpack中,一共使用2個表來充當字典的角色:靜態表和動態表。

靜態表

靜態表很簡單,只包含已知的header字段。點此查看完整的靜態表,分爲兩種:

  1. name和value均可以徹底肯定,好比:metho: GET:status: 200
  2. 只可以肯定name:好比:authoritycookie
  • 第一種狀況很好理解,已知鍵值對直接使用一個字符表示;
  • 第二種狀況稍微說明下:首先將name部分先用一個字符(好比cookie)來表示,同時,根據狀況判斷是否告知服務端,將 cookie: xxxxxxx 添加到動態表中(咱們這裏默認假定是從客戶端向服務端發送消息)

動態表

  • 動態表最初是一個空表,當每次解壓頭部的時候,有可能會添加條目(好比前面提到的cookie,當解壓過一次cookie時,cookie: xxxxxxx就有可能被添加到動態表了,至因而否添加要根據後面提到的指令判斷)
  • 動態表容許包含重複的條目,也就是可能出現徹底相同的鍵值對
  • 爲了限制解碼器的需求,動態表大小有嚴格限制的

索引地址空間

靜態表和動態表一塊兒組成一個索引地址空間。設靜態表長度爲s,動態表長度爲k,那麼最終的索引空間以下:

<----------  Index Address Space ---------->
<-- Static  Table -->  <-- Dynamic Table -->
+---+-----------+---+  +---+-----------+---+
| 1 |    ...    | s |  |s+1|    ...    |s+k|
+---+-----------+---+  +---+-----------+---+
                       ^                   |
                       |                   V
                Insertion Point      Dropping Point

其中:

  • 索引1-s是靜態表,s-k是動態表,
  • 新的條目從在動態表的開頭插入,從動態表末尾移除

有了這個索引空間之後,header的字段一共有如下幾種表示方法:

  • 直接用索引值來表示(好比2表示method:get)
  • 字段的name使用索引值表示,字段的value直接使用原有字面的值的八位字節序列或者使用靜態哈夫曼編碼表示

字段表示法

header字段的表示法一共分2種,下面逐一說明。

數字表示法

數字主要用來表示上文中索引空間的索引值,具體的規則以下:

  1. 先用限定位數的前綴表示,若是範圍足夠那就直接表示(限定位數是指下圖中的扣除xxx剩餘的長度,xxx的具體含義見下一節-動態表更新指令以及表示)
  2. 若是範圍不夠大,那麼接下來每次增長8個字節來表示
  3. 8個字節的最高位都做爲標誌位,表示是否要繼續向下延續(解碼的時候要用到)

接下來看官方的一些例子幫助理解:

1. 用5位前綴表示10

首先這裏限制位數爲5,因爲10小於2^5-1,能夠直接表示爲01010,結果爲:

0   1   2   3   4   5   6   7
+---+---+---+---+---+---+---+---+
| X | X | X | 0 | 1 | 0 | 1 | 0 |   10 stored on 5 bits
+---+---+---+---+---+---+---+---

2. 用5位前綴表示1337

  1. 1337>2^5-1,那麼前面5位只能表示到31,剩餘1337-31 = 1306
  2. 接下來:1306>2^7 =128(八位字節第一位是標誌位,因此表示範圍只有2^7-1)
  3. I % 128 == 26,26用7位2進製表示是0011010,因爲I >128 還須要繼續延續,因此標誌位取1,獲得第二行應該是10011010
  4. I / 128 = 10,10用7位2進製表示是0001010,標誌位取0便可因此最終結果以下:
0   1   2   3   4   5   6   7
+---+---+---+---+---+---+---+---+
| X | X | X | 1 | 1 | 1 | 1 | 1 |  Prefix = 31, I = 1306
| 1 | 0 | 0 | 1 | 1 | 0 | 1 | 0 |  1306>=128, encode(154), I=1306/128
| 0 | 0 | 0 | 0 | 1 | 0 | 1 | 0 |  10<128, encode(10), done
+---+---+---+---+---+---+---+---+

3. 直接從邊界開始表示42

直接從邊界開始,也就是使用8位前綴,42小於2^8-1=255 因此直接表示:

0   1   2   3   4   5   6   7
+---+---+---+---+---+---+---+---+
| 0 | 0 | 1 | 0 | 1 | 0 | 1 | 0 |   42 stored on 8 bits
+---+---+---+---+---+---+---+---+

字符串表示法

header的字段能夠用字符串文原本表示,具體的規則以下:

0   1   2   3   4   5   6   7
+---+---+---+---+---+---+---+---+
| H |    String Length (7+)     |
+---+---------------------------+
|  String Data (Length octets)  |
+-------------------------------+
  • H是一個標誌位,表示該字符串的八位字節是否被哈夫曼編碼過
  • String Length:,表示用於編碼的字節位數,具體的規則就是剛剛提到的7位前綴表示法
  • String Data:字符串編碼過的數據,若是h爲0,則編碼數據是字符串文字的原始八位字節;若是H是「1」,則編碼數據是字符串文字的huffman編碼。huffman編碼參見

動態表更新指令

狀況1:整個鍵值對都在現有的索引空間中

這種狀況下,第一個字節固定爲1,而後用7位前綴法表示索引的值

0   1   2   3   4   5   6   7
+---+---+---+---+---+---+---+---+
| 1 |        Index (7+)         |
+---+---------------------------+
Figure 5: Indexed Header Field

An indexed header field starts with the '1

例如10000010,表示索引值爲2,查找靜態表可知,對應的header字段是method:GET
圖片描述

注意咱們前面說索引空間的時候提到,索引空間地址是從1開始的,0的話會被視爲錯誤,也就是10000000解碼時會出錯。

狀況2:name在索引空間,可是value不在,且須要更新動態表

這種狀況下,前兩位固定爲01,後面6位表示索引值,取到對應的name,例如01010000對應32,查靜態表可知name是cookie,接下來使用字符串表示法表示對應的value字段,在解碼以後,這個字段就被加到動態表中,下次編碼的時候會直接使用狀況1,(這裏也就說明了爲何後續請求壓縮程度更大,由於動態表在不斷擴充,擴充的界限請看官方文檔這裏暫時不說明)

0   1   2   3   4   5   6   7
+---+---+---+---+---+---+---+---+
| 0 | 1 |      Index (6+)       |
+---+---+-----------------------+
| H |     Value Length (7+)     |
+---+---------------------------+
| Value String (Length octets)  |
+-------------------------------+

狀況3:name和value都不在索引空間,且須要更新動態表

這種狀況和上面的很類似,只要補上name部分的字符串表示,而且把index值設置爲0便可。

0   1   2   3   4   5   6   7
+---+---+---+---+---+---+---+---+
| 0 | 1 |           0           |
+---+---+-----------------------+
| H |     Name Length (7+)      |
+---+---------------------------+
|  Name String (Length octets)  |
+---+---------------------------+
| H |     Value Length (7+)     |
+---+---------------------------+
| Value String (Length octets)  |
+-------------------------------+

觀察狀況2和狀況3可知,若是須要更新動態表,前兩位標誌位都是01

狀況4:name在索引空間,可是value不在,且不須要更新動態表

這種狀況,前四位固定爲0000,其餘和狀況2一致,

0   1   2   3   4   5   6   7
+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | 0 |  Index (4+)   |
+---+---+-----------------------+
| H |     Value Length (7+)     |
+---+---------------------------+
| Value String (Length octets)  |
+-------------------------------+

狀況5 name和value都不在索引空間,且不須要更新動態表

同理,前四位固定爲0000,其餘和狀況3一致,

0   1   2   3   4   5   6   7
+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | 0 |       0       |
+---+---+-----------------------+
| H |     Name Length (7+)      |
+---+---------------------------+
|  Name String (Length octets)  |
+---+---------------------------+
| H |     Value Length (7+)     |
+---+---------------------------+
| Value String (Length octets)  |
+-------------------------------+

觀察狀況2和狀況3可知,若是須要更新動態表,前兩位標誌位都是0000

狀況6 name在索引空間,可是value不在,且絕對不容許更新動態表

這種狀況下和狀況4基本一致,只是前四位固定爲0001,區別在於:

  • 不須要更新表示,本次的發送過程不更新該字段到動態表;若是有屢次轉發,那麼並不對轉發作要求
  • 絕對不容許更新表示,若是這個請求被屢次轉發纔到目標,那麼轉發的全部中間對於該字段也必須採用相同的處理方案
0   1   2   3   4   5   6   7
+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | 0 |  Index (4+)   |
+---+---+-----------------------+
| H |     Value Length (7+)     |
+---+---------------------------+
| Value String (Length octets)  |
+-------------------------------+

狀況7 name和value都不在索引空間,且絕對不容許更新動態

0   1   2   3   4   5   6   7
+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | 1 |       0       |
+---+---+-----------------------+
| H |     Name Length (7+)      |
+---+---------------------------+
|  Name String (Length octets)  |
+---+---------------------------+
| H |     Value Length (7+)     |
+---+---------------------------+
| Value String (Length octets)  |
+-------------------------------+

和上面一種狀況同理,就略過了。

補充實例分析

主要內容已經都說完了,接下來抓一些請求來看看具體的內容,好比直接抓取segment下的請求。
圖片描述

  1. 在這裏method字段,就是前面提到的狀況1--直接在靜態表查詢就能夠獲得整個鍵值對,對應的索引值爲2。 查看底下的編碼10000010,第一位1表示整個鍵值對在索引空間存在,後面的0000010=2表示索引地址。

接下來看authority字段,咱們抓第一次和第二次請求的進行對比:

  • 第一次請求

圖片描述

能夠看出,這個字段符合前面提到的狀況2:第一次編碼是01000001,表示name直接使用索引,索引值爲1,且value不在索引空間中,後面的部分表示具體的value值

  • 第二次請求

圖片描述

第二次請求,發現已是直接使用索引空間的值(由於前一次請求已經要求更新到動態表),因此本次只要一個字符長度直接表示這個字段110001101,第一個1表示狀況1,後面1001101=64+8+4+1 =77 也就是此時對應的索引值

小結

咱們前面提到動態表會隨請求增長不斷更新,可是動態表實際上是有大小限制的,所以動態表在增長條目時也可能會刪除條目,具體的更新規則等限於篇幅(沒錯,不是由於懶)不在本文更新。還有就是相關的huffman編碼等也不在此說明,本文主要仍是針對Hpack算法的過程和編碼規則作一些說明。主要參照RFC 7541協議


慣例:若是內容有錯誤的地方歡迎指出(以爲看着不理解不舒服想吐槽也徹底沒問題);若是對你有幫助,歡迎點贊和收藏,轉載請徵得贊成後著明出處,若是有問題也歡迎私信交流,主頁有郵箱地址

相關文章
相關標籤/搜索