http://www.infoq.com/cn/news/2015/02/https-spdy-http2-comparison/
https://segmentfault.com/a/1190000002765886
SPDY令人贊嘆。這是十幾年來對HTTP的第一次真正的升級,它不僅解決了高延遲移動網絡的性能問題,還增強了Web的安全性。SPDY與HTTP有許多區別,但它最大的價值是它能通過復用僅僅一條(或幾條)TCP連接,在客戶端與服務器間發送幾十個請求或回應。
之前的評測吹噓說SPDY極其強力,從頁面加載速度變兩倍到相比純HTTP用SPDY和HTTPS能讓無線網站加載速度快23%。不過在實際生活中的網站上我沒發現有這么大的進步。老實說,我的測試表明SPDY僅比HTTPS快一點點,同時還比HTTP要慢。
為什么?簡單的說,SPDY改進了HTTP,但對大多數網站來說,HTTP不是瓶頸。
SPDY(讀作“SPeeDY”)是Google開發的基於TCP的傳輸層協議,用以最小化網絡延遲,提升網絡速度,優化用戶的網絡使用體驗。SPDY並不是一種用於替代HTTP的協議,而是對HTTP協議的增強。新協議的功能包括數據流的多路復用、請求優先級以及HTTP報頭壓縮。谷歌表示,引入SPDY協議后,在實驗室測試中頁面加載速度比原先快64%。[1-2]
-
將頁面加載時間減少50%。
-
最大限度地減少部署的復雜性。SPDY使用TCP作為傳輸層,因此無需改變現有的網絡設施。
-
避免網站開發者改動內容。 支持SPDY唯一需要變化的是客戶端代理和Web服務器應用程序。
-
單個TCP連接支持並發的HTTP請求。
-
壓縮報頭和去掉不必要的頭部來減少當前HTTP使用的帶寬。
-
定義一個容易實現,在服務器端高效率的協議。通過減少邊緣情況、定義易解析的消息格式來減少HTTP的復雜性。
-
強制使用 SSL,讓SSL協議在現存的網絡設施下有更好的安全性和兼容性。
https://baike.baidu.com/item/SPDY/3399551?fr=aladdin
HTTP/2 新特性淺析:
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 協議支持,為用戶提供更好更快的雲加速服務。
追加:目前又拍雲已全網支持 HTTP/2 協議及 SPDY3.1協議。