原文地址: blog.wangriyu.wang/2018/05-HTT…javascript
維基百科關於 HTTP/2 的介紹,能夠看下定義和發展歷史:php
Wikicss
RFC 7540 定義了 HTTP/2 的協議規範和細節,本文的細節主要來自此文檔,建議先看一遍本文,再回過頭來照着協議大體過一遍 RFC,若是想深刻某些細節再仔細翻看 RFChtml
RFC7540java
一、TCP 鏈接數限制nginx
對於同一個域名,瀏覽器最多隻能同時建立 6~8 個 TCP 鏈接 (不一樣瀏覽器不同)。爲了解決數量限制,出現了 域名分片
技術,其實就是資源分域,將資源放在不一樣域名下 (好比二級子域名下),這樣就能夠針對不一樣域名建立鏈接並請求,以一種討巧的方式突破限制,可是濫用此技術也會形成不少問題,好比每一個 TCP 鏈接自己須要通過 DNS 查詢、三步握手、慢啓動等,還佔用額外的 CPU 和內存,對於服務器來講過多鏈接也容易形成網絡擁擠、交通阻塞等,對於移動端來講問題更明顯,能夠參考這篇文章: Why Domain Sharding is Bad News for Mobile Performance and Usersgit
在圖中能夠看到新建了六個 TCP 鏈接,每次新建鏈接 DNS 解析須要時間(幾 ms 到幾百 ms 不等)、TCP 慢啓動也須要時間、TLS 握手又要時間,並且後續請求都要等待隊列調度github
二、線頭阻塞 (Head Of Line Blocking) 問題golang
每一個 TCP 鏈接同時只能處理一個請求 - 響應,瀏覽器按 FIFO 原則處理請求,若是上一個響應沒返回,後續請求 - 響應都會受阻。爲了解決此問題,出現了 管線化 - pipelining 技術,可是管線化存在諸多問題,好比第一個響應慢仍是會阻塞後續響應、服務器爲了按序返回相應須要緩存多個響應占用更多資源、瀏覽器中途斷連重試服務器可能得從新處理多個請求、還有必須客戶端 - 代理 - 服務器都支持管線化web
三、Header 內容多,並且每次請求 Header 不會變化太多,沒有相應的壓縮傳輸優化方案
四、爲了儘量減小請求數,須要作合併文件、雪碧圖、資源內聯等優化工做,可是這無疑形成了單個請求內容變大延遲變高的問題,且內嵌的資源不能有效地使用緩存機制
五、明文傳輸不安全
幀是數據傳輸的最小單位,以二進制傳輸代替本來的明文傳輸,本來的報文消息被劃分爲更小的數據幀:
h1 和 h2 的報文對比:
圖中 h2 的報文是重組解析事後的,能夠發現一些頭字段發生了變化,並且全部頭字段均小寫
strict-transport-security: max-age=63072000; includeSubdomains
字段是服務器開啓 HSTS 策略,讓瀏覽器強制使用 HTTPS 進行通訊,能夠減小重定向形成的額外請求和會話劫持的風險
服務器開啓 HSTS 的方法是: 以 nginx 爲例,在相應站點的 server 模塊中添加
add_header Strict-Transport-Security "max-age=63072000; includeSubdomains" always;
便可
在 Chrome 中能夠打開
chrome://net-internals/#hsts
進入瀏覽器的 HSTS 管理界面,能夠增長 / 刪除 / 查詢 HSTS 記錄,好比下圖:
在 HSTS 有效期內且 TLS 證書仍有效,瀏覽器訪問 blog.wangriyu.wang 會自動加上 https:// ,而不須要作一次查詢重定向到 https
關於幀詳見: How does it work ?- 幀
在一個 TCP 鏈接上,咱們能夠向對方不斷髮送幀,每幀的 stream identifier 的標明這一幀屬於哪一個流,而後在對方接收時,根據 stream identifier 拼接每一個流的全部幀組成一整塊數據。 把 HTTP/1.1 每一個請求都看成一個流,那麼多個請求變成多個流,請求響應數據分紅多個幀,不一樣流中的幀交錯地發送給對方,這就是 HTTP/2 中的多路複用。
流的概念實現了單鏈接上多請求 - 響應並行,解決了線頭阻塞的問題,減小了 TCP 鏈接數量和 TCP 鏈接慢啓動形成的問題
因此 http2 對於同一域名只須要建立一個鏈接,而不是像 http/1.1 那樣建立 6~8 個鏈接:
關於流詳見: How does it work ?- 流
瀏覽器發送一個請求,服務器主動向瀏覽器推送與這個請求相關的資源,這樣瀏覽器就不用發起後續請求。
Server-Push 主要是針對資源內聯作出的優化,相較於 http/1.1 資源內聯的優點:
關於服務端推送詳見: How does it work ?- Server-Push
使用 HPACK 算法來壓縮首部內容
關於 HPACK 詳見: How does it work ?- HPACK
對於 HTTP/1 來講,是經過設置 tcp segment 裏的 reset flag 來通知對端關閉鏈接的。這種方式會直接斷開鏈接,下次再發請求就必須從新創建鏈接。HTTP/2 引入 RST_STREAM 類型的 frame,能夠在不斷開鏈接的前提下取消某個 request 的 stream,表現更好。
HTTP/2 裏的每一個 stream 均可以設置依賴 (Dependency) 和權重,能夠按依賴樹分配優先級,解決了關鍵請求被阻塞的問題
每一個 http2 流都擁有本身的公示的流量窗口,它能夠限制另外一端發送數據。對於每一個流來講,兩端都必須告訴對方本身還有足夠的空間來處理新的數據,而在該窗口被擴大前,另外一端只被容許發送這麼多數據。
關於流量控制詳見: How does it work ?- 流量控制
合併文件、內聯資源、雪碧圖、域名分片對於 HTTP/2 來講是沒必要要的,使用 h2 儘量將資源細粒化,文件分解地儘量散,不用擔憂請求數多
全部幀都是一個固定的 9 字節頭部 (payload 以前) 跟一個指定長度的負載 (payload):
+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+=+=============================================================+
| Frame Payload (0...) ...
+---------------------------------------------------------------+
複製代碼
Length
表明整個 frame 的長度,用一個 24 位無符號整數表示。除非接收者在 SETTINGS_MAX_FRAME_SIZE 設置了更大的值 (大小能夠是 2^14(16384) 字節到 2^24-1(16777215) 字節之間的任意值),不然數據長度不該超過 2^14(16384) 字節。頭部的 9 字節不算在這個長度裏Type
定義 frame 的類型,用 8 bits 表示。幀類型決定了幀主體的格式和語義,若是 type 爲 unknown 應該忽略或拋棄。Flags
是爲幀類型相關而預留的布爾標識。標識對於不一樣的幀類型賦予了不一樣的語義。若是該標識對於某種幀類型沒有定義語義,則它必須被忽略且發送的時候應該賦值爲 (0x0)R
是一個保留的比特位。這個比特的語義沒有定義,發送時它必須被設置爲 (0x0), 接收時須要忽略。Frame Payload
是主體內容,由幀類型決定共分爲十種類型的幀:
HEADERS
: 報頭幀 (type=0x1),用來打開一個流或者攜帶一個首部塊片斷DATA
: 數據幀 (type=0x0),裝填主體信息,能夠用一個或多個 DATA 幀來返回一個請求的響應主體PRIORITY
: 優先級幀 (type=0x2),指定發送者建議的流優先級,能夠在任何流狀態下發送 PRIORITY 幀,包括空閒 (idle) 和關閉 (closed) 的流RST_STREAM
: 流終止幀 (type=0x3),用來請求取消一個流,或者表示發生了一個錯誤,payload 帶有一個 32 位無符號整數的錯誤碼 (Error Codes),不能在處於空閒 (idle) 狀態的流上發送 RST_STREAM 幀SETTINGS
: 設置幀 (type=0x4),設置此 鏈接
的參數,做用於整個鏈接PUSH_PROMISE
: 推送幀 (type=0x5),服務端推送,客戶端能夠返回一個 RST_STREAM 幀來選擇拒絕推送的流PING
: PING 幀 (type=0x6),判斷一個空閒的鏈接是否仍然可用,也能夠測量最小往返時間 (RTT)GOAWAY
: GOWAY 幀 (type=0x7),用於發起關閉鏈接的請求,或者警示嚴重錯誤。GOAWAY 會中止接收新流,而且關閉鏈接前會處理完先前創建的流WINDOW_UPDATE
: 窗口更新幀 (type=0x8),用於執行流量控制功能,能夠做用在單獨某個流上 (指定具體 Stream Identifier) 也能夠做用整個鏈接 (Stream Identifier 爲 0x0),只有 DATA 幀受流量控制影響。初始化流量窗口後,發送多少負載,流量窗口就減小多少,若是流量窗口不足就沒法發送,WINDOW_UPDATE 幀能夠增長流量窗口大小CONTINUATION
: 延續幀 (type=0x9),用於繼續傳送首部塊片斷序列,見 首部的壓縮與解壓縮+---------------+
|Pad Length? (8)|
+---------------+-----------------------------------------------+
| Data (*) ...
+---------------------------------------------------------------+
| Padding (*) ...
+---------------------------------------------------------------+
複製代碼
Pad Length
: ? 表示此字段的出現時有條件的,須要設置相應標識 (set flag),指定 Padding 長度,存在則表明 PADDING flag 被設置Data
: 傳遞的數據,其長度上限等於幀的 payload 長度減去其餘出現的字段長度Padding
: 填充字節,沒有具體語義,發送時必須設爲 0,做用是混淆報文長度,與 TLS 中 CBC 塊加密相似,詳見 httpwg.org/specs/rfc75…DATA 幀有以下標識 (flags):
例子:
+---------------+
|Pad Length? (8)|
+-+-------------+-----------------------------------------------+
|E| Stream Dependency? (31) |
+-+-------------+-----------------------------------------------+
| Weight? (8) |
+-+-------------+-----------------------------------------------+
| Header Block Fragment (*) ...
+---------------------------------------------------------------+
| Padding (*) ...
+---------------------------------------------------------------+
複製代碼
Pad Length
: 指定 Padding 長度,存在則表明 PADDING flag 被設置E
: 一個比特位聲明流的依賴性是不是排他的,存在則表明 PRIORITY flag 被設置Stream Dependency
: 指定一個 stream identifier,表明當前流所依賴的流的 id,存在則表明 PRIORITY flag 被設置Weight
: 一個無符號 8 爲整數,表明當前流的優先級權重值 (1~256),存在則表明 PRIORITY flag 被設置Header Block Fragment
: header 塊片斷Padding
: 填充字節,沒有具體語義,做用與 DATA 的 Padding 同樣,存在則表明 PADDING flag 被設置HEADERS 幀有如下標識 (flags):
例子:
HTTP/2 裏的首部字段也是一個鍵具備一個或多個值。這些首部字段用於 HTTP 請求和響應消息,也用於服務端推送操做。
首部列表 (Header List) 是零個或多個首部字段 (Header Field) 的集合。當經過鏈接傳送時,首部列表經過壓縮算法(即下文 HPACK) 序列化成首部塊 (Header Block)。而後,序列化的首部塊又被劃分紅一個或多個叫作首部塊片斷 (Header Block Fragment) 的字節序列,並經過 HEADERS、PUSH_PROMISE,或者 CONTINUATION 幀進行有效負載傳送。
Cookie 首部字段須要 HTTP 映射特殊對待,見 8.1.2.5. Compressing the Cookie Header Field
一個完整的首部塊有兩種可能
必須將首部塊做爲連續的幀序列傳送,不能插入任何其餘類型或其餘流的幀。尾幀設置 END_HEADERS 標識表明首部塊結束,這讓首部塊在邏輯上等價於一個單獨的幀。接收端鏈接片斷重組首部塊,而後解壓首部塊重建首部列表。
一個 SETTINGS 幀的 payload 由零個或多個參數組成,每一個參數的形式以下:
+-------------------------------+
| Identifier (16) |
+-------------------------------+-------------------------------+
| Value (32) |
+---------------------------------------------------------------+
複製代碼
Identifier
: 表明參數類型,好比 SETTINGS_HEADER_TABLE_SIZE 是 0x1Value
: 相應參數的值在創建鏈接開始時雙方都要發送 SETTINGS 幀以代表本身期許對方應作的配置,對方接收後贊成配置參數便返回帶有 ACK 標識的空 SETTINGS 幀表示確認,並且鏈接後任意時刻任意一方也均可能再發送 SETTINGS 幀調整,SETTINGS 幀中的參數會被最新接收到的參數覆蓋
SETTINGS 幀做用於整個鏈接,而不是某個流,並且 SETTINGS 幀的 stream identifier 必須是 0x0,不然接收方會認爲錯誤 (PROTOCOL_ERROR)。
SETTINGS 幀包含如下參數:
SETTINGS 幀有如下標識 (flags):
例子:
實際抓包會發現 HTTP2 請求建立鏈接發送 SETTINGS 幀初始化前還有一個 Magic 幀 (創建 HTTP/2 請求的前言)。
在 HTTP/2 中,要求兩端都要發送一個鏈接前言,做爲對所使用協議的最終確認,並肯定 HTTP/2 鏈接的初始設置,客戶端和服務端各自發送不一樣的鏈接前言。
客戶端的前言內容 (對應上圖中編號 23 的幀) 包含一個內容爲 PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n
的序列加上一個能夠爲空的 SETTINGS 幀,在收到 101(Switching Protocols) 響應 (表明 upgrade 成功) 後發送,或者做爲 TLS 鏈接的第一個傳輸的應用數據。若是在預先知道服務端支持 HTTP/2 的狀況下啓用 HTTP/2 鏈接,客戶端鏈接前言在鏈接創建時發送。
服務端的前言 (對應上圖中編號 26 的幀) 包含一個能夠爲空的 SETTINGS 幀,在創建 HTTP/2 鏈接後做爲第一幀發送。詳見 HTTP/2 Connection Preface
發送完前言後雙方都得向對方發送帶有 ACK 標識的 SETTINGS 幀表示確認,對應上圖中編號 29 和 31 的幀。
請求站點的所有幀序列,幀後面的數字表明所屬流的 id,最後以 GOAWAY 幀關閉鏈接:
GOAWAY 幀帶有最大的那個流標識符 (好比圖中第 29 幀是最大流),對於發送方來講會繼續處理完不大於此數字的流,而後再真正關閉鏈接
流只是一個邏輯上的概念,表明 HTTP/2 鏈接中在客戶端和服務器之間交換的獨立雙向幀序列,每一個幀的 Stream Identifier 字段指明瞭它屬於哪一個流。
流有如下特性:
注意圖中的 send 和 recv 對象是指端點,不是指當前的流
全部流以「空閒」狀態開始。在這種狀態下,沒有任何幀的交換
其狀態轉換:
idle
流變成打開 open
狀態,其中 HEADERS 幀的 Stream Identifier 字段指明瞭流 id。一樣的 HEADERS 幀(帶有 END_STREAM )也可使一個流當即進入 half-closed 狀態。open
或者半關閉 (遠端) half-closed(remote)
狀態的流 (由客戶端發起的) 上發送 PUSH_PROMISE 幀,其中 PUSH_PROMISE 幀的 Promised Stream ID 字段指定了一個預示的新流 (由服務端發起),
idle
狀態進入被保留的 (本地) reserved(local)
狀態idle
狀態進入被保留的 (遠端) reserved(remote)
狀態在 3.2 - Starting HTTP/2 for "http" URIs 中介紹了一種特殊狀況:
客戶端發起一個 HTTP/1.1 請求,請求帶有 Upgrade 機制,想建立 h2c 鏈接,服務端贊成升級返回 101 響應。 升級以前發送的 HTTP/1.1 請求被分配一個流標識符 0x1,並被賦予默認優先級值。從客戶端到服務端這個流 1 隱式地轉爲 "half-closed" 狀態,由於做爲 HTTP/1.1 請求它已經完成了。HTTP/2 鏈接開始後,流 1 用於響應。詳細過程能夠看下文的 HTTP/2 的協議協商機制
此狀態下接收到 HEADERS 和 PRIORITY 之外的幀被視爲 PROTOCOL_ERROR
狀態圖中 send PP
和 recv PP
是指鏈接的雙方端點發送或接收了 PUSH_PROMISE,不是指某個空閒流發送或接收了 PUSH_PROMISE,是 PUSH_PROMISE 的出現促使一個預示的流從 idle
狀態轉爲 reserved
在下文 Server-Push 中會詳細介紹服務端推送的內容和 PUSH_PROMISE 的使用情形
PUSH_PROMISE 預示的流由 idle
狀態進入此狀態,表明準備進行 Server push
其狀態轉換:
half-closed(remote)
狀態,在客戶端置於半關閉 (本地) half-closed(local)
狀態,最後以攜帶 END_STREAM 的幀結束,這會將流置於關閉 closed
狀態reserved
轉爲 closed
reserved(local)
狀態下的流不能發送 HEADERS、RST_STREAM、PRIORITY 之外的幀,接收到 RST_STREAM、PRIORITY、WINDOW_UPDATE 之外的幀被視爲 PROTOCOL_ERROR
reserved(remote)
狀態下的流不能發送 RST_STREAM、WINDOW_UPDATE、PRIORITY 之外的幀,接收到 HEADERS、RST_STREAM、PRIORITY 之外的幀被視爲 PROTOCOL_ERROR
處於 open
狀態的流能夠被兩個對端用來發送任何類型的幀
其狀態轉換:
half-closed(local)
狀態;接收方會轉入 half-closed(remote)
狀態closed
狀態流是雙向的,半關閉表示這個流單向關閉了,local 表明本端到對端的方向關閉了,remote 表明對端到本端的方向關閉了
此狀態下的流不能發送 WINDOW_UPDATE、PRIORITY、RST_STREAM 之外的幀
當此狀態下的流收到帶有 END_STREAM 標識的幀或者任一方發送 RST_STREAM 幀,會轉爲 closed
狀態
此狀態下的流收到的 PRIORITY 幀用以調整流的依賴關係順序,能夠看下文的流優先級
此狀態下的流不會被對端用於發送幀,執行流量控制的端點再也不有義務維護接收方的流控制窗口。
一個端點在此狀態的流上接收到 WINDOW_UPDATE、PRIORITY、RST_STREAM 之外的幀,應該響應一個 STREAM_CLOSED 流錯誤
此狀態下的流能夠被端點用於發送任意類型的幀,且此狀態下該端點仍會觀察流級別的流控制的限制
當此狀態下的流發送帶有 END_STREAM 標識的幀或者任一方發送 RST_STREAM 幀,會轉爲 closed
狀態
表明流已關閉
此狀態下的流不能發送 PRIORITY 之外的幀,發送 PRIORITY 幀是調整那些依賴這個已關閉的流的流優先級,端點都應該處理 PRIORITY 幀,儘管若是該流從依賴關係樹中移除了也能夠忽略優先級幀
此狀態下在收到帶有 END_STREAM 標識的 DATA 或 HEADERS 幀後的一小段時間內 (period) 仍可能接收到 WINDOW_UPDATE 或 RST_STREAM 幀,由於在遠程對端接收並處理 RST_STREAM 或帶有 END_STREAM 標誌的幀以前,它可能會發送這些類型的幀。可是端點必須忽略接收到的 WINDOW_UPDATE 或 RST_STREAM
若是一個流發送了 RST_STREAM 幀後轉入此狀態,而對端接收到 RST_STREAM 幀時可能已經發送了或者處在發送隊列中,這些幀是不可撤銷的,發送 RST_STREAM 幀的端點必須忽略這些幀。
一個端點能夠限制 period 的長短,在 period 內接受的幀會忽略,超出 period 的幀被視爲錯誤。
一個端點發送了 RST_STREAM 幀後接收到流控制幀(好比 DATA),仍會計入流量窗口,即便這些幀會被忽略,由於對端確定是在接收到 RST_STREAM 幀前發送的流控制幀,對端會認爲流控制已生效
一個端點可能會在發送了 RST_STREAM 幀後收到 PUSH_PROMISE 幀,即使預示的流已經被重置 (reset),PUSH_PROMISE 幀也能使預示流變成 reserved
狀態。所以,須要 RST_STREAM 來關閉一個不想要的預示流。
PRIORITY 幀能夠被任意狀態的流發送和接收,未知類型的幀會被忽略
下面看兩個例子來理解流狀態:
(1)、Server 在 Client 發起的一個流上發送 PUSH_PROMISE 幀,其 Promised Stream ID 指定一個預示流用於後續推送,send PP 後這個預示流在服務端從 idle 狀態轉爲 reserve(local) 狀態,客戶端 recv PP 後這個流從 idle 狀態轉爲 reserve(remote) 狀態
(2)(3)、此時預示流處於保留狀態,客戶端若是選擇拒絕接受推送,能夠發送 RST 幀關閉這個流;服務端若是此時出問題了也能夠發送 RST 幀取消推送。無論哪一方發送或接收到 RST,此狀態都轉爲 closed
(4)、沒有出現重置說明推送仍有效,則服務端開始推送,首先發送的確定是響應的 HEADERS 首部塊,此時流狀態轉爲半關閉 half-closed(remote);客戶端接收到 HEADERS 後流狀態轉爲半關閉 half-closed(local)
(5)(6)、半關閉狀態下的流應該還會繼續推送諸如 DATA 幀、CONTINUATION 幀這樣的數據幀,若是這個過程碰到任一方發起重置,則流會關閉進入 closed 狀態
(7)、若是一切順利,資源隨着數據幀響應完畢,最後一幀會帶上 END_STREAM 標識表明這個流結束了,此時流轉爲 closed 狀態
(1)、客戶端發起請求,首先發送一個 HEADERS 幀,其 Stream Identifier 建立一個新流,此流從 idle 狀態轉爲 open 狀態
(2)(3)、若是客戶端取消請求能夠發送 RST 幀,服務端出錯也能夠發送 RST 幀,無論哪一方接收或發送 RST,流關閉進入 closed 狀態;
(4)、若是請求結束(END_STREAM),流轉爲半關閉狀態。假如是 GET 請求,通常 HEADERS 幀就是最後一幀,send H 後流會當即進入半關閉狀態。假如是 POST 請求,待數據傳完,最後一幀帶上 END_STREAM 標識,流轉爲半關閉
(5)(6)、客戶端半關閉後服務端開始返回響應,此時任一方接收或發送 RST,流關閉;
(7)、若是一切順利,等待響應結束(END_STREAM),流關閉
流 ID 是 31 位無符號整數,客戶端發起的流必須是奇數,服務端發起的流必須是偶數,0x0 保留爲鏈接控制消息不能用於創建新流。
HTTP/1.1 Upgrade to HTTP/2 時響應的流 ID 是 0x1,在升級完成以後,流 0x1 在客戶端會轉爲 half-closed (local)
狀態,所以這種狀況下客戶端不能用 0x1 初始化一個流
新創建的流的 ID 必須大於全部已使用過的數字,接收到一個錯誤大小的 ID 應該返回 PROTOCOL_ERROR 響應
使用一個新流時隱式地關閉了對端發起的 ID 小於當前流的且處於 idle
狀態的流,好比一個流發送一個 HEADERS 幀打開了 ID 爲 7 的流,但還從未向 ID 爲 5 的流發送過幀,則流 0x5 會在 0x7 發送完或接收完第一幀後轉爲 closed
狀態
一個鏈接內的流 ID 不能重用
客戶端能夠經過 HEADERS 幀的 PRIORITY 信息指定一個新創建流的優先級,其餘期間也能夠發送 PRIORITY 幀調整流優先級
設置優先級的目的是爲了讓端點表達它所指望對端在併發的多個流之間如何分配資源的行爲。更重要的是,當發送容量有限時,可使用優先級來選擇用於發送幀的流。
流能夠被標記爲依賴其餘流,所依賴的流完成後再處理當前流。每一個依賴 (dependency) 後都跟着一個權重 (weight),這一數字是用來肯定依賴於相同的流的可分配可用資源的相對比例
每一個流均可以顯示地依賴另外一個流,包含依賴關係表示優先將資源分配給指定的流(上層節點)而不是依賴流
一個不依賴於其餘流的流會指定 stream dependency 爲 0x0 值,由於不存在的 0x0 流表明依賴樹的根
一個依賴於其餘流的流叫作依賴流,被依賴的流是當前流的父級。若是被依賴的流不在當前依賴樹中(好比狀態爲 idle
的流),被依賴的流會使用一個默認優先級
當依賴一個流時,該流會添加進父級的依賴關係中,共享相同父級的依賴流不會相對於彼此進行排序,好比 B 和 C 依賴 A,新添加一個依賴流 D,BCD 的順序是不固定的:
A A
/ \ ==> /|\
B C B D C
複製代碼
獨佔標識 (exclusive) 容許插入一個新層級(新的依賴關係),獨佔標識致使該流成爲父級的惟一依賴流,而其餘依賴流變爲其子級,好比一樣插入一個新依賴流 E (帶有 exclusive):
A
A |
/|\ ==> E
B D C /|\
B D C
複製代碼
在依賴關係樹中,只有當一個依賴流所依賴的全部流(父級最高爲 0x0 的鏈)被關閉或者沒法繼續在上面執行,這個依賴流才應該被分配資源
全部依賴流都會分配一個 1~256 權重值
相同父級的依賴流按權重比例分配資源,好比流 B 依賴於 A 且權重值爲 4,流 C 依賴於 A 且權重值爲 12,當 A 再也不執行時,B 理論上能分配的資源只有 C 的三分之一
使用 PRIORITY 幀能夠調整流優先級
PRIORITY 幀內容與 HEADERS 幀的優先級模塊相同:
+-+-------------------------------------------------------------+
|E| Stream Dependency (31) |
+-+-------------+-----------------------------------------------+
| Weight (8) |
+-+-------------+
複製代碼
若是父級從新設置了優先級,則依賴流會隨其父級流一塊兒移動。若調整優先級的流帶有獨佔標識,會致使新的父流的全部子級依賴於這個流
若是一個流調整爲依賴本身的一個子級,則這個將被依賴的子級首先移至調整流的父級之下(即同一層),再移動那個調整流的整棵子樹,移動的依賴關係保持其權重
看下面這個例子: 第一個圖是初始關係樹,如今 A 要調整爲依賴 D,根據第二點,現將 D 移至 x 之下,再把 A 調整爲 D 的子樹(圖 3),若是 A 調整時帶有獨佔標識根據第一點 F 也歸爲 A 子級(圖 4)
x x x x
| / \ | |
A D A D D
/ \ / / \ / \ |
B C ==> F B C ==> F A OR A
/ \ | / \ /|\
D E E B C B C F
| | |
F E E
(intermediate) (non-exclusive) (exclusive)
複製代碼
當一個流從依賴樹中移除,它的子級能夠調整爲依賴被關閉流的父級(應該就是鏈接上一層節點),新的依賴權重將根據關閉流的權重以及流自身的權重從新計算。
從依賴樹中移除流會致使某些優先級信息丟失。資源在具備相同父級的流之間共享,這意味着若是這個集合中的某個流關閉或者阻塞,任何空閒容量將分配給最近的相鄰流。然而,若是此集合的共有依賴(即父級節點)從樹中移除,這些子流將與更上一層的流共享資源
一個例子: 流 A 和流 B 依賴相同父級節點,而流 C 和流 D 都依賴 A,在移除流 A 以前的一段時間內,A 和 D 都沒法執行(可能任務阻塞了),則 C 會分配到 A 的全部資源; 若是 A 被移除出樹了,A 的權重按比從新計算分配給 C 和 D,此時 D 仍舊阻塞,C 分配的資源相較以前變少了。對於同等的初始權重,C 獲取到的可用資源是三分之一而不是二分之一(爲何是三分之一?文檔中沒有說明細節,權重如何從新分配也不太清楚,下面是按個人理解解釋的)
X 的資源爲 1,ABCD 初始權重均爲 16,*號表明節點當前不可用,圖一中 C 和 B 各佔一半資源,而 A 移除後 CD 的權重從新分配變爲 8,因此圖二中 C 和 B 佔比變爲 1:2,R(C) 變爲 1/3
X(v:1.0) X(v:1.0)
/ \ /|\
/ \ / | \
*A B ==> / | \
(w:16) (w:16) / | \
/ \ C *D B
/ \ (w:8)(w:8)(w:16)
C *D
(w:16) (w:16)
R(C)=16/(16+16)=1/2 ==> R(C)=8/(8+16)=1/3
複製代碼
可能向一個流建立依賴關係的優先級信息還在傳輸中,那個流就已經關閉了。若是一個依賴流的依賴指向沒有相關優先級信息(即父節點無效),則這個依賴流會分配默認優先級,這可能會形成不理想的優先級,由於給流分配了不在預期的優先級。
爲了不上述問題,一個端點應該在流關閉後的一段時間內保留流的優先級調整狀態信息,此狀態保留時間越長,流被分配錯誤的或者默認的優先級可能性越低。
相似地,處於「空閒」狀態的流能夠被分配優先級或成爲其餘流的父節點。這容許在依賴關係樹中建立分組節點,從而實現更靈活的優先級表達式。空閒流以默認優先級開始
流優先級狀態信息的保留可能增長終端的負擔,所以這種狀態能夠被限制。終端可能根據負荷來決定保留的額外的狀態的數目;在高負荷下,能夠丟棄額外的優先級狀態來限制資源的任務。在極端狀況下,終端甚至能夠丟棄激活或者保留狀態流的優先級信息。若是使用了固定的限制,終端應當至少保留跟 SETTINGS_MAX_CONCURRENT_STREAMS 設置同樣大小的流狀態
全部流都是初始爲非獨佔地依賴於流 0x0。
Pushed 流初始依賴於相關的流(見 Server-Push)。
以上兩種狀況,流的權重都指定爲 16。
+---------------+
|Pad Length? (8)|
+-+-------------+-----------------------------------------------+
|R| Promised Stream ID (31) |
+-+-----------------------------+-------------------------------+
| Header Block Fragment (*) ...
+---------------------------------------------------------------+
| Padding (*) ...
+---------------------------------------------------------------+
複製代碼
Pad Length
: 指定 Padding 長度,存在則表明 PADDING flag 被設置R
: 保留的1bit位Promised Stream ID
: 31 位的無符號整數,表明 PUSH_PROMISE 幀保留的流,對於發送者來講該流標識符必須是可用於下一個流的有效值Header Block Fragment
: 包含請求首部域的首部塊片斷Padding
: 填充字節,沒有具體語義,做用與 DATA 的 Padding 同樣,存在則表明 PADDING flag 被設置PUSH_PROMISE 幀有如下標識 (flags):
結合上文關於 Server-Push 的流狀態轉換
PUSH_PROMISE 幀只能在對端(客戶端)發起的且流狀態爲 open 或者 half-closed (remote) 的流上發送
PUSH_PROMISE 幀準備推送的響應老是和來自於客戶端的請求相關聯。服務端在該請求所在的流上發送 PUSH_PROMISE 幀。PUSH_PROMISE 幀包含一個 Promised Stream ID,該流標識符是從服務端可用的流標識符裏選出來的。
若是服務端收到了一個對文檔的請求,該文檔包含內嵌的指向多個圖片文件的連接,且服務端選擇向客戶端推送那些額外的圖片,那麼在發送包含圖片連接的 DATA 幀以前發送 PUSH_PROMISE 幀能夠確保客戶端在發現內嵌的連接以前,可以知道有一個資源將要被推送過來。一樣地,若是服務端準備推送被首部塊引用的響應 (好比,在 Link 首部字段 裏的),在發送首部塊以前發送一個 PUSH_PROMISE 幀,能夠確保客戶端再也不請求那些資源
一旦客戶端收到了 PUSH_PROMISE 幀,並選擇接收被推送的響應,客戶端就不該該爲準備推送的響應發起任何請求,直到預示的流被關閉之後。
注意圖中推送的四個資源各預示了一個流 (Promised Stream ID),而發送 PUSH_PROMISE 幀的仍是在客戶端發起的請求流 (Stream Identifier = 1) 上,客戶端收到 PUSH_PROMISE 幀並選擇接收便不會對這四個資源發起請求,以後服務端會發起預示的流而後推送資源相關的響應
無論出於什麼緣由,若是客戶端決定再也不從服務端接收準備推送的響應,或者若是服務端花費了太長時間準備發送被預示的響應,客戶端能夠發送一個 RST_STREAM 幀,該幀可使用 CANCEL 或者 REFUSED_STEAM 碼,並引用被推送的流標識符。
server-push 須要服務端設置,並非說瀏覽器發起請求,與此請求相關的資源服務端就會自動推送
以 nginx 爲例,從版本 1.13.9 開始正式支持 hppt2 serverpush 功能,
在相應 server 或 location 模塊中加入 http2_push
字段加上相對路徑的文件便可在請求該資源時推送相關資源,好比個人博客設置以下,訪問首頁時有四個文件會由服務器主動推送過去而不須要客戶端請求:
server_name blog.wangriyu.wang;
root /blog;
index index.html index.htm;
location = /index.html {
http2_push /css/style.css;
http2_push /js/main.js;
http2_push /img/yule.jpg;
http2_push /img/avatar.jpg;
}
複製代碼
經過瀏覽器控制檯能夠查看 Push
響應:
也能夠用 nghttp
測試 push 響應 (* 號表明是服務端推送的):
上面 http2_push
的設置適合靜態資源,服務端事先知道哪些文件是客戶端須要的,而後選擇性推送
假如是後臺應用動態生成的文件(好比 json 文件),服務器事先不知道要推送什麼,能夠用 Link
響應頭來作自動推送
在 server 模塊中添加 http2_push_preload on;
server_name blog.wangriyu.wang;
root /blog;
index index.html index.htm;
http2_push_preload on;
複製代碼
而後設置響應頭 (add_header) 或者後臺程序生成數據文件返回時帶上響應頭 Link 標籤,好比
Link: </style.css>; as=style; rel=preload, </main.js>; as=script; rel=preload, </image.jpg>; as=image; rel=preload
複製代碼
nginx 會根據 Link 響應頭主動推送這些資源
更多nginx 官方介紹見 Introducing HTTP/2 Server Push with NGINX 1.13.9
看了這篇文章 HTTP/2 中的 Server Push 討論,發現 Server-Push 有個潛在的問題
Server-Push 知足條件時便會發起推送,但是客戶端已經有緩存了想發送 RST 拒收,而服務器在收到 RST 以前已經推送資源了,雖然這部分推送無效可是確定會佔用帶寬
好比我上面博客關於 http2_push 的配置,我每次打開首頁服務器都會推送那四個文件,而實際上瀏覽器知道本身有緩存使用的也是本地緩存,也就是說本地緩存未失效的期間內,服務器的 Server-Push 只是起到了佔用帶寬的做用
固然實際上對個人小站點來講影響並不大,可是若是網站須要大量推送的話,須要考慮並測試 Server-Push 是否會影響用戶的後續訪問
另外服務端能夠設置 Cookie 或者 Session 記錄訪問時間,而後以後的訪問判斷是否須要 Push;還有就是客戶端能夠限制 PUSH 流的數目,也能夠設置一個很低的流量窗口來限制 PUSH 發送的數據大小
至於哪些資源須要推送,在《web 性能權威指南》中就提到幾種策略,好比 Apache 的 mod_spdy 可以識別 X-Associated-Content 首部,當中列出了但願服務器推送的資源;另外網上有人已經作了基於 Referer 首部的中間件來處理 Server-Push;或者服務端能更智能的識別文檔,根據當前流量決定是否推送或者推送那些資源。相信之後會有更多關於 Server-Push 的實現和應用
多路複用的流會競爭 TCP 資源,進而致使流被阻塞。流控制機制確保同一鏈接上的流不會相互干擾。流量控制做用於單個流或整個鏈接。HTTP/2 經過使用 WINDOW_UPDATE 幀來提供流量控制。
流控制具備如下特徵:
+-+-------------------------------------------------------------+
|R| Window Size Increment (31) |
+-+-------------------------------------------------------------+
複製代碼
Window Size Increment 表示除了現有的流量控制窗口以外,發送端還能夠傳送的字節數。取值範圍是 1 到 2^31 - 1 字節。
WINDOW_UPDATE 幀能夠是針對一個流或者是針對整個鏈接的。若是是前者,WINDOW_UPDATE 幀的流標識符指明瞭受影響的流;若是是後者,流標識符爲 0 表示做用於整個鏈接。
流量控制功能只適用於被標識的、受流量控制影響的幀。文檔定義的幀類型中,只有 DATA 幀受流量控制影響。除非接收端不能再分配資源去處理這些幀,不然不受流量控制影響的幀必須被接收並處理。若是接收端不能再接收幀了,能夠響應一個 FLOW_CONTROL_ERROR 類型的流錯誤或者鏈接錯誤。
WINDOW_UPDATE 能夠由發送過帶有 END_STREAM 標誌的幀的對端發送。這意味着接收端可能會在 half-closed (remote) 或者 closed 狀態的流上收到 WINDOW_UPDATE 幀,接收端不能將其當作錯誤。
流量控制窗口是一個簡單的整數值,指出了准許發送端傳送的數據的字節數。窗口值衡量了接收端的緩存能力。
除非將其當作鏈接錯誤,不然當接收端收到 DATA 幀時,必須老是從流量控制窗口中減掉其長度(不包括幀頭的長度,並且兩個級別的控制窗口都要減)。即便幀有錯誤,這也是有必要的,由於發送端已經將該幀計入流量控制窗口,若是接收端沒有這樣作,發送端和接收端的流量控制窗口就會不一致。
發送端不能發送受流量控制影響的、其長度超出接收端告知的兩種級別的流量控制窗口可用空間的幀。即便這兩種級別的流量控制窗口都沒有可用空間了,也能夠發送長度爲 0、設置了 END_STREAM 標誌的幀(即空的 DATA 幀)。
當幀的接收端消耗了數據並釋放了流量控制窗口的空間時,能夠發送一個 WINDOW_UPDATE 幀。對於流級別和鏈接級別的流量控制窗口,須要分別發送 WINDOW_UPDATE 幀。
新建鏈接時,流和鏈接的初始窗口大小都是 2^16 - 1(65535) 字節。能夠經過設置鏈接前言中 SETTINGS 幀的 SETTINGS_INITIAL_WINDOW_SIZE 參數改變流的初始窗口大小,這會做用於全部流。而鏈接的初始窗口大小不能改,但能夠用 WINDOW_UPDATE 幀來改變流量控制窗口
,這是爲何鏈接前言每每帶有一個 WINDOW_UPDATE 幀的緣由。
除了改變還未激活的流的流量控制窗口外,SETTIGNS 幀還能夠改變已活躍的流 (處於 open 或 half-closed (remote) 狀態的流)的初始流量控制窗口的大小。也就是說,當 SETTINGS_INITIAL_WINDOW_SIZE 的值變化時,接收端必須調整它所維護的全部流的流量控制窗口的值,無論是以前就打開的流仍是還沒有打開的流。
改變 SETTINGS_INITIAL_WINDOW_SIZE 可能引起流量控制窗口的可用空間變成負值。發送端必須追蹤負的流量控制窗口,而且直到它收到了使流量控制窗口變成正值的 WINDOW_UPDATE 幀,才能發送新的 DATA 幀。
例如,若是鏈接一創建客戶端就當即發送 60KB 的數據,而服務端卻將初始窗口大小設置爲 16KB,那麼客戶端一收到 SETTINGS 幀,就會將可用的流量控制窗口從新計算爲 -44KB。客戶端保持負的流量控制窗口,直到 WINDOW_UPDATE 幀將窗口值恢復爲正值,客戶端才能夠繼續發送數據。
若是改變 SETTINGS_INITIAL_WINDOW_SIZE 致使流量控制窗口超出了最大值,一端必須 將其當作類型爲 FLOW_CONTROL_ERROR 的鏈接錯誤
若是接收端但願使用比當前值小的流量控制窗口,能夠發送一個新的 SETTINGS 幀。可是,接收端必須準備好接收超出該窗口值的數據,由於可能在收到 SETTIGNS 幀以前,發送端已經發送了超出該較小窗口值的數據。
流量控制的定義是用來保護端點在資源約束條件下的操做。例如,一個代理須要在不少鏈接之間共享內存,也有可能有緩慢的上游鏈接和快速的下游鏈接。流量控制解決了接收方沒法在一個流上處理數據,但仍但願繼續處理同一鏈接中的其餘流的狀況。
不須要此功能的部署能夠通告最大大小 (2^31 - 1) 的流量控制窗口,而且能夠經過在收到任何數據時發送 WINDOW_UPDATE 幀來維護此窗口大小保持不變。這能夠有效禁用接受方的流控制。相反地,發送方老是受控於接收方通告的流控制窗口的限制。
資源約束下(例如內存)的調度可使用流量來限制一個對端能夠消耗的內存量。須要注意的是若是在不知道帶寬延遲積的時候啓用流量控制可能致使沒法最優的利用可用的網絡資源 (RFC1323)。
即使是對當前的網絡延遲乘積有充分的認識,流量控制的實現也可能很複雜。當使用流量控制時,接收端必須及時地從 TCP 接收緩衝區讀取數據。這樣作可能致使在一些例如 WINDOW_UPDATE 的關鍵幀在 HTTP/2 不可用時致使死鎖。可是流量控制能夠保證約束資源能在不須要減小鏈接利用的狀況下獲得保護。
客戶端使用 HTTP Upgrade 機制請求升級,HTTP2-Settings 首部字段是一個專用於鏈接的首部字段,它包含管理 HTTP/2 鏈接的參數(使用 Base64 編碼),其前提是假設服務端會接受升級請求
GET / HTTP/1.1
Host: server.example.com
Connection: Upgrade, HTTP2-Settings
Upgrade: h2c
HTTP2-Settings: <base64url encoding of HTTP/2 SETTINGS payload>
複製代碼
服務器若是支持 http/2 並贊成升級,則轉換協議,不然忽略
HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: h2c
複製代碼
此時潛在的存在一個流 0x1,客戶端上這個流在完成 h1 請求後便轉爲 half-closed
狀態,服務端會用這個流返回響應
注意圖中第一個響應所在的流是 0x1,與上文所說的一致
目前瀏覽器只支持 TLS 加密下的 HTTP/2 通訊,因此上述狀況在瀏覽器中目前是不可能碰到的,圖中顯示的是 nghttp 客戶端發起的請求
TLS 加密中在 Client-Hello 和 Server-Hello 的過程當中經過 ALPN 進行協議協商
應用層協議協商在 TLS 握手第一步的擴展中,Client Hello 中客戶端指定 ALPN Next Protocol 爲 h2 或者 http/1.1 說明客戶端支持的協議
服務端若是在 Server Hello 中選擇 h2 擴展,說明協商協議爲 h2,後續請求響應跟着變化;若是服務端未設置 http/2 或者不支持 h2,則繼續用 http/1.1 通訊
196: TLS 握手第一步 Client Hello,開始協議協商,且此處帶上了 Session Ticket
200: Server Hello 贊成使用 h2,並且客戶端的會話票證有效,恢復會話,握手成功
202: 客戶端也恢復會話,開始加密後續消息
205: 服務端發起一個鏈接前言 (SETTINGS),SETTINGS 幀中設置了最大並行流數量、初始窗口大小、最大幀長度,而後 (WINDOW_UPDATE) 擴大窗口大小
310: 客戶端也發送一個鏈接前言 Magic,並初始化設置 (SETTINGS),SETTINGS 幀中設置了 HEADER TABLE 大小、初始窗口大小、最大並行流數量,而後 (WINDOW_UPDATE) 擴大窗口大小
311: 客戶端發送完鏈接前言後能夠當即跟上一個請求,GET / (HEADERS[1]),並且這個 HEADERS 幀還帶有 END_STREAM,這會使流 1 從 idle 狀態當即轉爲 half-closed(local) 狀態 (open 是中間態)
311: 此消息中還包含一個客戶端發送給服務端的帶 ACK 的 SETTINGS 幀
312: 服務端也響應帶 ACK 的 SETTINGS 幀
321: 服務端在流 1 (此時狀態爲 half-closed(remote)) 上發送了四個 PUSH_PROMISE 幀,它們分別保留了流 二、四、六、8 用於後續推送,
321: 此消息中還返回了上面請求的響應 (HEADERS - DATA),最後 DATA 帶上 END_STREAM,流 1 從 half-closed 轉爲 closed
329: 調整流優先級,依賴關係: 8 -> 6 -> 4 -> 2 -> 1 (都帶有獨佔標誌,並且權重均爲 110)
342: 流 1 關閉後,流 2 獲得分配資源,服務器開始推送,數據由兩個 DATA 幀返回
344: 流 2 結束,開始推送流 4
356: 調整依賴關係
1 1 1 1(w: 110)
| | | |
2 2 2 2(w: 110)
| | | |
4 ==> 4 ==> 6 ==> 6(w: 147)
| | | |
6 8 4 8(w: 147)
| | | |
8 6 8 4(w: 110)
複製代碼
36七、36九、372: 推送 6 和 8 的流數據
377: 發起一個請求,打開流 3,其中客戶端發起的請求都是依賴流 0x0
以後都是一樣的套路完成請求 - 響應,最後以 GOAWAY 幀關閉鏈接結束
上圖來自 Ilya Grigorik 的 PPT - HTTP/2 is here, let's optimize!
能夠清楚地看到 HTTP2 頭部使用的也是鍵值對形式的值,並且 HTTP1 當中的請求行以及狀態行也被分割成鍵值對,還有全部鍵都是小寫,不一樣於 HTTP1。除此以外,還有一個包含靜態索引表和動態索引表的索引空間,實際傳輸時會把頭部鍵值表壓縮,使用的算法即 HPACK,其原理就是匹配當前鏈接存在的索引空間,若某個鍵值已存在,則用相應的索引代替首部條目,好比 「:method: GET」 能夠匹配到靜態索引中的 index 2,傳輸時只須要傳輸一個包含 2 的字節便可;若索引空間中不存在,則用字符編碼傳輸,字符編碼能夠選擇哈夫曼編碼,而後分狀況判斷是否須要存入動態索引表中
靜態索引表是固定的,對於客戶端服務端都同樣,目前協議商定的靜態索引包含 61 個鍵值,詳見 Static Table Definition - RFC 7541
好比前幾個以下
索引 | 字段值 | 鍵值 |
---|---|---|
index | Header Name | Header Value |
1 | :authority | |
2 | :method | GET |
3 | :method | POST |
4 | :path | / |
5 | :path | /index.html |
6 | :scheme | http |
7 | :scheme | https |
8 | :status | 200 |
動態索引表是一個 FIFO 隊列維護的有空間限制的表,裏面含有非靜態表的索引。 動態索引表是須要鏈接雙方維護的,其內容基於鏈接上下文,一個 HTTP2 鏈接有且僅有一份動態表。 當一個首部匹配不到索引時,能夠選擇把它插入動態索引表中,下次同名的值就可能會在表中查到索引並替換。 可是並不是全部首部鍵值都會存入動態索引,由於動態索引表是有空間限制的,最大值由 SETTING 幀中的 SETTINGS_HEADER_TABLE_SIZE (默認 4096 字節) 設置
大小均以字節爲單位,動態索引表的大小等於全部條目大小之和,每一個條目的大小 = 字段長度 + 鍵值長度 + 32
這個額外的 32 字節是預估的條目開銷,好比一個條目使用了兩個 64-bit 指針分別指向字段和鍵值,並使用兩個 64-bit 整數來記錄字段和鍵值的引用次數
golang 實現也是加上了 32: golang.org/x/net/http2…
SETTING 幀規定了動態表的最大大小,但編碼器能夠另外選擇一個比 SETTINGS_HEADER_TABLE_SIZE 小的值做爲動態表的有效負載量
修改最大動態表容量能夠發送一個 dynamic table size update
信號來更改:
+---+---+---+---+---+---+---+---+
| 0 | 0 | 1 | Max size (5+) |
+---+---------------------------+
複製代碼
前綴 001 表明此字節爲 dynamic table size update
信號,後面使用 N=5 的整數編碼方法表示新的最大動態表容量(不能超過 SETTINGS_HEADER_TABLE_SIZE),其計算方法下文會介紹。
須要注意的是這個信號必須在首部塊發送以前或者兩個首部塊傳輸的間隔發送,能夠經過發送一個 Max size 爲 0 的更新信號來清空現有動態表
關於動態索引表如何管理的,推薦看下 golang 的實現: golang.org/x/net/http2…,經過代碼能更明白這個過程
由靜態索引表和動態索引表能夠組成一個索引地址空間:
<---------- Index Address Space ---------->
<-- Static Table --> <-- Dynamic Table -->
+---+-----------+---+ +---+-----------+---+
| 1 | ... | s | |s+1| ... |s+k|
+---+-----------+---+ +---+-----------+---+
⍋ |
| ⍒
Insertion Point Dropping Point
複製代碼
目前 s 就是 61,而有新鍵值要插入動態索引表時,從 index 62 開始插入隊列,因此動態索引表中索引從小到大依次存着重新到舊的鍵值
HPACK 編碼使用兩種原始類型: 無符號可變長度整數和八位字節表示的字符串,相應地規定了如下兩種編碼方式
一個整數編碼能夠用於表示字段索引值、首部條目索引值或者字符串長度。 一個整數編碼含兩部分: 一個前綴字節和可選的後跟字節序列,只有前綴字節不足以表達整數值時才須要後跟字節,前綴字節中可用比特位 N 是整數編碼的一個參數
好比下面所示的是一個 N=5 的整數編碼(前三比特用於其餘標識),若是咱們要編碼的整數值小於 2^N - 1,直接用一個前綴字節表示便可,好比 10 就用 ???01010
表示
+---+---+---+---+---+---+---+---+
| ? | ? | ? | Value |
+---+---+---+-------------------+
複製代碼
若是要編碼的整數值 X 大於等於 2^N - 1,前綴字節的可用比特位都設成 1,而後把 X 減去 2^N - 1 獲得值 R,並用一個或多個字節序列表示 R,字節序列中每一個字節的最高有效位 (msb) 用於表示是否結束,msb 設爲 0 時表明是最後一個字節。具體編碼看下面的僞代碼和例子
+---+---+---+---+---+---+---+---+
| ? | ? | ? | 1 1 1 1 1 |
+---+---+---+-------------------+
| 1 | Value-(2^N-1) LSB |
+---+---------------------------+
...
+---+---------------------------+
| 0 | Value-(2^N-1) MSB |
+---+---------------------------+
複製代碼
編碼:
if I < 2^N - 1, encode I on N bits
else
encode (2^N - 1) on N bits
I = I - (2^N - 1)
while I >= 128
encode (I % 128 + 128) on 8 bits
I = I / 128
encode I on 8 bits
複製代碼
解碼:
decode I from the next N bits
if I < 2^N - 1, return I
else
M = 0
repeat
B = next octet
I = I + (B & 127) * 2^M
M = M + 7
while B & 128 == 128
return I
複製代碼
好比使用 N=5 的整數編碼表示 1337:
1337 大於 31 (2^5 - 1), 將前綴字節後五位填滿 1
I = 1337 - (2^5 - 1) = 1306
I 仍然大於 128, I % 128 = 26, 26 + 128 = 154
154 二進制編碼: 10011010, 這便是第一個後跟字節
I = 1306 / 128 = 10, I 小於 128, 循環結束
將 I 編碼成二進制: 00001010, 這便是最後一個字節
+---+---+---+---+---+---+---+---+
| X | X | X | 1 | 1 | 1 | 1 | 1 | Prefix = 31, I = 1306
| 1 | 0 | 0 | 1 | 1 | 0 | 1 | 0 | 1306 >= 128, encode(154), I=1306/128=10
| 0 | 0 | 0 | 0 | 1 | 0 | 1 | 0 | 10 < 128, encode(10), done
+---+---+---+---+---+---+---+---+
複製代碼
解碼時讀取第一個字節,發現後五位 (11111) 對應的值 I 等於 31(>= 2^N - 1),說明還有後跟字節;令 M=0,繼續讀下一個字節 B,I = I + (B & 127) * 2^M = 31 + 26 * 1 = 57,M = M + 7 = 7,最高有效位爲 1,表示字節序列未結束,B 指向下一個字節;I = I + (B & 127) * 2^M = 57 + 10 * 128 = 1337,最高有效位爲 0,表示字節碼結束,返回 I
這裏也能夠這樣處理 1306: 1306 = 0x51a = (0101 0001 1010)B,將 bit 序列從低到高按 7 個一組分組,則有第一組 001 1010,第二組 000 1010,加上最高有效位 0/1 便與上面的後跟字節對應
一個字符串可能表明 Header 條目的字段或者鍵值。字符編碼使用字節序列表示,要麼直接使用字符的八位字節碼要麼使用哈夫曼編碼。
+---+---+---+---+---+---+---+---+
| H | String Length (7+) |
+---+---------------------------+
| String Data (Length octets) |
+-------------------------------+
複製代碼
RFC 7541 給出了一份字符的哈夫曼編碼表: Huffman Code,這是基於大量 HTTP 首部數據生成的哈夫曼編碼。
使用哈夫曼編碼可能存在編碼不是整字節的,會在後面填充 1 使其變成整字節
好比下面的例子:
:authority: blog.wangriyu.wang
首部對應的編碼爲:
41 8e 8e 83 cc bf 81 d5 35 86 f5 6a fe 07 54 df
複製代碼
Literal Header Field with Incremental Indexing — Indexed Name
的編碼格式見下文
41 (0100 0001) 表示字段存在索引值 1,即對應靜態表中第一項 :authority
8e (1000 1110) 最高有效位爲 1 表示鍵值使用哈夫曼編碼,000 1110 表示字節序列長度爲 14
後面 8e 83 cc bf 81 d5 35 86 f5 6a fe 07 54 df
是一段哈夫曼編碼序列
由哈夫曼編碼表可知 100011 -> 'b', 101000 -> 'l', 00111 -> 'o', 100110 -> 'g', 010111 -> '.', 1111000 -> 'w', 00011 -> 'a', 101010 -> 'n', 100110 -> 'g', 101100 -> 'r', 00110 -> 'i', 1111010 -> 'y', 101101 -> 'u'
8e 83 cc bf 81 d5 35 86 f5 6a fe 07 54 df
|
⍒
1000 1110 1000 0011 1100 1100 1011 1111 1000 0001 1101 0101 0011 0101 1000 0110 1111 0101 0110 1010 1111 1110 0000 0111 0101 0100 1101 1111
|
⍒
100011 101000 00111 100110 010111 1111000 00011 101010 100110 101100 00110 1111010 101101 010111 1111000 00011 101010 100110 11111
|
⍒
blog.wangriyu.wang 最後 11111 用於填充
複製代碼
如今開始是 HPACK 真正的編解碼規範
Indexed Header Field
以 1 開始爲標識,能在索引空間匹配到索引的首部會替換成這種形式,後面的 index 使用上述的整數編碼方式且 N = 7。 好比 :method: GET
能夠用 0x82,即 10000010 表示
+---+---+---+---+---+---+---+---+
| 1 | Index (7+) |
+---+---------------------------+
複製代碼
還沒有被索引的首部有三種表示形式,第一種會添加進索引,第二種對於當前跳來講不會添加進索引,第三種絕對不被容許添加進索引
以 01 開始爲標識,此首部會加入到解碼後的首部列表 (Header List) 中而且會把它做爲新條目插入到動態索引表中
Literal Header Field with Incremental Indexing — Indexed Name
若是字段已經存在索引,但鍵值未被索引,好比首部 :authority: blog.wangriyu.wang
的字段 :authority
已存在索引但鍵值 blog.wangriyu.wang
不存在索引,則會替換成以下形式 (index 使用 N=6 的整數編碼表示)
+---+---+---+---+---+---+---+---+
| 0 | 1 | Index (6+) |
+---+---+-----------------------+
| H | Value Length (7+) |
+---+---------------------------+
| Value String (Length octets) |
+-------------------------------+
複製代碼
Literal Header Field with Incremental Indexing — New Name
若是字段和鍵值均未被索引,好比 upgrade-insecure-requests: 1
,則會替換成以下形式
+---+---+---+---+---+---+---+---+
| 0 | 1 | 0 |
+---+---+-----------------------+
| H | Name Length (7+) |
+---+---------------------------+
| Name String (Length octets) |
+---+---------------------------+
| H | Value Length (7+) |
+---+---------------------------+
| Value String (Length octets) |
+-------------------------------+
複製代碼
以 0000 開始爲標識,此首部會加入到解碼後的首部列表中,但不會插入到動態索引表中
Literal Header Field without Indexing — Indexed Name
若是字段已經存在索引,但鍵值未被索引,則會替換成以下形式 (index 使用 N=4 的整數編碼表示)
+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | 0 | Index (4+) |
+---+---+-----------------------+
| H | Value Length (7+) |
+---+---------------------------+
| Value String (Length octets) |
+-------------------------------+
複製代碼
Literal Header Field without Indexing — New Name
若是字段和鍵值均未被索引,則會替換成以下形式。好比 strict-transport-security: max-age=63072000; includeSubdomains
+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | 0 | 0 |
+---+---+-----------------------+
| H | Name Length (7+) |
+---+---------------------------+
| Name String (Length octets) |
+---+---------------------------+
| H | Value Length (7+) |
+---+---------------------------+
| Value String (Length octets) |
+-------------------------------+
複製代碼
這與上一種首部相似,只是標識爲 0001,首部也是會添加進解碼後的首部列表中但不會插入動態更新表。
區別在於這類首部發出是什麼格式表示,接收也是同樣的格式,做用於每一跳 (hop),若是中間經過代理,代理必須原樣轉發不能另行編碼。
而上一種首部只是做用當前跳,經過代理後可能會被從新編碼
golang 實現中使用一個 Sensitive
標明哪些字段是絕對不添加索引的: golang.org/x/net/http2…
RFC 文檔中詳細說明了這麼作的緣由: Never-Indexed Literals
表示形式除了標識其餘都跟上一種首部同樣:
Literal Header Field Never Indexed — Indexed Name
+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | 1 | Index (4+) |
+---+---+-----------------------+
| H | Value Length (7+) |
+---+---------------------------+
| Value String (Length octets) |
+-------------------------------+
複製代碼
Literal Header Field Never Indexed — New Name
+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | 1 | 0 |
+---+---+-----------------------+
| H | Name Length (7+) |
+---+---------------------------+
| Name String (Length octets) |
+---+---------------------------+
| H | Value Length (7+) |
+---+---------------------------+
| Value String (Length octets) |
+-------------------------------+
複製代碼
以 001 開始爲標識,做用前面已經提過
+---+---+---+---+---+---+---+---+
| 0 | 0 | 1 | Max size (5+) |
+---+---------------------------+
複製代碼
能夠發送 Max Size 爲 0 的更新來清空動態索引表
RFC 中給出了不少實例 Examples - RFC 7541,推薦看一遍加深理解
網站啓用 h2 的先後對比,使用 WebPageTest 作的測試,第一張是 h1,第二張是 h2:
nginx 開啓 HTTP2 只需在相應的 HTTPS 設置後加上 http2
便可
listen [::]:443 ssl http2 ipv6only=on;
listen 443 ssl http2;
複製代碼
一、開啓壓縮
配置 gzip 等可使傳輸內容更小,傳輸速度更快
例如 nginx 能夠再 http 模塊中加入如下字段,其餘字段和詳細解釋能夠谷歌
gzip on; // 開啓
gzip_min_length 1k;
gzip_comp_level 1; // 壓縮級別
gzip_types text/plain application/javascript application/x-javascript application/octet-stream application/json text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png font/ttf font/otf image/svg+xml; // 須要壓縮的文件類型
gzip_vary on;
gzip_disable "MSIE [1-6]\.";
複製代碼
二、使用緩存
給靜態資源設置一個緩存期是很是有必要的,關於緩存見另外一篇博文 HTTP Message
例如 nginx 在 server 模塊中添加如下字段能夠設置緩存時間
location ~* ^.+\.(ico|gif|jpg|jpeg|png|moc|mtn|mp3|mp4|mov)$ {
access_log off;
expires 30d;
}
location ~* ^.+\.(css|js|txt|xml|swf|wav|json)$ {
access_log off;
expires 5d;
}
location ~* ^.+\.(html|htm)$ {
expires 24h;
}
location ~* ^.+\.(eot|ttf|otf|woff|svg)$ {
access_log off;
expires 30d;
}
複製代碼
三、CDN 加速
CDN 的好處是就近訪問,延遲低,訪問快
四、減小 DNS 查詢
每一個域名都須要 DNS 查詢,通常須要幾毫秒到幾百毫秒,移動環境下會更慢。DNS 解析完成以前,請求會被阻塞。減小 DNS 查詢也是優化項之一
瀏覽器的 DNS Prefetching 技術也是一種優化手段
五、減小重定向
重定向可能引入新的 DNS 查詢、新的 TCP 鏈接以及新的 HTTP 請求,因此減小重定向也很重要。
瀏覽器基本都會緩存經過 301 Moved Permanently 指定的跳轉,因此對於永久性跳轉,能夠考慮使用狀態碼 301。對於啓用了 HTTPS 的網站,配置 HSTS 策略,也能夠減小從 HTTP 到 HTTPS 的重定向
一、域名分片
HTTP/2 對於同一域名使用一個 TCP 鏈接足矣,過多 TCP 鏈接浪費資源並且效果不見得必定好
並且資源分域會破壞 HTTP/2 的優先級特性,還會下降頭部壓縮效果
二、資源合併
資源合併會不利於緩存機制,並且單文件過大對於 HTTP/2 的傳輸很差,儘可能作到細粒化更有利於 HTTP/2 傳輸
三、資源內聯
HTTP/2 支持 Server-Push,相比較內聯優點更大效果更好
並且內聯的資源不能有效緩存
若是有共用,多頁面內聯也會形成浪費
使用 HTTP/2 儘量用最少的鏈接,由於同一個鏈接上產生的請求和響應越多,動態字典積累得越全,頭部壓縮效果也就越好,並且多路複用效率高,不會像多鏈接那樣形成資源浪費
爲此須要注意如下兩點:
因此使用相同的 IP 和證書部署 Web 服務是目前最好的選擇,由於這讓支持 HTTP/2 的終端能夠複用同一個鏈接,實現 HTTP/2 協議帶來的好處;而只支持 HTTP/1.1 的終端則會不一樣域名創建不一樣鏈接,達到同時更多併發請求的目的
好比 Google 一系列網站都是用的同一個證書:
可是這好像也會形成一個問題,我使用 nginx 搭建的 webserver,有三個虛擬主機,它們共用一套證書,其中兩個我顯示地配置了 http2,而剩下一個我並無配置 http2,結果我訪問未配置 http2 的站點時也變成了 http2。
先比較一下 h1 和 h2 的頁面加載時間,圖中綠色表明發起請求收到響應等待負載的時間,藍色表明下載負載的時間:
能夠發現 h2 加載時間還比 h1 慢一點,特別是碰到大圖片時差異更明顯
這篇文章對不一樣場景下 h1 和 h2 加載圖片作了測試: Real–world HTTP/2: 400gb of images per day
其結果是:
對一個典型的富圖像,延遲限制 (latency–bound) 的界面來講。使用一個高速,低延遲的鏈接,視覺完成度 (visual completion) 平均會快 5%。
對一個圖像極其多,帶寬限制 (bandwidth–bound) 的頁面來講。使用一樣的鏈接,視覺完成度平均將會慢 5–10%,但頁面的總體加載時間實際是減小了,由於得益於鏈接延遲少。
一個高延遲,低速度的鏈接(好比移動端的慢速 3G) 會對頁面的視覺完成形成極大的延遲,但 h2 的視覺完成度明顯更高更好。
在全部的測試中,均可以看到: h2 使總體頁面的加載速度提升了,而且在初次繪製 (initial render) 上作的更好,雖然第二種狀況中視覺完成度略微降低,但整體效果仍是好的
視覺完成度降低的緣由是由於沒有 HTTP/1.x 同時鏈接數量的限制,h2 能夠同時發起多張圖片的請求,服務器能夠同時響應圖片的負載,能夠從下面的動圖中看到
一旦圖片下載完成,瀏覽器就會繪製出它們,然而,小圖片下載後會渲染地更快,可是若是一個大圖片剛好是初始的視圖,那就會花費較長的時間加載,延遲視覺上的完成度。
上面的動圖是在 Safari 上的測試結果,圖片最後都下載成功了,而我在 Chrome 上測試時後面的部分圖片直接掛了,都報 ERR_SPDY_PROTOCOL_ERROR
錯誤,並且是百分百復現
去看了下 ERR_SPDY_PROTOCOL_ERROR
出在哪,發現是 Server reset stream,應該是哪出錯了致使流提早終止
而後再研究了一下 HTTP/2 的幀序列,發出的請求都在 629 號消息中響應成功了,可是返回的數據幀只有流 15 上的,實際收到的圖片又不止流 15 對應的圖片,這是爲何?
後面我繼續測試發現連續請求幾張大圖片,雖然 HEADERS 幀都打開的是不一樣的流,返回的響應的 HEADERS 幀也仍是對應前面的流 ID,可是響應的 DATA 幀都是從第一個打開的流上返回的。
若是是小圖片的話,一個請求響應事後這個流就關閉了,下一張小圖是在其本身對應的流上返回的。只有連續幾張大圖會出現上述情形,這個機制很奇怪,我暫時尚未找到解釋的文檔。
至於 chrome 爲何出錯呢,看一下 TCP 報文就會發現全部數據在一個鏈接上發送,到後面 TCP 包會出現各類問題,丟包、重傳、失序、重包等等,不清楚 Safari 是否也是這樣,由於 wireshark 只能解 chrome 的包解不了 Safari 的包
《web 性能權威指南》中說起 HTTP/2 中一個 TCP 可能會形成的問題: 雖然消除了 HTTP 隊首阻塞現象,但 TCP 層次上仍存在隊首阻塞問題;若是 TCP 窗口縮放被禁用,那帶寬延遲積效應可能會限制鏈接的吞吐量;丟包時 TCP 擁塞窗口會縮小;
TCP 是一方面緣由,還有另外一方面應該是瀏覽器策略問題,估計也是 chrome bug,對比兩張動圖你會發現,safari 接收負載是輪流接收,咱們幾個接收一點而後換幾我的接收,直到全部都接受完;而 chrome 則是按順序接收,這個接收完才輪到下一個接收,結果後面的圖片可能長時間未響應就掛了。
漸進式 jpg 代替普通 jpg 有利於提升視覺完成度,並且文件更小:
輸入 convert --version
看看是否已安裝 ImageMagic,若是沒有先安裝: Mac 能夠用 brew install imagemagick
,Centos 能夠用 yum install imagemagick
檢測是否爲 progressive jpeg,若是輸出 None 說明不是 progressive jpeg;若是輸出 JPEG 說明是 progressive jpeg:
$ identify -verbose filename.jpg | grep Interlace
複製代碼
將 basic jpeg 轉換成 progressive jpeg,interlace 參數:
$ convert -strip -interlace Plane source.jpg destination.jpg // 還能夠指定質量 -quality 90
// 批量處理
$ for i in ./*.jpg; do convert -strip -interlace Plane $i $i; done
複製代碼
也能夠轉換 PNG 和 GIF,可是我試過 convert -strip -interlace Plane source.png destination.png
但轉換後的圖片每每會更大,不推薦這麼用,能夠 convert source.png destination.jpg
ImageMagic 還有不少強大的功能
// 圖片縮放
$ convert -resize 50%x50% source.jpg destination.jpg
// 圖片格式轉換
$ convert source.jpg destination.png
// 配合 find 命令,將當前目錄下大於 100kb 的圖片按 75% 質量進行壓縮
$ find -E . -iregex '.*\.(jpg|png|bmp)' -size +100k -exec convert -strip +profile 「*」 -quality 75 {} {} \;
複製代碼
png 壓縮推薦使用 pngquant
另外 photoshop 保存圖片時也能夠設置漸進或交錯:
漸進式圖片:選擇圖片格式爲 JPEG => 選中「連續」
交錯式圖片:選擇圖片格式爲 PNG/GIF => 選中「交錯」
SPDY 是 HTTP2 的前身,大部分特性與 HTTP2 保持一致,包括服務器端推送,多路複用和幀做爲傳輸的最小單位。但 SPDY 與 HTTP2 也有一些實現上的不一樣,好比 SPDY 的頭部壓縮使用的是 DEFLATE 算法,而 HTTP2 使用的是 HPACK 算法,壓縮率更高。
Google 的 QUIC(Quick UDP Internet Connections) 協議,繼承了 SPDY 的特色。QUIC 是一個 UDP 版的 TCP + TLS + HTTP/2 替代實現。
QUIC 能夠建立更低延遲的鏈接,而且也像 HTTP/2 同樣,經過僅僅阻塞部分流解決了包裹丟失這個問題,讓鏈接在不一樣網絡上創建變得更簡單 - 這其實正是 MPTCP 想去解決的問題。
QUIC 如今還只有 Google 的 Chrome 和它後臺服務器上的實現,雖然有第三方庫 libquic,但這些代碼仍然很難在其餘地方被複用。該協議也被 IETF 通訊工做組引入了草案。
Caddy: 基於 Go 語言開發的 Web Server, 對 HTTP/2 和 HTTPS 有着良好的支持,也開始支持 QUIC 協議 (試驗性)
若是你訪問的站點開啓了 HTTP/2,圖標會亮起,並且點擊會進入 chrome 內置的 HTTP/2 監視工具
C 語言實現的 HTTP/2,能夠用它調試 HTTP/2 請求
直接 brew install nghttp2
就能夠安裝,安裝好後輸入 nghttp -nv https://nghttp2.org
就能夠查看 h2 請求
除 nghttp2 外還能夠用 h2i 測試 http2: github.com/golang/net/…
還能夠用 wireshark 解 h2 的包,不過得設置瀏覽器提供的對稱協商密鑰或者服務器提供的私鑰,具體方法看此文: 使用 Wireshark 調試 HTTP/2 流量
若是沒法解包看一下 sslkeylog.log 文件有沒有寫入數據,若是沒有數聽說明瀏覽器打開方式不對,得用命令行打開瀏覽器,這樣才能讓瀏覽器讀取環境變量而後向 sslkeylog 寫入密鑰,另外此方法好像支持谷歌瀏覽器和火狐,對 Safari 無效
若是 sslkeylog.log 有數據,wireshark 仍是沒法解包,打開設置的 SSL 選項從新選擇一下文件試試,若是仍是不行也用命令行打開 Wireshark
一次不行多試幾回