Http演進之路之六

lonnieZ:Http演進之路之六?

zhuanlan.zhihu.com圖標

Http/2

鑒於SPDY的成功,HTTP/2的開發計劃也呼之欲出並且眾望所歸的採用了SPDY作為整個方案的藍圖進行開發。由IETF推動,Google等公司重點參與並於2015年3月公布了草案。其最終RFC可以參考這裡。

與SPDY的差異

雖然HTTP/2大體上沿用了SPDY的設計理念。但仍然有部分差異,這主要集中體現在以下幾點:

  • HTTP/2可以在TCP之上直接使用,不像SPDY那樣必須在TLS層之上
  • 更加完善的協議商討和確認流程
  • 新的頭部壓縮演算法HPACK
  • 添加了控制幀的種類,對幀的格式考慮更加細緻
  • 更加完善的Server Push流程

不一樣的層次結構

對於SPDY而言,在使用SPDY協議之前可以通過NPN(Next Protocol Negotiation)進行協議溝通來協商使用的具體協議(HTTP/1.X或SPDY),一旦決定使用SPDY則必須建立在TLS之上。

對於HTTP/2而言則沒有這個限制,在使用HTTP/2之前也需要通過ALPN(Application Layer Protocol Negotiation)協商具體協議(HTTP/1.X或HTTP/2),當決定使用HTTP/2時可以建立在TLS之上,也可以直接建立在TCP之上。

ALPN協商

在HTTP/2中使用ALPN(Application Layer Protocol Negotiation)替代了SPDY中的NPN(Next Protocol Negotiation)來協商使用的具體協議。ALPN與NPN都是TLS擴展協議,他們發生在ClientHello和ServerHello階段。他們用來client端與server端協商使用的協議,由於並不是所有server端或client端都支持SPDY或HTTP/2,因此在正式啟用相關協議之前,客戶端與服務端要進行協商。以下是ALPN的一個具體示例,首先看到的是ClientHello中向server端問詢可以使用的協議:

從圖中我們可以看到,Client端向Server端問詢了HTTP/2和HTTP/1.1可以使用哪個協議。下面是server端通過ServerHello來答覆client端:

從Server端的答覆來看可以使用HTTP/2來進行通信。而下圖是另一個server的答覆,這個server目前只能支持HTTP/1.1:

HPACK壓縮(見Http演進之路之六)

幀格式

在HTTP/2中把幀分為數據幀與控制幀:

SETTINGS幀

+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| 0x4 (8) | Flags (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier/0x0 (32) |
+=+=============================+===============================+
| Identifier (16) |
+-------------------------------+-------------------------------+
| Value (32) |
+---------------------------------------------------------------+

當連接建立成功後發送的第一個幀,用來傳遞配置參數。在連接周期內的任意時刻、任意一方都可以發送SETTINGS幀來調節相關參數。由於SETTINGS幀是針對整個連接的,而不是只對某一個單獨的stream,因此其內部的Stream ID值為0。通過下面的抓包可以看到在該SETTINGS幀中會攜帶Header Table Size、Initial Window Size、Max Frame Size等參數。

PING幀

+-----------------------------------------------+
| 0x8 (24) |
+---------------+---------------+---------------+
| 0x6 (8) | Flag (8) |
+-+-------------+---------------+-------------------------------+
|R| 0x0 (32) |
+=+=============================================================+
| Opaque Data (64) |
+---------------------------------------------------------------+

用來進行心跳監測或計算兩端直接的RTT。其中的Flag如果為0表示該幀為一個PING操作,接收方必須答覆一個Flag為1的PONG幀。

GOAWAY幀

+-+-------------------------------------------------------------+
|R| Last-Stream-ID (31) |
+-+-------------------------------------------------------------+
| Error Code (32) |
+---------------------------------------------------------------+
| Additional Debug Data (*) |
+---------------------------------------------------------------+

該幀用於觸發連接的關閉流程,或者將嚴重的錯誤通知給對端。它允許端點停止接受新流同時繼續完成之前建立的流的處理過程。

WINDOW_UPDATE幀

+-----------------------------------------------+
| 0x4 (24) |
+---------------+---------------+---------------+
| 0x8 (8) | 0x0 (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+=+=============================================================+
|R| Window Size Increment (31) |
+-+-------------------------------------------------------------+

WINDOW_UPDATE幀用來實現流量控制,發送方發送WINDOW_UPDATE幀來告訴接收方自己此時可以發送的最大位元組數以及接收者可以接收到的最大位元組數。流量控制可以應用到單個流也可以應用到整個連接承載的所有流(此時流ID為0)。此外,發送者不能發送一個大於接收者已持有(接收端已經擁有一個流控值)可用空間大小的WINDOW_UPDATE幀且該幀僅會影響DATA幀。下圖為一個對整個連接的流控消息:

下面是針對某個stream上的流控消息:

PRIORITY幀

+-----------------------------------------------+
| 0x5 (24) |
+---------------+---------------+---------------+
| 0x2 (8) | 0x0 (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+=+=============================================================+
|E| Stream Dependency (31) |
+-+-------------+-----------------------------------------------+
| Weight (8) |
+---------------+

明確了發送者建議的流的優先順序。該幀可以在任意流狀態下發送,包括空閑狀態和關閉狀態。其中Weight(權重)為8bit的整數,用來標識流的優先順序權重,範圍是1-256之間;Dependency表示是否依賴於其他流(父親流)。此外,優先順序也可以通過HEADER幀進行傳遞。

RST_STREAM幀

+-----------------------------------------------+
| 0x4 (24) |
+---------------+---------------+---------------+
| 0x3 (8) | 0x0 (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+=+=============================================================+
| Error Code (32) |
+---------------------------------------------------------------+

Reset幀,用來在發生錯誤的時候關閉流。在流上收到該幀後,除了PRIORITY幀,接收方不能再發送額外的幀,而發送方必須做好接收和處理該流上額外的幀,這些幀可能是對端在收到RST_STREAM之前發送出來的。此外,RST_STREAM幀必須與某一個流關聯。

HEADER幀

+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| 0x1 (8) | Flags (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier/0x0 (32) |
+=+=============================+===============================+
|Pad Length? (8)|
+-+-------------+-----------------------------------------------+
|E| Stream Dependency? (31) |
+-+-------------+-----------------------------------------------+
| Weight? (8) |
+-+-------------+-----------------------------------------------+
| Header Block Fragment (*) ...
+---------------------------------------------------------------+
| Padding (*) ...
+---------------------------------------------------------------+

HEADER幀用來打開一個流以及傳遞Headers信息。其中Pad Length(填充長度)表示填充位元組(Padding)的長度。該欄位為可選,只有設置了PADDED標記位時才有效。E佔用1個bit,表示依賴流是否為排他的,只有設置了PRIORITY標誌位時才有效。Stream Dependency為依賴流,即「父親流」,仍然為設置了PRIORITY標誌位時才有效。Weight為權重,為設置了PRIORITY標誌位時才有效。Header Block Fragment包含了頭域信息。

從這個HEADER中可以看出其Flag為0x24,即"End Headers"和「Priority」為True,由於「Priority」為True,因此後面的E、Stream Dependency、Weight都是有效的。由於「End Headers」為True,表明請求頭/響應頭信息傳遞結束,如果沒有設置「End Headers」則後面必須跟一個CONTINUATION幀繼續傳遞剩餘的信息。當「End Stream」為True的時候,表明該HEADER幀為當前流上的最後一個數據。

從這個HEADER信息可以看出,當Flag裡面沒有設置PRIORITY的時候,則E、Stream Dependency、Priority都是無效的。

DATA幀

+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| 0x0 (8) | Flag (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+=+=============+===============================================+
|Pad Length (8)|
+---------------+-----------------------------------------------+
| Data (*) ...
+---------------------------------------------------------------+
| Padding (*) ...
+---------------------------------------------------------------+

DATA幀用來傳遞與具體流相關聯的任意的、可變長度的位元組序列。DATA幀必須與一個具體的流相關聯,即其Stream Identifier不能為0,此外,只有當流處於「打開」或「半關閉」狀態時,才能發送DATA幀。

上圖是流45上的兩個DATA幀,當「End Stream」為true時表示該幀是當前流上最後一個幀,從而導致流進入「半關閉」或「關閉」狀態。當「Padded」為true時表示該幀中包含「Pad Length」和Padding

完善的Server Push機制

有些人對「Server Push」機制存在一定的誤解,認為這種技術可以讓服務端主動向瀏覽器「推送消息」,甚至將其與WebSocket進行對比。實際上「Server Push」機制只是省去了瀏覽器發送請求的過程。從上面在SPDY章節中介紹「Server Push」機制中就可以看出,只有當服務端認為某些資源存在一定的關聯性,即用戶申請了資源A,勢必會繼續申請資源B、資源C、資源D...的時候,服務端才會主動推送這些資源,以此來達到節省瀏覽器發送request請求的過程。PUSH_PROMISE幀 當服務端想使用Server Push推送資源的時候,會先向客戶端發送PUSH_PROMISE幀。

+---------------+
|Pad Length? (8)|
+-+-------------+-----------------------------------------------+
|R| Promised Stream ID (31) |
+-+-----------------------------+-------------------------------+
| Header Block Fragment (*) ...
+---------------------------------------------------------------+
| Padding (*) ...
+---------------------------------------------------------------+

其中Promised Stream ID標識了它所關聯的流,它的值不能為0.

為了進一步演示HTTP2的PUSH功能,我們在本地搭建了一個ngnix的server使其具有push的功能。搭建的方法可以參考這裡。可以看到,當我們把其中的png和css資源設置為push後,在通過chrome訪問時,他們的狀態會置為push狀態。

性能對比

為了驗證HTTP/2的性能,我在本地使用Nginx(版本1.15.2)搭建了一個伺服器,該伺服器上的index頁面分別包含了10個、30個、50個、100個ico圖片,令server端分別工作在http、http2、http2-push模式並通過chrome瀏覽器對其進行訪問。

下圖是http訪問100個ico圖片的結果。從圖中我們可以看到,瀏覽器與伺服器之間建立了多條連接(確切的說是6條連接,上文有相關說明),我們還可以看出「灰色」部分(Stalled)占的時間比重較大,這部分代表等待發起請求的時間,由於http每個request是順序進行請求,因此在同一個連接上我們可以看出請求等待時間顯「瀑布狀」,而實際每個資源的下載時間(「藍色」部分)則佔比很小,可以理解為:當發出了request,下載數據的速度很快。

下圖是http2訪問100個ico圖片的訪問結果。從圖中可以看出對於同一個域名,只有一條連接,所有資源的申請都是通過這一條連接完成的。此外,從時間上看「灰色」的佔比很小,「綠色」(發出request後等待response的時間)和「藍色」的佔比則很大,這與之前http的情況截然相反。這是因為在http2中,所有請求被「打散」到不同的幀中進行申請的,因此等待request發送的時間(灰色部分)比較短。但所有請求和響應都「擠在」了一條「車道」上,因此等待response(綠色部分)以及下載最終資源(藍色部分)的時間則比較長。

下圖是http2-push訪問100個ico圖片的結果。從圖中可以看出,它與http2的現象很類似,不同之處在於有些資源是主動push的,這在一定程度上減少了客戶端發送請求的次數,縮短了訪問資源的時間。

下面的數據是使用http、http2、http2-push分別訪問10個、30個、50個、100個ico資源的時間。從圖中可以看出:普通http的訪問速度比http2和http2-push要快,http2-push開啟後要比http2快一點。因此,對於域名比較單一的網站,http2的效果不一定好於http,即多個連接的效果要好於單個連接的情況。添加主動push功能比沒有開啟的效果要好一些。因此,我們在選用http2的時候也要與我們的實際業務場景相結合。

推薦閱讀:

相关文章