本文首發於 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 請求。
客戶端和服務端必須維護一套一樣的靜態字典,這裡給出了完整的靜態字典,客戶端和服務端都會遵守這套靜態字典。
你會發現靜態字典中有些 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 的情況下,就顯得不必要了。