原文:http://io.upyun.com/2015/05/13/http2/?utm_source=tuicool&utm_medium=referral
作者;sabakugaara
HTTP/2 源自 SPDY/2
SPDY 系列協議由谷歌開發,於 2009 年公開。它的設計目標是降低 50% 的頁面載入時間。當下很多著名的網際網路公司,例如百度、淘寶、UPYUN 都在自己的網站或 APP 中採用了 SPDY 系列協議(當前最新版本是 SPDY/3.1),因為它對效能的提升是顯而易見的。主流的瀏覽器(谷歌、火狐、Opera)也都早已經支援 SPDY,它已經成為了工業標準,HTTP Working-Group 最終決定以 SPDY/2 為基礎,開發 HTTP/2。
但是,HTTP/2 跟 SPDY 仍有不同的地方,主要是以下兩點:
HTTP/2 的優勢
相比 HTTP/1.x,HTTP/2 在底層傳輸做了很大的改動和優化:
- HTTP/2 採用二進位制格式傳輸資料,而非 HTTP/1.x 的文字格式。二進位制格式在協議的解析和優化擴充套件上帶來更多的優勢和可能。
- HTTP/2 對訊息頭採用 HPACK 進行壓縮傳輸,能夠節省訊息頭佔用的網路的流量。而 HTTP/1.x 每次請求,都會攜帶大量冗餘頭資訊,浪費了很多頻寬資源。頭壓縮能夠很好的解決該問題。
- 多路複用,直白的說就是所有的請求都是通過一個 TCP 連線併發完成。HTTP/1.x 雖然通過 pipeline 也能併發請求,但是多個請求之間的響應會被阻塞的,所以 pipeline 至今也沒有被普及應用,而 HTTP/2 做到了真正的併發請求。同時,流還支援優先順序和流量控制。
- Server Push:服務端能夠更快的把資源推送給客戶端。例如服務端可以主動把 JS 和 CSS 檔案推送給客戶端,而不需要客戶端解析 HTML 再傳送這些請求。當客戶端需要的時候,它已經在客戶端了。
HTTP/2 主要是 HTTP/1.x 在底層傳輸機制上的完全重構,HTTP/2 是基本相容 HTTP/1.x 的語義的(詳細相容性說明請戳這裡)。Content-Type
仍然是 Content-Type
,只不過它不再是文字傳輸了。那麼 HTTP/2 的這些新特性又是如何實現的呢?
HTTP/2 的基石 - Frame
Frame 是 HTTP/2 二進位制格式的基礎,基本可以把它理解為它 TCP 裡面的資料包一樣。HTTP/2 之所以能夠有如此多的新特性,正是因為底層資料格式的改變。 Frame 的基本格式如下(圖中的數字表示所佔位數,內容摘自 http2-draft-17):
+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+-+-------------+---------------+-------------------+
|R| Stream Identifier (31) |
+=+=================================================+
| Frame Payload (0...) ...
+---------------------------------------------------+
Length: 表示 Frame Payload 部分的長度,另外 Frame Header 的長度是固定的 9 位元組(Length + Type + Flags + R + Stream Identifier = 72 bit)。
Type: 區分這個 Frame Payload 儲存的資料是屬於 HTTP Header 還是 HTTP Body;另外 HTTP/2 新定義了一些其他的 Frame Type,例如,這個欄位為 0 時,表示 DATA 型別(即 HTTP/1.x 裡的 Body 部分資料)
Flags: 共 8 位, 每位都起標記作用。每種不同的 Frame Type 都有不同的 Frame Flags。例如傳送最後一個 DATA 型別的 Frame 時,就會將 Flags 最後一位設定 1(flags &= 0x01
),表示 END_STREAM,說明這個 Frame 是流的最後一個資料包。
R: 保留位。
Stream Identifier: 流 ID,當客戶端和服務端建立 TCP 連結時,就會先傳送一個 Stream ID = 0 的流,用來做些初始化工作。之後客戶端和服務端從 1 開始傳送請求/響應。
Frame 由 Frame Header 和 Frame Payload 兩部分組成。不論是原來的 HTTP Header 還是 HTTP Body,在 HTTP/2 中,都將這些資料儲存到 Frame Payload,組成一個個 Frame,再傳送響應/請求。通過 Frame Header 中的 Type 區分這個 Frame 的型別。由此可見語義並沒有太大變化,而是資料的格式變成二進位制的 Frame。二者的轉換和關係如下圖:
為 HTTP/2 頭壓縮專門設計的 HPACK
如果我們約定將常用的請求比如 GET /index.html
用一個 1 來表示,POST /index.html
用 2 來表示。那麼是不是可以節省很多位元組?
為 HTTP/2 的專門量身打造的 HPACK 便是類似這樣的思路延伸。它使用一份索引表來定義常用的 HTTP Header。把常用的 HTTP Header 存放在表裡。請求的時候便只需要傳送在表裡的索引位置即可。例如 :method=GET
使用索引值 2 表示,:path=/index.html
使用索引值 5 表示(完整的列表參考:HPACK Static Table)。只要給服務端傳送一個 Frame,該 Frame 的 Payload 部分儲存 0x8285
,Frame 的 Type 設定為 Header 型別,便可表示這個 Frame 屬於 HTTP Header,請求的內容是:
GET /index.html
為什麼是 0x8285
,而不是 0x0205
? 這是因為高位設定為 1 表示這個位元組是一個完全索引值(key 和 value 都在索引中)。類似的,通過高位的標誌位可以區分出這個位元組是屬於一個完全索引值,還是僅索引了 key,還是 key 和 value 都沒有索引。因為索引表的大小的是有限的,它僅儲存了一些常用的 HTTP Header,同時每次請求還可以在表的末尾動態追加新的 HTTP Header 快取。動態部分稱之為 Dynamic Table。Static Table 和 Dynamic Table 在一起組合成了索引表:
<---------- Index Address Space ---------->
<-- Static Table --> <-- Dynamic Table -->
+---+-----------+---+ +---+-----------+---+
| 1 | ... | s | |s+1| ... |s+k|
+---+-----------+---+ +---+-----------+---+
^ |
| V
Insertion Point Dropping Point
HPACK 不僅僅通過索引鍵值對來降低資料量,同時還會將字串進行霍夫曼編碼來壓縮字串大小。
以常用的 User-Agent
為例,它在靜態表中的索引值是 58,它的值是不存在表中的,因為它的值是多變的。第一次請求的時候它的 key 用 58 表示,表示這是一個 User-Agent
,它的值部分會進行霍夫曼編碼(如果編碼後的字串變更長了,則不採用霍夫曼編碼)。服務端收到請求後,會將這個 User-Agent
新增到 Dynamic Table 快取起來,分配一個新的索引值。客戶端下一次請求時,假設上次請求User-Agent
的在表中的索引位置是 62, 此時只需要傳送 0xBE
(同樣的,高位置 1),便可以代表: User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.146 Safari/537.36
。其過程如下圖所示:
最終,相同的 Header 只需要傳送索引值,新的 Header 會重新加入 Dynamic Table。
Multipexing 多路複用
每個 Frame Header 都有一個 Stream ID 就是被用於實現該特性。每次請求/響應使用不同的 Stream ID。就像同一個 TCP 連結上的資料包通過 IP:PORT
來區分出資料包去往哪裡一樣。通過 Stream ID 標識,所有的請求和響應都可以歡快的同時跑在一條 TCP 連結上了。 下圖是 http 和 spdy(http2 的模型和 spdy 是類似的) 的併發模型對比:
當流併發時,就會涉及到流的優先順序和依賴。優先順序高的流會被優先傳送。圖片請求的優先順序要低於 CSS 和 SCRIPT,這個設計可以確保重要的東西可以被優先載入完。
Server Push
當服務端需要主動推送某個資源時,便會傳送一個 Frame Type 為 PUSH_PROMISE 的 Frame,裡面帶了 PUSH 需要新建的 Stream ID。意思是告訴客戶端:接下來我要用這個 ID 向你傳送東西,客戶端準備好接著。客戶端解析 Frame 時,發現它是一個 PUSH_PROMISE 型別,便會準備接收服務端要推送的流。
結束語
本文簡化了很多 HTTP/2 協議中的具體細節,只描述了 HTTP/2 中主要特性實現的基本過程。
如果你想實現一個支援 HTTP/2 的伺服器,那麼你可以移步 HTTP/2 官網 做更多瞭解,它還提供了一份已經實現 HTTP/2 的專案列表:https://github.com/http2/http2-spec/wiki/Implementations 。
另外,關於 HTTP/2 效能如何,可以參考官方小組給出的例子:https://http2.akamai.com/demo。
UPYUN 在不久的將來也會加入對 HTTP/2 協議支援,為使用者提供更好更快的雲加速服務。