http2 簡介

jaychen發表於2018-03-17

本文首發於 https://jaychen.cc

作者:jaychen

寫一點東西關於 http2 的東西。

http2 的前身是由 google 領導開發的 SPDY,後來 google 把整個成果交給 IETF,IETF 把 SPDY 標準化之後變成 http2。google 也很大方的廢棄掉 SPDY,轉向支援 http2。http2 是完全相容 http/1.x 的,在此基礎上新增了 4 個主要新特性:

  • 二進位制分幀
  • 頭部壓縮
  • 服務端推送
  • 多路複用
  • 優化手段

下面主要講下這 4 個特性。

二進位制分幀

http/1.x 是一個文字協議,而 http2 是一個徹徹底底的二進位制協議,這也是 http2 可以折騰出那麼多新花樣的原因。http2 的二進位制協議被稱之為二進位制分幀

http2 協議的格式為幀,類似 TCP 中的資料包文。

+--------------------------------------------------------------+   ^
|                                                              |   |
|                   Length (24)                                |   |
|                                                              |   |
|                                                              |   |
+----------------------+---------------------------------------+   |
|                      |                                       |   +
|                      |                                       |
|        Type (8)      |     Flag (8)                          |  Frame Header
|                      |                                       |   +
+----+-----------------+---------------------------------------+   |
|    |                                                         |   |
|    |                                                         |   |
| R  |                  Stream Identifier (31)                 |   |
|    |                                                         |   v
+----+---------------------------------------------------------+
|                                                              |
|                            Frame Payload                     |
|                                                              |
+--------------------------------------------------------------+


複製程式碼

幀由 Frame Header 和 Frame Payload 組成。之前在 http/1.x 中的 header 和 body 都放在 Frame Payload 中。

  • Type 欄位用來表示該幀中的 Frame Payload 儲存的是 header 資料還是 body 資料。除了用於標識 header/body,還有一些額外的 Frame Type。
  • Length 欄位用來表示 Frame Payload 資料大小。
  • Frame Payload 用來儲存 header 或者 body 的資料。

**Stream Identifier 用來標識該 frame 屬於哪個 stream。**這句話可能感覺略突兀,這裡要明白 Stream Identifier 的作用,需要引出 http2 的第二個特性『多路複用』。

多路複用

在 http/1.x 情況下,每個 http 請求都會建立一個 TCP 連線,這就意味著每個請求都需要進行三次握手。這樣子就會浪費比較多的時間和資源,這點在 http/1.x 的情況下是沒有辦法避免的。並且瀏覽器會限制同一個域名下併發請求的個數。所以,在 http/1.x 的情況下,一個常見的優化手段是把靜態資源分佈到不同域名下,以此來突破瀏覽器併發數的限制。

在 http2 的情況下,所有的請求都會共用一個 TCP 連線,這個可以說是 http2 殺手級的特性了。 :punch: 因為這點,許多在 http/1.x 時代的優化手段都可以退休了。但是這裡也出現了一個問題,所有的請求都共用一個 TCP 連線,那麼客戶端/服務端怎麼知道某一幀(別忘記上面說了 http2 是的基本單位是幀)的資料屬於哪個請求呢?

上面的 Stream Identifier 就是用來標識該幀屬於哪個請求的。

當客戶端同時向服務端發起多個請求,那麼這些請求會被分解成一一個的幀,每個幀都會在一個 TCP 鏈路中無序的傳輸,同一個請求的幀的 Stream Identifier 都是一樣的。當幀到達服務端之後,就可以根據 Stream Identifier 來重新組合得到完整的請求。

頭部壓縮

在 http/1.x 協議中,每次請求都會攜帶 header 資料,而類似 User-Agent, Accept-Language 等資訊在每次請求過程中幾乎是不變的,那麼這些資訊在每次請求過程中就變成了浪費。所以, http2 中提出了一個 HPACK 的壓縮方式,用於減少 http header 在每次請求中消耗的流量。

HPACK 壓縮的原理如下 :

客戶端和服務端共同維護一個『靜態字典』,字典中每行 3 列,類似下表

index header name header value
2 :method GET
3 :method POST

當請求的 header 頭部中包含 :mehtod:GET,客戶端在傳送請求的時候,會直接傳送靜態欄位中對應的 index 值,在這裡也就是 2。服務端在接受到請求的時候,去尋找靜態字典中 index = 2 對應的 header name 和 header value,就明白了客戶端發起了一個 GET 請求。

客戶端和服務端必須維護一套一樣的靜態字典,這裡給出了完整的靜態字典,客戶端和服務端都會遵守這套靜態字典。

http2 簡介

你會發現靜態字典中有些 header value 沒有值。這是因為有些 header 欄位的值是不定的,比如 User-Agent 欄位,所以標準中沒有定下 header value 的值。

那麼如果碰到在靜態字典中 header value 沒有的值,HPEACK 演算法會採取下面的方式:

假設 http 請求的 header 中包含了 User-Agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36,那麼 HPACK 會對 User-Agent 的值進行哈夫曼編碼,然後在靜態字典中找到 User-Agent 的 index 為 58,那麼客戶端會把 User-Agent 的 index 值和 User-Agent 值對應的哈夫曼編碼值傳送給服務端。

User-Agent : Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36

會被轉換陳下面的 kv 值傳送給服務端:

58 : Huffman('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36')
複製程式碼

服務端收到請求之後,把 User-Agent 和哈夫曼編碼值追加到靜態字典後面,這些追加的行稱之為『動態字典』。

index header name header value
2 :method GET
3 :method POST
... .... .....
62 User-Agent Huffman('header value')

客戶端在傳送請求的時候,也會把該行新增到自己維護的靜態字典表後面,這樣子客戶端和服務端維護的字典表就會保持一致。之後的請求客戶端如果需要攜帶 User-Agent 欄位,只要傳送 62 即可。

http2 中情況就完全不一樣了,所有的請求都是在一個 TCP 連線中完成的。

服務端推送

服務端推送指的是服務端主動向客戶端推送資料。

舉個例子,index.html 有如下程式碼

<!DOCTYPE html>
<html>
<head>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <h1>hello world</h1>
  <img src="something.png">
</body>
</html>
複製程式碼

那麼正常情況下,為了展示頁面需要發 3 次請求:

  • 發起 1 次請求 index.html 頁面
  • 解析 index.html 頁面發現 style.css 和 something.png 資源,發起 2 次請求獲取資源。

如果服務端配置了服務端推送之後,那麼情況變成下面的樣子:

  • 瀏覽器請求 index.html。
  • 伺服器發現瀏覽器請求的 index.html 中包含 style.css 和 something.png 資源,於是直接 index.html, style.css, something.png 三個資源都返回給瀏覽器。

這樣,服務端和瀏覽器只需要進行一次通訊,就可以獲取到全部資源。

http/1.x 轉 http2

http2 的目的就是為了優化 http/1.x 的一些效能問題,所以當 http2 到來之後,很多針對 http/1.x 的優化手段已經不管用。而使用 http2 我們又應該注意一些什麼問題?

https

https 和 http2 的恩怨很有趣。google 在開發 SPDY 的時候是強制使用 https 的,按照道理基於 SPDY 的 http2 也應該是強制 https 的,但是由於社群的阻礙 http2 可以不使用 https 協議。但是 chrome 和 firefox 都表示只會開發基於 https 的 http2,所以基本意味著使用 http2 的前提是必須是 https。

不必要的優化

在 http/1.x 的時代,為了減少瀏覽器的請求數/提高瀏覽器的併發數,通常會使用如下的手段來進行優化:

  • 域名分片:把靜態資源分佈在不同的域名下,以突破瀏覽器對統一域名併發數的限制。(在多路複用中提到)
  • 合併檔案:前端通常會把多個小檔案合併成一個大檔案,這樣瀏覽器只要進行一次請求就可以獲取資源。但是這樣做有一個缺陷就是:如果只是改動了檔案的一小部分內容,就要重新傳送全部內容。

以上的優化手段,在 http2 的情況下,就顯得不必要了。