維基百科關(guān)于 HTTP/2 的介紹收捣,可以看下定義和發(fā)展歷史:
RFC 7540 定義了 HTTP/2 的協(xié)議規(guī)范和細節(jié),本文的細節(jié)主要來自此文檔届囚,建議先看一遍本文咕村,再回過頭來照著協(xié)議大致過一遍 RFC荡短,如果想深入某些細節(jié)再仔細翻看 RFC
Why use it ?
HTTP/1.1 存在的問題:
1、TCP 連接數(shù)限制
對于同一個域名,瀏覽器最多只能同時創(chuàng)建 6~8 個 TCP 連接 (不同瀏覽器不一樣)贮配。為了解決數(shù)量限制,出現(xiàn)了 域名分片
技術(shù)塞赂,其實就是資源分域泪勒,將資源放在不同域名下 (比如二級子域名下),這樣就可以針對不同域名創(chuàng)建連接并請求宴猾,以一種討巧的方式突破限制圆存,但是濫用此技術(shù)也會造成很多問題,比如每個 TCP 連接本身需要經(jīng)過 DNS 查詢仇哆、三步握手沦辙、慢啟動等,還占用額外的 CPU 和內(nèi)存讹剔,對于服務(wù)器來說過多連接也容易造成網(wǎng)絡(luò)擁擠油讯、交通阻塞等,對于移動端來說問題更明顯延欠,可以參考這篇文章: Why Domain Sharding is Bad News for Mobile Performance and Users
在圖中可以看到新建了六個 TCP 連接陌兑,每次新建連接 DNS 解析需要時間(幾 ms 到幾百 ms 不等)、TCP 慢啟動也需要時間由捎、TLS 握手又要時間兔综,而且后續(xù)請求都要等待隊列調(diào)度
2、線頭阻塞 (Head Of Line Blocking) 問題
每個 TCP 連接同時只能處理一個請求 - 響應(yīng),瀏覽器按 FIFO 原則處理請求软驰,如果上一個響應(yīng)沒返回涧窒,后續(xù)請求 - 響應(yīng)都會受阻。為了解決此問題碌宴,出現(xiàn)了 管線化 - pipelining 技術(shù)杀狡,但是管線化存在諸多問題,比如第一個響應(yīng)慢還是會阻塞后續(xù)響應(yīng)贰镣、服務(wù)器為了按序返回相應(yīng)需要緩存多個響應(yīng)占用更多資源呜象、瀏覽器中途斷連重試服務(wù)器可能得重新處理多個請求、還有必須客戶端 - 代理 - 服務(wù)器都支持管線化
3碑隆、Header 內(nèi)容多恭陡,而且每次請求 Header 不會變化太多,沒有相應(yīng)的壓縮傳輸優(yōu)化方案
4上煤、為了盡可能減少請求數(shù)休玩,需要做合并文件、雪碧圖劫狠、資源內(nèi)聯(lián)等優(yōu)化工作拴疤,但是這無疑造成了單個請求內(nèi)容變大延遲變高的問題,且內(nèi)嵌的資源不能有效地使用緩存機制
5独泞、明文傳輸不安全
HTTP2 的優(yōu)勢:
1呐矾、二進制分幀層 (Binary Framing Layer)
幀是數(shù)據(jù)傳輸?shù)淖钚挝唬远M制傳輸代替原本的明文傳輸懦砂,原本的報文消息被劃分為更小的數(shù)據(jù)幀:
h1 和 h2 的報文對比:
圖中 h2 的報文是重組解析過后的蜒犯,可以發(fā)現(xiàn)一些頭字段發(fā)生了變化,而且所有頭字段均小寫
strict-transport-security: max-age=63072000; includeSubdomains
字段是服務(wù)器開啟 HSTS 策略荞膘,讓瀏覽器強制使用 HTTPS 進行通信罚随,可以減少重定向造成的額外請求和會話劫持的風險
服務(wù)器開啟 HSTS 的方法是: 以 nginx 為例,在相應(yīng)站點的 server 模塊中添加
add_header Strict-Transport-Security "max-age=63072000; includeSubdomains" always;
即可
在 Chrome 中可以打開
chrome://net-internals/#hsts
進入瀏覽器的 HSTS 管理界面羽资,可以增加 / 刪除 / 查詢 HSTS 記錄淘菩,比如下圖:
在 HSTS 有效期內(nèi)且 TLS 證書仍有效,瀏覽器訪問 blog.wangriyu.wang 會自動加上 https:// 屠升,而不需要做一次查詢重定向到 https
關(guān)于幀詳見: How does it work 瞄勾?- 幀
2、多路復(fù)用 (MultiPlexing)
在一個 TCP 連接上弥激,我們可以向?qū)Ψ讲粩喟l(fā)送幀进陡,每幀的 stream identifier 的標明這一幀屬于哪個流,然后在對方接收時微服,根據(jù) stream identifier 拼接每個流的所有幀組成一整塊數(shù)據(jù)趾疚。
把 HTTP/1.1 每個請求都當作一個流,那么多個請求變成多個流,請求響應(yīng)數(shù)據(jù)分成多個幀糙麦,不同流中的幀交錯地發(fā)送給對方辛孵,這就是 HTTP/2 中的多路復(fù)用。
流的概念實現(xiàn)了單連接上多請求 - 響應(yīng)并行赡磅,解決了線頭阻塞的問題魄缚,減少了 TCP 連接數(shù)量和 TCP 連接慢啟動造成的問題
所以 http2 對于同一域名只需要創(chuàng)建一個連接,而不是像 http/1.1 那樣創(chuàng)建 6~8 個連接:
關(guān)于流詳見: How does it work 焚廊?- 流
3冶匹、服務(wù)端推送 (Server Push)
瀏覽器發(fā)送一個請求,服務(wù)器主動向瀏覽器推送與這個請求相關(guān)的資源咆瘟,這樣瀏覽器就不用發(fā)起后續(xù)請求嚼隘。
Server-Push 主要是針對資源內(nèi)聯(lián)做出的優(yōu)化,相較于 http/1.1 資源內(nèi)聯(lián)的優(yōu)勢:
- 客戶端可以緩存推送的資源
- 客戶端可以拒收推送過來的資源
- 推送資源可以由不同頁面共享
- 服務(wù)器可以按照優(yōu)先級推送資源
關(guān)于服務(wù)端推送詳見: How does it work 袒餐?- Server-Push
4飞蛹、Header 壓縮 (HPACK)
使用 HPACK 算法來壓縮首部內(nèi)容
關(guān)于 HPACK 詳見: How does it work ?- HPACK
5灸眼、應(yīng)用層的重置連接
對于 HTTP/1 來說卧檐,是通過設(shè)置 tcp segment 里的 reset flag 來通知對端關(guān)閉連接的。這種方式會直接斷開連接焰宣,下次再發(fā)請求就必須重新建立連接霉囚。HTTP/2 引入 RST_STREAM 類型的 frame,可以在不斷開連接的前提下取消某個 request 的 stream宛徊,表現(xiàn)更好。
6逻澳、請求優(yōu)先級設(shè)置
HTTP/2 里的每個 stream 都可以設(shè)置依賴 (Dependency) 和權(quán)重闸天,可以按依賴樹分配優(yōu)先級,解決了關(guān)鍵請求被阻塞的問題
7斜做、流量控制
每個 http2 流都擁有自己的公示的流量窗口苞氮,它可以限制另一端發(fā)送數(shù)據(jù)。對于每個流來說瓤逼,兩端都必須告訴對方自己還有足夠的空間來處理新的數(shù)據(jù)笼吟,而在該窗口被擴大前,另一端只被允許發(fā)送這么多數(shù)據(jù)霸旗。
關(guān)于流量控制詳見: How does it work 贷帮?- 流量控制
8、HTTP/1 的幾種優(yōu)化可以棄用
合并文件诱告、內(nèi)聯(lián)資源撵枢、雪碧圖、域名分片對于 HTTP/2 來說是不必要的,使用 h2 盡可能將資源細脸荩化潜必,文件分解地盡可能散,不用擔心請求數(shù)多
How does it work ?
幀 - Frame
幀的結(jié)構(gòu)
所有幀都是一個固定的 9 字節(jié)頭部 (payload 之前) 跟一個指定長度的負載 (payload):
+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+=+=============================================================+
| Frame Payload (0...) ...
+---------------------------------------------------------------+
-
Length
代表整個 frame 的長度沃但,用一個 24 位無符號整數(shù)表示磁滚。除非接收者在 SETTINGS_MAX_FRAME_SIZE 設(shè)置了更大的值 (大小可以是 2^14(16384) 字節(jié)到 2^24-1(16777215) 字節(jié)之間的任意值),否則數(shù)據(jù)長度不應(yīng)超過 2^14(16384) 字節(jié)宵晚。頭部的 9 字節(jié)不算在這個長度里 -
Type
定義 frame 的類型垂攘,用 8 bits 表示。幀類型決定了幀主體的格式和語義坝疼,如果 type 為 unknown 應(yīng)該忽略或拋棄搜贤。 -
Flags
是為幀類型相關(guān)而預(yù)留的布爾標識。標識對于不同的幀類型賦予了不同的語義钝凶。如果該標識對于某種幀類型沒有定義語義仪芒,則它必須被忽略且發(fā)送的時候應(yīng)該賦值為 (0x0) -
R
是一個保留的比特位。這個比特的語義沒有定義耕陷,發(fā)送時它必須被設(shè)置為 (0x0), 接收時需要忽略掂名。 - Stream Identifier 用作流控制,用 31 位無符號整數(shù)表示哟沫〗让铮客戶端建立的 sid 必須為奇數(shù),服務(wù)端建立的 sid 必須為偶數(shù)嗜诀,值 (0x0) 保留給與整個連接相關(guān)聯(lián)的幀 (連接控制消息)猾警,而不是單個流
-
Frame Payload
是主體內(nèi)容,由幀類型決定
共分為十種類型的幀:
-
HEADERS
: 報頭幀 (type=0x1)隆敢,用來打開一個流或者攜帶一個首部塊片段 -
DATA
: 數(shù)據(jù)幀 (type=0x0)发皿,裝填主體信息,可以用一個或多個 DATA 幀來返回一個請求的響應(yīng)主體 -
PRIORITY
: 優(yōu)先級幀 (type=0x2)拂蝎,指定發(fā)送者建議的流優(yōu)先級穴墅,可以在任何流狀態(tài)下發(fā)送 PRIORITY 幀,包括空閑 (idle) 和關(guān)閉 (closed) 的流 -
RST_STREAM
: 流終止幀 (type=0x3)温自,用來請求取消一個流玄货,或者表示發(fā)生了一個錯誤,payload 帶有一個 32 位無符號整數(shù)的錯誤碼 (Error Codes)悼泌,不能在處于空閑 (idle) 狀態(tài)的流上發(fā)送 RST_STREAM 幀 -
SETTINGS
: 設(shè)置幀 (type=0x4)松捉,設(shè)置此連接
的參數(shù),作用于整個連接 -
PUSH_PROMISE
: 推送幀 (type=0x5)馆里,服務(wù)端推送惩坑,客戶端可以返回一個 RST_STREAM 幀來選擇拒絕推送的流 -
PING
: PING 幀 (type=0x6)掉盅,判斷一個空閑的連接是否仍然可用,也可以測量最小往返時間 (RTT) -
GOAWAY
: GOWAY 幀 (type=0x7)以舒,用于發(fā)起關(guān)閉連接的請求趾痘,或者警示嚴重錯誤。GOAWAY 會停止接收新流蔓钟,并且關(guān)閉連接前會處理完先前建立的流 -
WINDOW_UPDATE
: 窗口更新幀 (type=0x8)永票,用于執(zhí)行流量控制功能,可以作用在單獨某個流上 (指定具體 Stream Identifier) 也可以作用整個連接 (Stream Identifier 為 0x0)滥沫,只有 DATA 幀受流量控制影響侣集。初始化流量窗口后,發(fā)送多少負載兰绣,流量窗口就減少多少世分,如果流量窗口不足就無法發(fā)送,WINDOW_UPDATE 幀可以增加流量窗口大小 -
CONTINUATION
: 延續(xù)幀 (type=0x9)缀辩,用于繼續(xù)傳送首部塊片段序列臭埋,見 首部的壓縮與解壓縮
DATA 幀格式
+---------------+
|Pad Length? (8)|
+---------------+-----------------------------------------------+
| Data (*) ...
+---------------------------------------------------------------+
| Padding (*) ...
+---------------------------------------------------------------+
-
Pad Length
: ? 表示此字段的出現(xiàn)時有條件的,需要設(shè)置相應(yīng)標識 (set flag)臀玄,指定 Padding 長度瓢阴,存在則代表 PADDING flag 被設(shè)置 -
Data
: 傳遞的數(shù)據(jù),其長度上限等于幀的 payload 長度減去其他出現(xiàn)的字段長度 -
Padding
: 填充字節(jié)健无,沒有具體語義荣恐,發(fā)送時必須設(shè)為 0,作用是混淆報文長度累贤,與 TLS 中 CBC 塊加密類似叠穆,詳見 https://httpwg.org/specs/rfc7540.html#padding
DATA 幀有如下標識 (flags):
- END_STREAM: bit 0 設(shè)為 1 代表當前流的最后一幀
- PADDED: bit 3 設(shè)為 1 代表存在 Padding
例子:
HEADERS 幀格式
+---------------+
|Pad Length? (8)|
+-+-------------+-----------------------------------------------+
|E| Stream Dependency? (31) |
+-+-------------+-----------------------------------------------+
| Weight? (8) |
+-+-------------+-----------------------------------------------+
| Header Block Fragment (*) ...
+---------------------------------------------------------------+
| Padding (*) ...
+---------------------------------------------------------------+
-
Pad Length
: 指定 Padding 長度,存在則代表 PADDING flag 被設(shè)置 -
E
: 一個比特位聲明流的依賴性是否是排他的臼膏,存在則代表 PRIORITY flag 被設(shè)置 -
Stream Dependency
: 指定一個 stream identifier硼被,代表當前流所依賴的流的 id,存在則代表 PRIORITY flag 被設(shè)置 -
Weight
: 一個無符號 8 為整數(shù)讶请,代表當前流的優(yōu)先級權(quán)重值 (1~256)祷嘶,存在則代表 PRIORITY flag 被設(shè)置 -
Header Block Fragment
: header 塊片段 -
Padding
: 填充字節(jié)屎媳,沒有具體語義夺溢,作用與 DATA 的 Padding 一樣,存在則代表 PADDING flag 被設(shè)置
HEADERS 幀有以下標識 (flags):
- END_STREAM: bit 0 設(shè)為 1 代表當前 header 塊是發(fā)送的最后一塊烛谊,但是帶有 END_STREAM 標識的 HEADERS 幀后面還可以跟 CONTINUATION 幀 (這里可以把 CONTINUATION 看作 HEADERS 的一部分)
- END_HEADERS: bit 2 設(shè)為 1 代表 header 塊結(jié)束
- PADDED: bit 3 設(shè)為 1 代表 Pad 被設(shè)置风响,存在 Pad Length 和 Padding
- PRIORITY: bit 5 設(shè)為 1 表示存在 Exclusive Flag (E), Stream Dependency, 和 Weight
例子:
首部的壓縮與解壓縮
HTTP/2 里的首部字段也是一個鍵具有一個或多個值。這些首部字段用于 HTTP 請求和響應(yīng)消息丹禀,也用于服務(wù)端推送操作状勤。
首部列表 (Header List) 是零個或多個首部字段 (Header Field) 的集合鞋怀。當通過連接傳送時,首部列表通過壓縮算法(即下文 HPACK) 序列化成首部塊 (Header Block)持搜。然后密似,序列化的首部塊又被劃分成一個或多個叫做首部塊片段 (Header Block Fragment) 的字節(jié)序列,并通過 HEADERS葫盼、PUSH_PROMISE残腌,或者 CONTINUATION 幀進行有效負載傳送。
Cookie 首部字段需要 HTTP 映射特殊對待贫导,見 8.1.2.5. Compressing the Cookie Header Field
一個完整的首部塊有兩種可能
- 一個 HEADERS 幀或 PUSH_PROMISE 幀加上設(shè)置 END_HEADERS flag
- 一個未設(shè)置 END_HEADERS flag 的 HEADERS 幀或 PUSH_PROMISE 幀抛猫,加上多個 CONTINUATION 幀,其中最后一個 CONTINUATION 幀設(shè)置 END_HEADERS flag
必須將首部塊作為連續(xù)的幀序列傳送孩灯,不能插入任何其他類型或其他流的幀闺金。尾幀設(shè)置 END_HEADERS 標識代表首部塊結(jié)束,這讓首部塊在邏輯上等價于一個單獨的幀。接收端連接片段重組首部塊,然后解壓首部塊重建首部列表挟憔。
SETTINGS 幀格式
https://httpwg.org/specs/rfc7540.html#SETTINGS
一個 SETTINGS 幀的 payload 由零個或多個參數(shù)組成对扶,每個參數(shù)的形式如下:
+-------------------------------+
| Identifier (16) |
+-------------------------------+-------------------------------+
| Value (32) |
+---------------------------------------------------------------+
-
Identifier
: 代表參數(shù)類型,比如 SETTINGS_HEADER_TABLE_SIZE 是 0x1 -
Value
: 相應(yīng)參數(shù)的值
在建立連接開始時雙方都要發(fā)送 SETTINGS 幀以表明自己期許對方應(yīng)做的配置鸳慈,對方接收后同意配置參數(shù)便返回帶有 ACK 標識的空 SETTINGS 幀表示確認,而且連接后任意時刻任意一方也都可能再發(fā)送 SETTINGS 幀調(diào)整,SETTINGS 幀中的參數(shù)會被最新接收到的參數(shù)覆蓋
SETTINGS 幀作用于整個連接归榕,而不是某個流,而且 SETTINGS 幀的 stream identifier 必須是 0x0吱涉,否則接收方會認為錯誤 (PROTOCOL_ERROR)刹泄。
SETTINGS 幀包含以下參數(shù):
- SETTINGS_HEADER_TABLE_SIZE (0x1): 用于解析 Header block 的 Header 壓縮表的大小,初始值是 4096 字節(jié)
- SETTINGS_ENABLE_PUSH (0x2): 可以關(guān)閉 Server Push怎爵,該值初始為 1特石,表示允許服務(wù)端推送功能
- SETTINGS_MAX_CONCURRENT_STREAMS (0x3): 代表發(fā)送端允許接收端創(chuàng)建的最大流數(shù)目
- SETTINGS_INITIAL_WINDOW_SIZE (0x4): 指明發(fā)送端所有流的流量控制窗口的初始大小,會影響所有流鳖链,該初始值是 2^16 - 1(65535) 字節(jié)姆蘸,最大值是 2^31 - 1,如果超出最大值則會返回 FLOW_CONTROL_ERROR
- SETTINGS_MAX_FRAME_SIZE (0x5): 指明發(fā)送端允許接收的最大幀負載的字節(jié)數(shù)芙委,初始值是 2^14(16384) 字節(jié)逞敷,如果該值不在初始值 (2^14) 和最大值 (2^24 - 1) 之間,返回 PROTOCOL_ERROR
- SETTINGS_MAX_HEADER_LIST_SIZE (0x6): 通知對端灌侣,發(fā)送端準備接收的首部列表大小的最大字節(jié)數(shù)推捐。該值是基于未壓縮的首部域大小,包括名稱和值的字節(jié)長度侧啼,外加每個首部域的 32 字節(jié)的開銷
SETTINGS 幀有以下標識 (flags):
- ACK: bit 0 設(shè)為 1 代表已接收到對方的 SETTINGS 請求并同意設(shè)置牛柒,設(shè)置此標志的 SETTINGS 幀 payload 必須為空
例子:
實際抓包會發(fā)現(xiàn) HTTP2 請求創(chuàng)建連接發(fā)送 SETTINGS 幀初始化前還有一個 Magic 幀 (建立 HTTP/2 請求的前言)堪簿。
在 HTTP/2 中,要求兩端都要發(fā)送一個連接前言皮壁,作為對所使用協(xié)議的最終確認椭更,并確定 HTTP/2 連接的初始設(shè)置,客戶端和服務(wù)端各自發(fā)送不同的連接前言蛾魄。
客戶端的前言內(nèi)容 (對應(yīng)上圖中編號 23 的幀) 包含一個內(nèi)容為 PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n
的序列加上一個可以為空的 SETTINGS 幀甜孤,在收到 101(Switching Protocols) 響應(yīng) (代表 upgrade 成功) 后發(fā)送,或者作為 TLS 連接的第一個傳輸?shù)膽?yīng)用數(shù)據(jù)畏腕。如果在預(yù)先知道服務(wù)端支持 HTTP/2 的情況下啟用 HTTP/2 連接缴川,客戶端連接前言在連接建立時發(fā)送。
服務(wù)端的前言 (對應(yīng)上圖中編號 26 的幀) 包含一個可以為空的 SETTINGS 幀描馅,在建立 HTTP/2 連接后作為第一幀發(fā)送把夸。詳見 HTTP/2 Connection Preface
發(fā)送完前言后雙方都得向?qū)Ψ桨l(fā)送帶有 ACK 標識的 SETTINGS 幀表示確認,對應(yīng)上圖中編號 29 和 31 的幀铭污。
請求站點的全部幀序列恋日,幀后面的數(shù)字代表所屬流的 id,最后以 GOAWAY 幀關(guān)閉連接:
GOAWAY 幀帶有最大的那個流標識符 (比如圖中第 29 幀是最大流)嘹狞,對于發(fā)送方來說會繼續(xù)處理完不大于此數(shù)字的流岂膳,然后再真正關(guān)閉連接
流 - Stream
流只是一個邏輯上的概念,代表 HTTP/2 連接中在客戶端和服務(wù)器之間交換的獨立雙向幀序列磅网,每個幀的 Stream Identifier 字段指明了它屬于哪個流谈截。
流有以下特性:
- 單個 h2 連接可以包含多個并發(fā)的流,兩端之間可以交叉發(fā)送不同流的幀
- 流可以由客戶端或服務(wù)器來單方面地建立和使用涧偷,或者共享
- 流可以由任一方關(guān)閉
- 幀在流上發(fā)送的順序非常重要簸喂,最后接收方會把相同 Stream Identifier (同一個流) 的幀重新組裝成完整消息報文
流的狀態(tài)
注意圖中的 send 和 recv 對象是指端點,不是指當前的流
idle
所有流以“空閑”狀態(tài)開始燎潮。在這種狀態(tài)下喻鳄,沒有任何幀的交換
其狀態(tài)轉(zhuǎn)換:
- 發(fā)送或者接收一個 HEADERS 幀會使空閑
idle
流變成打開open
狀態(tài),其中 HEADERS 幀的 Stream Identifier 字段指明了流 id确封。同樣的 HEADERS 幀(帶有 END_STREAM )也可以使一個流立即進入 half-closed 狀態(tài)除呵。 - 服務(wù)端必須在一個打開
open
或者半關(guān)閉 (遠端)half-closed(remote)
狀態(tài)的流 (由客戶端發(fā)起的) 上發(fā)送 PUSH_PROMISE 幀,其中 PUSH_PROMISE 幀的 Promised Stream ID 字段指定了一個預(yù)示的新流 (由服務(wù)端發(fā)起)爪喘,- 在服務(wù)端該新流會由空閑
idle
狀態(tài)進入被保留的 (本地)reserved(local)
狀態(tài) - 在客戶端該新流會由空閑
idle
狀態(tài)進入被保留的 (遠端)reserved(remote)
狀態(tài)
- 在服務(wù)端該新流會由空閑
在 3.2 - Starting HTTP/2 for "http" URIs 中介紹了一種特殊情況:
客戶端發(fā)起一個 HTTP/1.1 請求颜曾,請求帶有 Upgrade 機制,想創(chuàng)建 h2c 連接腥放,服務(wù)端同意升級返回 101 響應(yīng)泛啸。
升級之前發(fā)送的 HTTP/1.1 請求被分配一個流標識符 0x1绿语,并被賦予默認優(yōu)先級值秃症。從客戶端到服務(wù)端這個流 1 隱式地轉(zhuǎn)為 "half-closed" 狀態(tài)候址,因為作為 HTTP/1.1 請求它已經(jīng)完成了。HTTP/2 連接開始后种柑,流 1 用于響應(yīng)岗仑。詳細過程可以看下文的 HTTP/2 的協(xié)議協(xié)商機制
此狀態(tài)下接收到 HEADERS 和 PRIORITY 以外的幀被視為 PROTOCOL_ERROR
狀態(tài)圖中 send PP
和 recv PP
是指連接的雙方端點發(fā)送或接收了 PUSH_PROMISE,不是指某個空閑流發(fā)送或接收了 PUSH_PROMISE聚请,是 PUSH_PROMISE 的出現(xiàn)促使一個預(yù)示的流從 idle
狀態(tài)轉(zhuǎn)為 reserved
在下文 Server-Push 中會詳細介紹服務(wù)端推送的內(nèi)容和 PUSH_PROMISE 的使用情形
reserved (local) / reserved (remote)
PUSH_PROMISE 預(yù)示的流由 idle
狀態(tài)進入此狀態(tài)荠雕,代表準備進行 Server push
其狀態(tài)轉(zhuǎn)換:
- PUSH_PROMISE 幀預(yù)示的流的響應(yīng)以 HEADERS 幀開始,這會立即將該流在服務(wù)端置于半關(guān)閉 (遠端)
half-closed(remote)
狀態(tài)驶赏,在客戶端置于半關(guān)閉 (本地)half-closed(local)
狀態(tài)炸卑,最后以攜帶 END_STREAM 的幀結(jié)束,這會將流置于關(guān)閉closed
狀態(tài) - 任一端點都可以發(fā)送 RST_STREAM 幀來終止這個流煤傍,其狀態(tài)由
reserved
轉(zhuǎn)為closed
reserved(local)
狀態(tài)下的流不能發(fā)送 HEADERS盖文、RST_STREAM、PRIORITY 以外的幀蚯姆,接收到 RST_STREAM五续、PRIORITY、WINDOW_UPDATE 以外的幀被視為 PROTOCOL_ERROR
reserved(remote)
狀態(tài)下的流不能發(fā)送 RST_STREAM龄恋、WINDOW_UPDATE疙驾、PRIORITY 以外的幀,接收到 HEADERS郭毕、RST_STREAM它碎、PRIORITY 以外的幀被視為 PROTOCOL_ERROR
open
處于 open
狀態(tài)的流可以被兩個對端用來發(fā)送任何類型的幀
其狀態(tài)轉(zhuǎn)換:
- 任一端都可以發(fā)送帶有 END_STREAM 標識的幀,發(fā)送方會轉(zhuǎn)入
half-closed(local)
狀態(tài)显押;接收方會轉(zhuǎn)入half-closed(remote)
狀態(tài) - 任一端都可以發(fā)送 RST_STREAM 幀链韭,這會使流立即進入
closed
狀態(tài)
half-closed (local)
流是雙向的,半關(guān)閉表示這個流單向關(guān)閉了煮落,local 代表本端到對端的方向關(guān)閉了敞峭,remote 代表對端到本端的方向關(guān)閉了
此狀態(tài)下的流不能發(fā)送 WINDOW_UPDATE、PRIORITY蝉仇、RST_STREAM 以外的幀
當此狀態(tài)下的流收到帶有 END_STREAM 標識的幀或者任一方發(fā)送 RST_STREAM 幀旋讹,會轉(zhuǎn)為 closed
狀態(tài)
此狀態(tài)下的流收到的 PRIORITY 幀用以調(diào)整流的依賴關(guān)系順序,可以看下文的流優(yōu)先級
half-closed (remote)
此狀態(tài)下的流不會被對端用于發(fā)送幀轿衔,執(zhí)行流量控制的端點不再有義務(wù)維護接收方的流控制窗口沉迹。
一個端點在此狀態(tài)的流上接收到 WINDOW_UPDATE、PRIORITY害驹、RST_STREAM 以外的幀鞭呕,應(yīng)該響應(yīng)一個 STREAM_CLOSED 流錯誤
此狀態(tài)下的流可以被端點用于發(fā)送任意類型的幀,且此狀態(tài)下該端點仍會觀察流級別的流控制的限制
當此狀態(tài)下的流發(fā)送帶有 END_STREAM 標識的幀或者任一方發(fā)送 RST_STREAM 幀宛官,會轉(zhuǎn)為 closed
狀態(tài)
closed
代表流已關(guān)閉
此狀態(tài)下的流不能發(fā)送 PRIORITY 以外的幀葫松,發(fā)送 PRIORITY 幀是調(diào)整那些依賴這個已關(guān)閉的流的流優(yōu)先級瓦糕,端點都應(yīng)該處理 PRIORITY 幀,盡管如果該流從依賴關(guān)系樹中移除了也可以忽略優(yōu)先級幀
此狀態(tài)下在收到帶有 END_STREAM 標識的 DATA 或 HEADERS 幀后的一小段時間內(nèi) (period) 仍可能接收到 WINDOW_UPDATE 或 RST_STREAM 幀腋么,因為在遠程對端接收并處理 RST_STREAM 或帶有 END_STREAM 標志的幀之前咕娄,它可能會發(fā)送這些類型的幀。但是端點必須忽略接收到的 WINDOW_UPDATE 或 RST_STREAM
如果一個流發(fā)送了 RST_STREAM 幀后轉(zhuǎn)入此狀態(tài)珊擂,而對端接收到 RST_STREAM 幀時可能已經(jīng)發(fā)送了或者處在發(fā)送隊列中圣勒,這些幀是不可撤銷的,發(fā)送 RST_STREAM 幀的端點必須忽略這些幀摧扇。
一個端點可以限制 period 的長短圣贸,在 period 內(nèi)接受的幀會忽略,超出 period 的幀被視為錯誤扛稽。
一個端點發(fā)送了 RST_STREAM 幀后接收到流控制幀(比如 DATA)旁趟,仍會計入流量窗口,即使這些幀會被忽略庇绽,因為對端肯定是在接收到 RST_STREAM 幀前發(fā)送的流控制幀锡搜,對端會認為流控制已生效
一個端點可能會在發(fā)送了 RST_STREAM 幀后收到 PUSH_PROMISE 幀,即便預(yù)示的流已經(jīng)被重置 (reset)瞧掺,PUSH_PROMISE 幀也能使預(yù)示流變成 reserved
狀態(tài)耕餐。因此,需要 RST_STREAM 來關(guān)閉一個不想要的預(yù)示流辟狈。
PRIORITY 幀可以被任意狀態(tài)的流發(fā)送和接收肠缔,未知類型的幀會被忽略
流狀態(tài)的轉(zhuǎn)換
下面看兩個例子來理解流狀態(tài):
(1)、Server 在 Client 發(fā)起的一個流上發(fā)送 PUSH_PROMISE 幀哼转,其 Promised Stream ID 指定一個預(yù)示流用于后續(xù)推送明未,send PP 后這個預(yù)示流在服務(wù)端從 idle 狀態(tài)轉(zhuǎn)為 reserve(local) 狀態(tài),客戶端 recv PP 后這個流從 idle 狀態(tài)轉(zhuǎn)為 reserve(remote) 狀態(tài)
(2)(3)壹蔓、此時預(yù)示流處于保留狀態(tài)趟妥,客戶端如果選擇拒絕接受推送,可以發(fā)送 RST 幀關(guān)閉這個流佣蓉;服務(wù)端如果此時出問題了也可以發(fā)送 RST 幀取消推送披摄。不管哪一方發(fā)送或接收到 RST,此狀態(tài)都轉(zhuǎn)為 closed
(4)勇凭、沒有出現(xiàn)重置說明推送仍有效疚膊,則服務(wù)端開始推送,首先發(fā)送的肯定是響應(yīng)的 HEADERS 首部塊虾标,此時流狀態(tài)轉(zhuǎn)為半關(guān)閉 half-closed(remote)寓盗;客戶端接收到 HEADERS 后流狀態(tài)轉(zhuǎn)為半關(guān)閉 half-closed(local)
(5)(6)、半關(guān)閉狀態(tài)下的流應(yīng)該還會繼續(xù)推送諸如 DATA 幀、CONTINUATION 幀這樣的數(shù)據(jù)幀傀蚌,如果這個過程碰到任一方發(fā)起重置基显,則流會關(guān)閉進入 closed 狀態(tài)
(7)、如果一切順利喳张,資源隨著數(shù)據(jù)幀響應(yīng)完畢,最后一幀會帶上 END_STREAM 標識代表這個流結(jié)束了美澳,此時流轉(zhuǎn)為 closed 狀態(tài)
(1)销部、客戶端發(fā)起請求,首先發(fā)送一個 HEADERS 幀制跟,其 Stream Identifier 創(chuàng)建一個新流舅桩,此流從 idle 狀態(tài)轉(zhuǎn)為 open 狀態(tài)
(2)(3)、如果客戶端取消請求可以發(fā)送 RST 幀雨膨,服務(wù)端出錯也可以發(fā)送 RST 幀擂涛,不管哪一方接收或發(fā)送 RST,流關(guān)閉進入 closed 狀態(tài)聊记;
(4)撒妈、如果請求結(jié)束(END_STREAM),流轉(zhuǎn)為半關(guān)閉狀態(tài)排监。假如是 GET 請求狰右,一般 HEADERS 幀就是最后一幀,send H 后流會立即進入半關(guān)閉狀態(tài)舆床。假如是 POST 請求棋蚌,待數(shù)據(jù)傳完,最后一幀帶上 END_STREAM 標識挨队,流轉(zhuǎn)為半關(guān)閉
(5)(6)谷暮、客戶端半關(guān)閉后服務(wù)端開始返回響應(yīng),此時任一方接收或發(fā)送 RST盛垦,流關(guān)閉湿弦;
(7)、如果一切順利腾夯,等待響應(yīng)結(jié)束(END_STREAM)省撑,流關(guān)閉
流的標識符
流 ID 是 31 位無符號整數(shù),客戶端發(fā)起的流必須是奇數(shù)俯在,服務(wù)端發(fā)起的流必須是偶數(shù)竟秫,0x0 保留為連接控制消息不能用于建立新流。
HTTP/1.1 Upgrade to HTTP/2 時響應(yīng)的流 ID 是 0x1跷乐,在升級完成之后肥败,流 0x1 在客戶端會轉(zhuǎn)為 half-closed (local)
狀態(tài),因此這種情況下客戶端不能用 0x1 初始化一個流
新建立的流的 ID 必須大于所有已使用過的數(shù)字,接收到一個錯誤大小的 ID 應(yīng)該返回 PROTOCOL_ERROR 響應(yīng)
使用一個新流時隱式地關(guān)閉了對端發(fā)起的 ID 小于當前流的且處于 idle
狀態(tài)的流馒稍,比如一個流發(fā)送一個 HEADERS 幀打開了 ID 為 7 的流皿哨,但還從未向 ID 為 5 的流發(fā)送過幀,則流 0x5 會在 0x7 發(fā)送完或接收完第一幀后轉(zhuǎn)為 closed
狀態(tài)
一個連接內(nèi)的流 ID 不能重用
流的優(yōu)先級
客戶端可以通過 HEADERS 幀的 PRIORITY 信息指定一個新建立流的優(yōu)先級纽谒,其他期間也可以發(fā)送 PRIORITY 幀調(diào)整流優(yōu)先級
設(shè)置優(yōu)先級的目的是為了讓端點表達它所期望對端在并發(fā)的多個流之間如何分配資源的行為证膨。更重要的是,當發(fā)送容量有限時鼓黔,可以使用優(yōu)先級來選擇用于發(fā)送幀的流央勒。
流可以被標記為依賴其他流,所依賴的流完成后再處理當前流澳化。每個依賴 (dependency) 后都跟著一個權(quán)重 (weight)崔步,這一數(shù)字是用來確定依賴于相同的流的可分配可用資源的相對比例
流依賴(Stream Dependencies)
每個流都可以顯示地依賴另一個流,包含依賴關(guān)系表示優(yōu)先將資源分配給指定的流(上層節(jié)點)而不是依賴流
一個不依賴于其他流的流會指定 stream dependency 為 0x0 值缎谷,因為不存在的 0x0 流代表依賴樹的根
一個依賴于其他流的流叫做依賴流井濒,被依賴的流是當前流的父級。如果被依賴的流不在當前依賴樹中(比如狀態(tài)為 idle
的流)列林,被依賴的流會使用一個默認優(yōu)先級
當依賴一個流時瑞你,該流會添加進父級的依賴關(guān)系中,共享相同父級的依賴流不會相對于彼此進行排序希痴,比如 B 和 C 依賴 A捏悬,新添加一個依賴流 D,BCD 的順序是不固定的:
A A
/ \ ==> /|\
B C B D C
獨占標識 (exclusive) 允許插入一個新層級(新的依賴關(guān)系)润梯,獨占標識導(dǎo)致該流成為父級的唯一依賴流过牙,而其他依賴流變?yōu)槠渥蛹墸热缤瑯硬迦胍粋€新依賴流 E (帶有 exclusive):
A
A |
/|\ ==> E
B D C /|\
B D C
在依賴關(guān)系樹中纺铭,只有當一個依賴流所依賴的所有流(父級最高為 0x0 的鏈)被關(guān)閉或者無法繼續(xù)在上面執(zhí)行寇钉,這個依賴流才應(yīng)該被分配資源
依賴權(quán)重
所有依賴流都會分配一個 1~256 權(quán)重值
相同父級的依賴流按權(quán)重比例分配資源,比如流 B 依賴于 A 且權(quán)重值為 4舶赔,流 C 依賴于 A 且權(quán)重值為 12扫倡,當 A 不再執(zhí)行時,B 理論上能分配的資源只有 C 的三分之一
優(yōu)先級調(diào)整 (Reprioritization)
使用 PRIORITY 幀可以調(diào)整流優(yōu)先級
PRIORITY 幀內(nèi)容與 HEADERS 幀的優(yōu)先級模塊相同:
+-+-------------------------------------------------------------+
|E| Stream Dependency (31) |
+-+-------------+-----------------------------------------------+
| Weight (8) |
+-+-------------+
如果父級重新設(shè)置了優(yōu)先級竟纳,則依賴流會隨其父級流一起移動撵溃。若調(diào)整優(yōu)先級的流帶有獨占標識,會導(dǎo)致新的父流的所有子級依賴于這個流
如果一個流調(diào)整為依賴自己的一個子級锥累,則這個將被依賴的子級首先移至調(diào)整流的父級之下(即同一層)缘挑,再移動那個調(diào)整流的整棵子樹颜屠,移動的依賴關(guān)系保持其權(quán)重
看下面這個例子: 第一個圖是初始關(guān)系樹放航,現(xiàn)在 A 要調(diào)整為依賴 D,根據(jù)第二點孩革,現(xiàn)將 D 移至 x 之下,再把 A 調(diào)整為 D 的子樹(圖 3)惶翻,如果 A 調(diào)整時帶有獨占標識根據(jù)第一點 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)
流優(yōu)先級的狀態(tài)管理
當一個流從依賴樹中移除姑蓝,它的子級可以調(diào)整為依賴被關(guān)閉流的父級(應(yīng)該就是連接上一層節(jié)點),新的依賴權(quán)重將根據(jù)關(guān)閉流的權(quán)重以及流自身的權(quán)重重新計算吕粗。
從依賴樹中移除流會導(dǎo)致某些優(yōu)先級信息丟失纺荧。資源在具有相同父級的流之間共享,這意味著如果這個集合中的某個流關(guān)閉或者阻塞颅筋,任何空閑容量將分配給最近的相鄰流宙暇。然而,如果此集合的共有依賴(即父級節(jié)點)從樹中移除垃沦,這些子流將與更上一層的流共享資源
一個例子: 流 A 和流 B 依賴相同父級節(jié)點客给,而流 C 和流 D 都依賴 A用押,在移除流 A 之前的一段時間內(nèi)肢簿,A 和 D 都無法執(zhí)行(可能任務(wù)阻塞了),則 C 會分配到 A 的所有資源蜻拨;
如果 A 被移除出樹了池充,A 的權(quán)重按比重新計算分配給 C 和 D,此時 D 仍舊阻塞缎讼,C 分配的資源相較之前變少了收夸。對于同等的初始權(quán)重,C 獲取到的可用資源是三分之一而不是二分之一(為什么是三分之一?文檔中沒有說明細節(jié)血崭,權(quán)重如何重新分配也不太清楚卧惜,下面是按我的理解解釋的)
X 的資源為 1,ABCD 初始權(quán)重均為 16夹纫,*號代表節(jié)點當前不可用咽瓷,圖一中 C 和 B 各占一半資源,而 A 移除后 CD 的權(quán)重重新分配變?yōu)?8舰讹,所以圖二中 C 和 B 占比變?yōu)?1:2茅姜,R(C) 變?yōu)?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
可能向一個流創(chuàng)建依賴關(guān)系的優(yōu)先級信息還在傳輸中,那個流就已經(jīng)關(guān)閉了月匣。如果一個依賴流的依賴指向沒有相關(guān)優(yōu)先級信息(即父節(jié)點無效)钻洒,則這個依賴流會分配默認優(yōu)先級,這可能會造成不理想的優(yōu)先級锄开,因為給流分配了不在預(yù)期的優(yōu)先級素标。
為了避免上述問題,一個端點應(yīng)該在流關(guān)閉后的一段時間內(nèi)保留流的優(yōu)先級調(diào)整狀態(tài)信息萍悴,此狀態(tài)保留時間越長糯钙,流被分配錯誤的或者默認的優(yōu)先級可能性越低粪狼。
類似地,處于“空閑”狀態(tài)的流可以被分配優(yōu)先級或成為其他流的父節(jié)點任岸。這允許在依賴關(guān)系樹中創(chuàng)建分組節(jié)點再榄,從而實現(xiàn)更靈活的優(yōu)先級表達式∠砬保空閑流以默認優(yōu)先級開始
流優(yōu)先級狀態(tài)信息的保留可能增加終端的負擔困鸥,因此這種狀態(tài)可以被限制。終端可能根據(jù)負荷來決定保留的額外的狀態(tài)的數(shù)目剑按;在高負荷下疾就,可以丟棄額外的優(yōu)先級狀態(tài)來限制資源的任務(wù)。在極端情況下艺蝴,終端甚至可以丟棄激活或者保留狀態(tài)流的優(yōu)先級信息猬腰。如果使用了固定的限制,終端應(yīng)當至少保留跟 SETTINGS_MAX_CONCURRENT_STREAMS 設(shè)置一樣大小的流狀態(tài)
默認優(yōu)先級
所有流都是初始為非獨占地依賴于流 0x0猜敢。
Pushed 流初始依賴于相關(guān)的流(見 Server-Push)姑荷。
以上兩種情況,流的權(quán)重都指定為 16缩擂。
Server-Push
PUSH_PROMISE 幀格式
+---------------+
|Pad Length? (8)|
+-+-------------+-----------------------------------------------+
|R| Promised Stream ID (31) |
+-+-----------------------------+-------------------------------+
| Header Block Fragment (*) ...
+---------------------------------------------------------------+
| Padding (*) ...
+---------------------------------------------------------------+
-
Pad Length
: 指定 Padding 長度鼠冕,存在則代表 PADDING flag 被設(shè)置 -
R
: 保留的1bit位 -
Promised Stream ID
: 31 位的無符號整數(shù),代表 PUSH_PROMISE 幀保留的流胯盯,對于發(fā)送者來說該流標識符必須是可用于下一個流的有效值 -
Header Block Fragment
: 包含請求首部域的首部塊片段 -
Padding
: 填充字節(jié)懈费,沒有具體語義,作用與 DATA 的 Padding 一樣博脑,存在則代表 PADDING flag 被設(shè)置
PUSH_PROMISE 幀有以下標識 (flags):
- END_HEADERS: bit 2 置 1 代表 header 塊結(jié)束
- PADDED: bit 3 置 1 代表 Pad 被設(shè)置憎乙,存在 Pad Length 和 Padding
Push 的過程
結(jié)合上文關(guān)于 Server-Push 的流狀態(tài)轉(zhuǎn)換
PUSH_PROMISE 幀只能在對端(客戶端)發(fā)起的且流狀態(tài)為 open 或者 half-closed (remote) 的流上發(fā)送
PUSH_PROMISE 幀準備推送的響應(yīng)總是和來自于客戶端的請求相關(guān)聯(lián)。服務(wù)端在該請求所在的流上發(fā)送 PUSH_PROMISE 幀叉趣。PUSH_PROMISE 幀包含一個 Promised Stream ID泞边,該流標識符是從服務(wù)端可用的流標識符里選出來的。
如果服務(wù)端收到了一個對文檔的請求君账,該文檔包含內(nèi)嵌的指向多個圖片文件的鏈接繁堡,且服務(wù)端選擇向客戶端推送那些額外的圖片,那么在發(fā)送包含圖片鏈接的 DATA 幀之前發(fā)送 PUSH_PROMISE 幀可以確毕缡客戶端在發(fā)現(xiàn)內(nèi)嵌的鏈接之前椭蹄,能夠知道有一個資源將要被推送過來。同樣地净赴,如果服務(wù)端準備推送被首部塊引用的響應(yīng) (比如绳矩,在 Link 首部字段 里的),在發(fā)送首部塊之前發(fā)送一個 PUSH_PROMISE 幀玖翅,可以確币砉荩客戶端不再請求那些資源
一旦客戶端收到了 PUSH_PROMISE 幀割以,并選擇接收被推送的響應(yīng),客戶端就不應(yīng)該為準備推送的響應(yīng)發(fā)起任何請求应媚,直到預(yù)示的流被關(guān)閉以后严沥。
注意圖中推送的四個資源各預(yù)示了一個流 (Promised Stream ID),而發(fā)送 PUSH_PROMISE 幀的還是在客戶端發(fā)起的請求流 (Stream Identifier = 1) 上中姜,客戶端收到 PUSH_PROMISE 幀并選擇接收便不會對這四個資源發(fā)起請求消玄,之后服務(wù)端會發(fā)起預(yù)示的流然后推送資源相關(guān)的響應(yīng)
不管出于什么原因,如果客戶端決定不再從服務(wù)端接收準備推送的響應(yīng)丢胚,或者如果服務(wù)端花費了太長時間準備發(fā)送被預(yù)示的響應(yīng)翩瓜,客戶端可以發(fā)送一個 RST_STREAM 幀,該幀可以使用 CANCEL 或者 REFUSED_STEAM 碼携龟,并引用被推送的流標識符兔跌。
nginx 配置 Server-Push
server-push 需要服務(wù)端設(shè)置,并不是說瀏覽器發(fā)起請求峡蟋,與此請求相關(guān)的資源服務(wù)端就會自動推送
以 nginx 為例坟桅,從版本 1.13.9 開始正式支持 hppt2 serverpush 功能,
在相應(yīng) server 或 location 模塊中加入 http2_push
字段加上相對路徑的文件即可在請求該資源時推送相關(guān)資源层亿,比如我的博客設(shè)置如下桦卒,訪問首頁時有四個文件會由服務(wù)器主動推送過去而不需要客戶端請求:
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
響應(yīng):
也可以用 nghttp
測試 push 響應(yīng) (* 號代表是服務(wù)端推送的):
上面 http2_push
的設(shè)置適合靜態(tài)資源立美,服務(wù)端事先知道哪些文件是客戶端需要的匿又,然后選擇性推送
假如是后臺應(yīng)用動態(tài)生成的文件(比如 json 文件),服務(wù)器事先不知道要推送什么建蹄,可以用 Link
響應(yīng)頭來做自動推送
在 server 模塊中添加 http2_push_preload on;
server_name blog.wangriyu.wang;
root /blog;
index index.html index.htm;
http2_push_preload on;
然后設(shè)置響應(yīng)頭 (add_header) 或者后臺程序生成數(shù)據(jù)文件返回時帶上響應(yīng)頭 Link 標簽碌更,比如
Link: </style.css>; as=style; rel=preload, </main.js>; as=script; rel=preload, </image.jpg>; as=image; rel=preload
nginx 會根據(jù) Link 響應(yīng)頭主動推送這些資源
更多nginx 官方介紹見 Introducing HTTP/2 Server Push with NGINX 1.13.9
Server-Push 潛在的問題
看了這篇文章 HTTP/2 中的 Server Push 討論,發(fā)現(xiàn) Server-Push 有個潛在的問題
Server-Push 滿足條件時便會發(fā)起推送洞慎,可是客戶端已經(jīng)有緩存了想發(fā)送 RST 拒收痛单,而服務(wù)器在收到 RST 之前已經(jīng)推送資源了,雖然這部分推送無效但是肯定會占用帶寬
比如我上面博客關(guān)于 http2_push 的配置劲腿,我每次打開首頁服務(wù)器都會推送那四個文件旭绒,而實際上瀏覽器知道自己有緩存使用的也是本地緩存,也就是說本地緩存未失效的期間內(nèi)焦人,服務(wù)器的 Server-Push 只是起到了占用帶寬的作用
當然實際上對我的小站點來說影響并不大挥吵,但是如果網(wǎng)站需要大量推送的話,需要考慮并測試 Server-Push 是否會影響用戶的后續(xù)訪問
另外服務(wù)端可以設(shè)置 Cookie 或者 Session 記錄訪問時間花椭,然后之后的訪問判斷是否需要 Push忽匈;還有就是客戶端可以限制 PUSH 流的數(shù)目,也可以設(shè)置一個很低的流量窗口來限制 PUSH 發(fā)送的數(shù)據(jù)大小
至于哪些資源需要推送矿辽,在《web 性能權(quán)威指南》中就提到幾種策略丹允,比如 Apache 的 mod_spdy 能夠識別 X-Associated-Content 首部郭厌,當中列出了希望服務(wù)器推送的資源;另外網(wǎng)上有人已經(jīng)做了基于 Referer 首部的中間件來處理 Server-Push雕蔽;或者服務(wù)端能更智能的識別文檔折柠,根據(jù)當前流量決定是否推送或者推送那些資源。相信以后會有更多關(guān)于 Server-Push 的實現(xiàn)和應(yīng)用
流量控制
多路復(fù)用的流會競爭 TCP 資源批狐,進而導(dǎo)致流被阻塞液走。流控制機制確保同一連接上的流不會相互干擾。流量控制作用于單個流或整個連接贾陷。HTTP/2 通過使用 WINDOW_UPDATE 幀來提供流量控制缘眶。
流控制具有以下特征:
- 流量控制是特定于連接的。兩種級別的流量控制都位于單跳的端點之間髓废,而不是整個端到端的路徑巷懈。比如 server 前面有一個 front-end proxy 如 Nginx,這時就會有兩個 connection慌洪,browser-Nginx, Nginx—server顶燕,flow control 分別作用于兩個 connection。詳情見: How is HTTP/2 hop-by-hop flow control accomplished? - stackoverflow
- 流量控制是基于 WINDOW_UPDATE 幀的冈爹。接收方公布自己打算在每個流以及整個連接上分別接收多少字節(jié)涌攻。這是一個以信用為基礎(chǔ)的方案。
- 流量控制是有方向的频伤,由接收者全面控制恳谎。接收方可以為每個流和整個連接設(shè)置任意的窗口大小。發(fā)送方必須尊重接收方設(shè)置的流量控制限制憋肖∫蛲矗客戶方、服務(wù)端和中間代理作為接收方時都獨立地公布各自的流量控制窗口岸更,作為發(fā)送方時都遵守對端的流量控制設(shè)置鸵膏。
- 無論是新流還是整個連接,流量控制窗口的初始值是 65535 字節(jié)怎炊。
- 幀的類型決定了流量控制是否適用于幀谭企。目前,只有 DATA 幀會受流量控制影響评肆,所有其它類型的幀并不消耗流量控制窗口的空間债查。這保證了重要的控制幀不會被流量控制阻塞。
- 流量控制不能被禁用糟港。
- HTTP/2 只定義了 WINDOW_UPDATE 幀的格式和語義攀操,并沒有規(guī)定接收方如何決定何時發(fā)送幀、發(fā)送什么樣的值秸抚,也沒有規(guī)定發(fā)送方如何選擇發(fā)送包速和。具體實現(xiàn)可以選擇任何滿足需求的算法歹垫。
WINDOW_UPDATE 幀格式
+-+-------------------------------------------------------------+
|R| Window Size Increment (31) |
+-+-------------------------------------------------------------+
Window Size Increment 表示除了現(xiàn)有的流量控制窗口之外,發(fā)送端還可以傳送的字節(jié)數(shù)颠放。取值范圍是 1 到 2^31 - 1 字節(jié)排惨。
WINDOW_UPDATE 幀可以是針對一個流或者是針對整個連接的。如果是前者碰凶,WINDOW_UPDATE 幀的流標識符指明了受影響的流暮芭;如果是后者,流標識符為 0 表示作用于整個連接欲低。
流量控制功能只適用于被標識的辕宏、受流量控制影響的幀。文檔定義的幀類型中砾莱,只有 DATA 幀受流量控制影響瑞筐。除非接收端不能再分配資源去處理這些幀,否則不受流量控制影響的幀必須被接收并處理腊瑟。如果接收端不能再接收幀了聚假,可以響應(yīng)一個 FLOW_CONTROL_ERROR 類型的流錯誤或者連接錯誤。
WINDOW_UPDATE 可以由發(fā)送過帶有 END_STREAM 標志的幀的對端發(fā)送闰非。這意味著接收端可能會在 half-closed (remote) 或者 closed 狀態(tài)的流上收到 WINDOW_UPDATE 幀膘格,接收端不能將其當做錯誤。
流量控制窗口
流量控制窗口是一個簡單的整數(shù)值财松,指出了準許發(fā)送端傳送的數(shù)據(jù)的字節(jié)數(shù)瘪贱。窗口值衡量了接收端的緩存能力。
除非將其當做連接錯誤游岳,否則當接收端收到 DATA 幀時政敢,必須總是從流量控制窗口中減掉其長度(不包括幀頭的長度其徙,而且兩個級別的控制窗口都要減)胚迫。即使幀有錯誤,這也是有必要的唾那,因為發(fā)送端已經(jīng)將該幀計入流量控制窗口访锻,如果接收端沒有這樣做,發(fā)送端和接收端的流量控制窗口就會不一致闹获。
發(fā)送端不能發(fā)送受流量控制影響的期犬、其長度超出接收端告知的兩種級別的流量控制窗口可用空間的幀。即使這兩種級別的流量控制窗口都沒有可用空間了避诽,也可以發(fā)送長度為 0龟虎、設(shè)置了 END_STREAM 標志的幀(即空的 DATA 幀)。
當幀的接收端消耗了數(shù)據(jù)并釋放了流量控制窗口的空間時沙庐,可以發(fā)送一個 WINDOW_UPDATE 幀鲤妥。對于流級別和連接級別的流量控制窗口佳吞,需要分別發(fā)送 WINDOW_UPDATE 幀。
新建連接時棉安,流和連接的初始窗口大小都是 2^16 - 1(65535) 字節(jié)底扳。可以通過設(shè)置連接前言中 SETTINGS 幀的 SETTINGS_INITIAL_WINDOW_SIZE 參數(shù)改變流的初始窗口大小贡耽,這會作用于所有流衷模。而連接的初始窗口大小不能改,但可以用 WINDOW_UPDATE 幀來改變流量控制窗口
蒲赂,這是為什么連接前言往往帶有一個 WINDOW_UPDATE 幀的原因阱冶。
除了改變還未激活的流的流量控制窗口外,SETTIGNS 幀還可以改變已活躍的流 (處于 open 或 half-closed (remote) 狀態(tài)的流)的初始流量控制窗口的大小滥嘴。也就是說熙揍,當 SETTINGS_INITIAL_WINDOW_SIZE 的值變化時,接收端必須調(diào)整它所維護的所有流的流量控制窗口的值氏涩,不管是之前就打開的流還是尚未打開的流届囚。
改變 SETTINGS_INITIAL_WINDOW_SIZE 可能引發(fā)流量控制窗口的可用空間變成負值。發(fā)送端必須追蹤負的流量控制窗口是尖,并且直到它收到了使流量控制窗口變成正值的 WINDOW_UPDATE 幀意系,才能發(fā)送新的 DATA 幀。
例如饺汹,如果連接一建立客戶端就立即發(fā)送 60KB 的數(shù)據(jù)蛔添,而服務(wù)端卻將初始窗口大小設(shè)置為 16KB,那么客戶端一收到 SETTINGS 幀兜辞,就會將可用的流量控制窗口重新計算為 -44KB迎瞧。客戶端保持負的流量控制窗口逸吵,直到 WINDOW_UPDATE 幀將窗口值恢復(fù)為正值凶硅,客戶端才可以繼續(xù)發(fā)送數(shù)據(jù)。
如果改變 SETTINGS_INITIAL_WINDOW_SIZE 導(dǎo)致流量控制窗口超出了最大值扫皱,一端必須 將其當做類型為 FLOW_CONTROL_ERROR 的連接錯誤
如果接收端希望使用比當前值小的流量控制窗口足绅,可以發(fā)送一個新的 SETTINGS 幀。但是韩脑,接收端必須準備好接收超出該窗口值的數(shù)據(jù)氢妈,因為可能在收到 SETTIGNS 幀之前,發(fā)送端已經(jīng)發(fā)送了超出該較小窗口值的數(shù)據(jù)。
合理使用流控制
流量控制的定義是用來保護端點在資源約束條件下的操作。例如旁振,一個代理需要在很多連接之間共享內(nèi)存赖条,也有可能有緩慢的上游連接和快速的下游連接创夜。流量控制解決了接收方無法在一個流上處理數(shù)據(jù)锤悄,但仍希望繼續(xù)處理同一連接中的其他流的情況熬甚。
不需要此功能的部署可以通告最大大小 (2^31 - 1) 的流量控制窗口冤狡,并且可以通過在收到任何數(shù)據(jù)時發(fā)送 WINDOW_UPDATE 幀來維護此窗口大小保持不變生百。這可以有效禁用接受方的流控制递雀。相反地,發(fā)送方總是受控于接收方通告的流控制窗口的限制蚀浆。
資源約束下(例如內(nèi)存)的調(diào)度可以使用流量來限制一個對端可以消耗的內(nèi)存量缀程。需要注意的是如果在不知道帶寬延遲積的時候啟用流量控制可能導(dǎo)致無法最優(yōu)的利用可用的網(wǎng)絡(luò)資源 (RFC1323)。
即便是對當前的網(wǎng)絡(luò)延遲乘積有充分的認識市俊,流量控制的實現(xiàn)也可能很復(fù)雜杨凑。當使用流量控制時,接收端必須及時地從 TCP 接收緩沖區(qū)讀取數(shù)據(jù)摆昧。這樣做可能導(dǎo)致在一些例如 WINDOW_UPDATE 的關(guān)鍵幀在 HTTP/2 不可用時導(dǎo)致死鎖撩满。但是流量控制可以保證約束資源能在不需要減少連接利用的情況下得到保護。
HTTP/2 的協(xié)議協(xié)商機制
非加密下的協(xié)商 - h2c
客戶端使用 HTTP Upgrade 機制請求升級绅你,HTTP2-Settings 首部字段是一個專用于連接的首部字段伺帘,它包含管理 HTTP/2 連接的參數(shù)(使用 Base64 編碼),其前提是假設(shè)服務(wù)端會接受升級請求
GET / HTTP/1.1
Host: server.example.com
Connection: Upgrade, HTTP2-Settings
Upgrade: h2c
HTTP2-Settings: <base64url encoding of HTTP/2 SETTINGS payload>
服務(wù)器如果支持 http/2 并同意升級忌锯,則轉(zhuǎn)換協(xié)議伪嫁,否則忽略
HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: h2c
此時潛在的存在一個流 0x1,客戶端上這個流在完成 h1 請求后便轉(zhuǎn)為 half-closed
狀態(tài)偶垮,服務(wù)端會用這個流返回響應(yīng)
注意圖中第一個響應(yīng)所在的流是 0x1张咳,與上文所說的一致
目前瀏覽器只支持 TLS 加密下的 HTTP/2 通信,所以上述情況在瀏覽器中目前是不可能碰到的似舵,圖中顯示的是 nghttp 客戶端發(fā)起的請求
加密的協(xié)商機制 - h2
TLS 加密中在 Client-Hello 和 Server-Hello 的過程中通過 ALPN 進行協(xié)議協(xié)商
應(yīng)用層協(xié)議協(xié)商在 TLS 握手第一步的擴展中脚猾,Client Hello 中客戶端指定 ALPN Next Protocol 為 h2 或者 http/1.1 說明客戶端支持的協(xié)議
服務(wù)端如果在 Server Hello 中選擇 h2 擴展,說明協(xié)商協(xié)議為 h2砚哗,后續(xù)請求響應(yīng)跟著變化龙助;如果服務(wù)端未設(shè)置 http/2 或者不支持 h2,則繼續(xù)用 http/1.1 通信
分析實例
196: TLS 握手第一步 Client Hello频祝,開始協(xié)議協(xié)商泌参,且此處帶上了 Session Ticket
200: Server Hello 同意使用 h2,而且客戶端的會話票證有效常空,恢復(fù)會話,握手成功
202: 客戶端也恢復(fù)會話盖溺,開始加密后續(xù)消息
205: 服務(wù)端發(fā)起一個連接前言 (SETTINGS)漓糙,SETTINGS 幀中設(shè)置了最大并行流數(shù)量、初始窗口大小烘嘱、最大幀長度昆禽,然后 (WINDOW_UPDATE) 擴大窗口大小
310: 客戶端也發(fā)送一個連接前言 Magic蝗蛙,并初始化設(shè)置 (SETTINGS),SETTINGS 幀中設(shè)置了 HEADER TABLE 大小醉鳖、初始窗口大小捡硅、最大并行流數(shù)量,然后 (WINDOW_UPDATE) 擴大窗口大小
311: 客戶端發(fā)送完連接前言后可以立即跟上一個請求盗棵,GET / (HEADERS[1])壮韭,而且這個 HEADERS 幀還帶有 END_STREAM,這會使流 1 從 idle 狀態(tài)立即轉(zhuǎn)為 half-closed(local) 狀態(tài) (open 是中間態(tài))
311: 此消息中還包含一個客戶端發(fā)送給服務(wù)端的帶 ACK 的 SETTINGS 幀
312: 服務(wù)端也響應(yīng)帶 ACK 的 SETTINGS 幀
321: 服務(wù)端在流 1 (此時狀態(tài)為 half-closed(remote)) 上發(fā)送了四個 PUSH_PROMISE 幀纹因,它們分別保留了流 2喷屋、4、6瞭恰、8 用于后續(xù)推送屯曹,
321: 此消息中還返回了上面請求的響應(yīng) (HEADERS - DATA),最后 DATA 帶上 END_STREAM惊畏,流 1 從 half-closed 轉(zhuǎn)為 closed
329: 調(diào)整流優(yōu)先級恶耽,依賴關(guān)系: 8 -> 6 -> 4 -> 2 -> 1 (都帶有獨占標志,而且權(quán)重均為 110)
342: 流 1 關(guān)閉后颜启,流 2 得到分配資源驳棱,服務(wù)器開始推送,數(shù)據(jù)由兩個 DATA 幀返回
344: 流 2 結(jié)束农曲,開始推送流 4
356: 調(diào)整依賴關(guān)系
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)
367社搅、369、372: 推送 6 和 8 的流數(shù)據(jù)
377: 發(fā)起一個請求乳规,打開流 3形葬,其中客戶端發(fā)起的請求都是依賴流 0x0
之后都是同樣的套路完成請求 - 響應(yīng),最后以 GOAWAY 幀關(guān)閉連接結(jié)束
HPACK 算法
上圖來自 Ilya Grigorik 的 PPT - HTTP/2 is here, let's optimize!
可以清楚地看到 HTTP2 頭部使用的也是鍵值對形式的值暮的,而且 HTTP1 當中的請求行以及狀態(tài)行也被分割成鍵值對笙以,還有所有鍵都是小寫,不同于 HTTP1冻辩。除此之外猖腕,還有一個包含靜態(tài)索引表和動態(tài)索引表的索引空間,實際傳輸時會把頭部鍵值表壓縮恨闪,使用的算法即 HPACK倘感,其原理就是匹配當前連接存在的索引空間,若某個鍵值已存在咙咽,則用相應(yīng)的索引代替首部條目老玛,比如 “:method: GET” 可以匹配到靜態(tài)索引中的 index 2,傳輸時只需要傳輸一個包含 2 的字節(jié)即可;若索引空間中不存在蜡豹,則用字符編碼傳輸麸粮,字符編碼可以選擇哈夫曼編碼,然后分情況判斷是否需要存入動態(tài)索引表中
索引表
靜態(tài)索引
靜態(tài)索引表是固定的镜廉,對于客戶端服務(wù)端都一樣弄诲,目前協(xié)議商定的靜態(tài)索引包含 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 |
動態(tài)索引
動態(tài)索引表是一個 FIFO 隊列維護的有空間限制的表娇唯,里面含有非靜態(tài)表的索引齐遵。
動態(tài)索引表是需要連接雙方維護的,其內(nèi)容基于連接上下文视乐,一個 HTTP2 連接有且僅有一份動態(tài)表洛搀。
當一個首部匹配不到索引時,可以選擇把它插入動態(tài)索引表中佑淀,下次同名的值就可能會在表中查到索引并替換留美。
但是并非所有首部鍵值都會存入動態(tài)索引,因為動態(tài)索引表是有空間限制的伸刃,最大值由 SETTING 幀中的 SETTINGS_HEADER_TABLE_SIZE (默認 4096 字節(jié)) 設(shè)置
- 如何計算動態(tài)索引表的大小 (Table Size):
大小均以字節(jié)為單位谎砾,動態(tài)索引表的大小等于所有條目大小之和,每個條目的大小 = 字段長度 + 鍵值長度 + 32
這個額外的 32 字節(jié)是預(yù)估的條目開銷捧颅,比如一個條目使用了兩個 64-bit 指針分別指向字段和鍵值景图,并使用兩個 64-bit 整數(shù)來記錄字段和鍵值的引用次數(shù)
golang 實現(xiàn)也是加上了 32: golang.org/x/net/http2/hpack/hpack.go#L61
SETTING 幀規(guī)定了動態(tài)表的最大大小,但編碼器可以另外選擇一個比 SETTINGS_HEADER_TABLE_SIZE 小的值作為動態(tài)表的有效負載量
- 如何更新動態(tài)索引表的最大容量
修改最大動態(tài)表容量可以發(fā)送一個 dynamic table size update
信號來更改:
+---+---+---+---+---+---+---+---+
| 0 | 0 | 1 | Max size (5+) |
+---+---------------------------+
前綴 001 代表此字節(jié)為 dynamic table size update
信號碉哑,后面使用 N=5 的整數(shù)編碼方法表示新的最大動態(tài)表容量(不能超過 SETTINGS_HEADER_TABLE_SIZE)挚币,其計算方法下文會介紹。
需要注意的是這個信號必須在首部塊發(fā)送之前或者兩個首部塊傳輸?shù)拈g隔發(fā)送扣典,可以通過發(fā)送一個 Max size 為 0 的更新信號來清空現(xiàn)有動態(tài)表
- 動態(tài)索引表什么時候需要驅(qū)逐條目
- 每當出現(xiàn)表大小更新的信號時妆毕,需要判斷并驅(qū)逐隊尾的條目,即舊的索引贮尖,直到當前大小小于等于新的容量
- 每當插入新條目時笛粘,需要判斷并驅(qū)逐隊尾的條目,直到當前大小小于等于容量湿硝。這個情形下插入一個比 Max size 還大的新條目不會視作錯誤薪前,但其結(jié)果是會清空動態(tài)索引表
關(guān)于動態(tài)索引表如何管理的,推薦看下 golang 的實現(xiàn): golang.org/x/net/http2/hpack/hpack.go#L157关斜,通過代碼能更明白這個過程
索引地址空間
由靜態(tài)索引表和動態(tài)索引表可以組成一個索引地址空間:
<---------- Index Address Space ---------->
<-- Static Table --> <-- Dynamic Table -->
+---+-----------+---+ +---+-----------+---+
| 1 | ... | s | |s+1| ... |s+k|
+---+-----------+---+ +---+-----------+---+
? |
| ?
Insertion Point Dropping Point
目前 s 就是 61示括,而有新鍵值要插入動態(tài)索引表時,從 index 62 開始插入隊列蚤吹,所以動態(tài)索引表中索引從小到大依次存著從新到舊的鍵值
編碼類型表示
HPACK 編碼使用兩種原始類型: 無符號可變長度整數(shù)和八位字節(jié)表示的字符串例诀,相應(yīng)地規(guī)定了以下兩種編碼方式
整數(shù)編碼
一個整數(shù)編碼可以用于表示字段索引值随抠、首部條目索引值或者字符串長度裁着。
一個整數(shù)編碼含兩部分: 一個前綴字節(jié)和可選的后跟字節(jié)序列繁涂,只有前綴字節(jié)不足以表達整數(shù)值時才需要后跟字節(jié),前綴字節(jié)中可用比特位 N 是整數(shù)編碼的一個參數(shù)
比如下面所示的是一個 N=5 的整數(shù)編碼(前三比特用于其他標識)二驰,如果我們要編碼的整數(shù)值小于 2^N - 1扔罪,直接用一個前綴字節(jié)表示即可,比如 10 就用 ???01010
表示
+---+---+---+---+---+---+---+---+
| ? | ? | ? | Value |
+---+---+---+-------------------+
如果要編碼的整數(shù)值 X 大于等于 2^N - 1桶雀,前綴字節(jié)的可用比特位都設(shè)成 1矿酵,然后把 X 減去 2^N - 1 得到值 R,并用一個或多個字節(jié)序列表示 R矗积,字節(jié)序列中每個字節(jié)的最高有效位 (msb) 用于表示是否結(jié)束全肮,msb 設(shè)為 0 時代表是最后一個字節(jié)。具體編碼看下面的偽代碼和例子
+---+---+---+---+---+---+---+---+
| ? | ? | ? | 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 的整數(shù)編碼表示 1337:
1337 大于 31 (2^5 - 1), 將前綴字節(jié)后五位填滿 1
I = 1337 - (2^5 - 1) = 1306
I 仍然大于 128, I % 128 = 26, 26 + 128 = 154
154 二進制編碼: 10011010, 這即是第一個后跟字節(jié)
I = 1306 / 128 = 10, I 小于 128, 循環(huán)結(jié)束
將 I 編碼成二進制: 00001010, 這即是最后一個字節(jié)
+---+---+---+---+---+---+---+---+
| 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
+---+---+---+---+---+---+---+---+
解碼時讀取第一個字節(jié)棘捣,發(fā)現(xiàn)后五位 (11111) 對應(yīng)的值 I 等于 31(>= 2^N - 1)辜腺,說明還有后跟字節(jié);令 M=0乍恐,繼續(xù)讀下一個字節(jié) B评疗,I = I + (B & 127) * 2^M = 31 + 26 * 1 = 57,M = M + 7 = 7茵烈,最高有效位為 1百匆,表示字節(jié)序列未結(jié)束,B 指向下一個字節(jié)呜投;I = I + (B & 127) * 2^M = 57 + 10 * 128 = 1337加匈,最高有效位為 0,表示字節(jié)碼結(jié)束仑荐,返回 I
這里也可以這樣處理 1306: 1306 = 0x51a = (0101 0001 1010)B雕拼,將 bit 序列從低到高按 7 個一組分組,則有第一組 001 1010释漆,第二組 000 1010悲没,加上最高有效位 0/1 便與上面的后跟字節(jié)對應(yīng)
字符編碼
一個字符串可能代表 Header 條目的字段或者鍵值。字符編碼使用字節(jié)序列表示男图,要么直接使用字符的八位字節(jié)碼要么使用哈夫曼編碼示姿。
+---+---+---+---+---+---+---+---+
| H | String Length (7+) |
+---+---------------------------+
| String Data (Length octets) |
+-------------------------------+
- H: 一個比特位表示是否使用哈夫曼編碼
- String Length: 代表字節(jié)序列長度,即 String Data 的長度逊笆,使用 N=7 的整數(shù)編碼方式表示
- String Data: 字符串的八位字節(jié)碼序列表示栈戳,如果 H 為 0,則此處就是原字符的八位字節(jié)碼表示难裆;如果 H 為 1子檀,則此處為原字符的哈夫曼編碼
RFC 7541 給出了一份字符的哈夫曼編碼表: Huffman Code镊掖,這是基于大量 HTTP 首部數(shù)據(jù)生成的哈夫曼編碼。
- 當中第一列 (sym) 表示要編碼的字符褂痰,最后的特殊字符 “EOS” 代表字符串結(jié)束
- 第二列 (code as bits) 是二進制哈夫曼編碼亩进,向最高有效位對齊
- 第三列 (code as hex) 是十六進制哈夫曼編碼,向最低有效位對齊
- 最后一列 (len) 代表編碼長度缩歪,單位 bit
使用哈夫曼編碼可能存在編碼不是整字節(jié)的归薛,會在后面填充 1 使其變成整字節(jié)
比如下面的例子:
:authority: blog.wangriyu.wang
首部對應(yīng)的編碼為:
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,即對應(yīng)靜態(tài)表中第一項 :authority
8e (1000 1110) 最高有效位為 1 表示鍵值使用哈夫曼編碼匪蝙,000 1110 表示字節(jié)序列長度為 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 用于填充
二進制編碼
現(xiàn)在開始是 HPACK 真正的編解碼規(guī)范
已索引首部條目表示 (Indexed Header Field Representation)
Indexed Header Field
以 1 開始為標識主籍,能在索引空間匹配到索引的首部會替換成這種形式,后面的 index 使用上述的整數(shù)編碼方式且 N = 7逛球。
比如 :method: GET
可以用 0x82千元,即 10000010 表示
+---+---+---+---+---+---+---+---+
| 1 | Index (7+) |
+---+---------------------------+
未索引文字首部條目表示 (Literal Header Field Representation)
尚未被索引的首部有三種表示形式,第一種會添加進索引颤绕,第二種對于當前跳來說不會添加進索引幸海,第三種絕對不被允許添加進索引
- 會添加索引的文字首部 (Literal Header Field with Incremental Indexing)
以 01 開始為標識,此首部會加入到解碼后的首部列表 (Header List) 中并且會把它作為新條目插入到動態(tài)索引表中
Literal Header Field with Incremental Indexing — Indexed Name
如果字段已經(jīng)存在索引屋厘,但鍵值未被索引涕烧,比如首部 :authority: blog.wangriyu.wang
的字段 :authority
已存在索引但鍵值 blog.wangriyu.wang
不存在索引,則會替換成如下形式 (index 使用 N=6 的整數(shù)編碼表示)
+---+---+---+---+---+---+---+---+
| 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) |
+-------------------------------+
- 不添加索引的首部 (Literal Header Field without Indexing)
以 0000 開始為標識,此首部會加入到解碼后的首部列表中溢谤,但不會插入到動態(tài)索引表中
Literal Header Field without Indexing — Indexed Name
如果字段已經(jīng)存在索引瞻凤,但鍵值未被索引,則會替換成如下形式 (index 使用 N=4 的整數(shù)編碼表示)
+---+---+---+---+---+---+---+---+
| 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) |
+-------------------------------+
- 絕對不添加索引的首部 (Literal Header Field Never Indexed)
這與上一種首部類似,只是標識為 0001瞻坝,首部也是會添加進解碼后的首部列表中但不會插入動態(tài)更新表蛛壳。
區(qū)別在于這類首部發(fā)出是什么格式表示,接收也是一樣的格式所刀,作用于每一跳 (hop)衙荐,如果中間通過代理,代理必須原樣轉(zhuǎn)發(fā)不能另行編碼浮创。
而上一種首部只是作用當前跳忧吟,通過代理后可能會被重新編碼
golang 實現(xiàn)中使用一個 Sensitive
標明哪些字段是絕對不添加索引的: golang.org/x/net/http2/hpack/hpack.go#L41
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) |
+-------------------------------+
動態(tài)表最大容量更新 (Dynamic Table Size Update)
以 001 開始為標識,作用前面已經(jīng)提過
+---+---+---+---+---+---+---+---+
| 0 | 0 | 1 | Max size (5+) |
+---+---------------------------+
可以發(fā)送 Max Size 為 0 的更新來清空動態(tài)索引表
實例
RFC 中給出了很多實例 Examples - RFC 7541斩披,推薦看一遍加深理解
What then ?
HTTP/2 演示
網(wǎng)站啟用 h2 的前后對比溜族,使用 WebPageTest 做的測試讹俊,第一張是 h1,第二張是 h2:
使用 HTTP/2 建議
nginx 開啟 HTTP2 只需在相應(yīng)的 HTTPS 設(shè)置后加上 http2
即可
listen [::]:443 ssl http2 ipv6only=on;
listen 443 ssl http2;
以下幾點是 HTTP/1 和 HTTP/2 都同樣適用的
1煌抒、開啟壓縮
配置 gzip 等可以使傳輸內(nèi)容更小仍劈,傳輸速度更快
例如 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]\.";
2摧玫、使用緩存
給靜態(tài)資源設(shè)置一個緩存期是非常有必要的耳奕,關(guān)于緩存見另一篇博文 HTTP Message
例如 nginx 在 server 模塊中添加以下字段可以設(shè)置緩存時間
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;
}
3绑青、CDN 加速
CDN 的好處是就近訪問诬像,延遲低,訪問快
4闸婴、減少 DNS 查詢
每個域名都需要 DNS 查詢坏挠,一般需要幾毫秒到幾百毫秒,移動環(huán)境下會更慢邪乍。DNS 解析完成之前降狠,請求會被阻塞。減少 DNS 查詢也是優(yōu)化項之一
瀏覽器的 DNS Prefetching 技術(shù)也是一種優(yōu)化手段
5庇楞、減少重定向
重定向可能引入新的 DNS 查詢榜配、新的 TCP 連接以及新的 HTTP 請求,所以減少重定向也很重要吕晌。
瀏覽器基本都會緩存通過 301 Moved Permanently 指定的跳轉(zhuǎn)蛋褥,所以對于永久性跳轉(zhuǎn),可以考慮使用狀態(tài)碼 301睛驳。對于啟用了 HTTPS 的網(wǎng)站烙心,配置 HSTS 策略,也可以減少從 HTTP 到 HTTPS 的重定向
但以下幾點就不推薦在 HTTP/2 中用了
1乏沸、域名分片
HTTP/2 對于同一域名使用一個 TCP 連接足矣淫茵,過多 TCP 連接浪費資源而且效果不見得一定好
而且資源分域會破壞 HTTP/2 的優(yōu)先級特性,還會降低頭部壓縮效果
2蹬跃、資源合并
資源合并會不利于緩存機制匙瘪,而且單文件過大對于 HTTP/2 的傳輸不好,盡量做到細恋海化更有利于 HTTP/2 傳輸
3丹喻、資源內(nèi)聯(lián)
HTTP/2 支持 Server-Push,相比較內(nèi)聯(lián)優(yōu)勢更大效果更好
而且內(nèi)聯(lián)的資源不能有效緩存
如果有共用扼劈,多頁面內(nèi)聯(lián)也會造成浪費
HTTP/2 最佳實踐
使用 HTTP/2 盡可能用最少的連接驻啤,因為同一個連接上產(chǎn)生的請求和響應(yīng)越多,動態(tài)字典積累得越全荐吵,頭部壓縮效果也就越好骑冗,而且多路復(fù)用效率高赊瞬,不會像多連接那樣造成資源浪費
為此需要注意以下兩點:
- 同一域名下的資源使用同一個連接,這是 HTTP/2 的特性
- 不同域名下的資源贼涩,如果滿足能解析到同一 IP 或者使用的是同一個證書(比如泛域名證書)巧涧,HTTP/2 可以合并多個連接
所以使用相同的 IP 和證書部署 Web 服務(wù)是目前最好的選擇,因為這讓支持 HTTP/2 的終端可以復(fù)用同一個連接遥倦,實現(xiàn) HTTP/2 協(xié)議帶來的好處谤绳;而只支持 HTTP/1.1 的終端則會不同域名建立不同連接,達到同時更多并發(fā)請求的目的
比如 Google 一系列網(wǎng)站都是用的同一個證書:
但是這好像也會造成一個問題袒哥,我使用 nginx 搭建的 webserver缩筛,有三個虛擬主機,它們共用一套證書堡称,其中兩個我顯示地配置了 http2瞎抛,而剩下一個我并沒有配置 http2,結(jié)果我訪問未配置 http2 的站點時也變成了 http2却紧。
大圖片傳輸碰到的問題
先比較一下 h1 和 h2 的頁面加載時間桐臊,圖中綠色代表發(fā)起請求收到響應(yīng)等待負載的時間,藍色代表下載負載的時間:
可以發(fā)現(xiàn) h2 加載時間還比 h1 慢一點晓殊,特別是碰到大圖片時差別更明顯
這篇文章對不同場景下 h1 和 h2 加載圖片做了測試: Real–world HTTP/2: 400gb of images per day
其結(jié)果是:
對一個典型的富圖像断凶,延遲限制 (latency–bound) 的界面來說。使用一個高速巫俺,低延遲的連接认烁,視覺完成度 (visual completion) 平均會快 5%。
對一個圖像極其多识藤,帶寬限制 (bandwidth–bound) 的頁面來說砚著。使用同樣的連接,視覺完成度平均將會慢 5–10%痴昧,但頁面的整體加載時間實際是減少了稽穆,因為得益于連接延遲少。
一個高延遲赶撰,低速度的連接(比如移動端的慢速 3G) 會對頁面的視覺完成造成極大的延遲舌镶,但 h2 的視覺完成度明顯更高更好。
在所有的測試中豪娜,都可以看到: h2 使整體頁面的加載速度提高了餐胀,并且在初次繪制 (initial render) 上做的更好,雖然第二種情況中視覺完成度略微下降瘤载,但總體效果還是好的
視覺完成度下降的原因是因為沒有 HTTP/1.x 同時連接數(shù)量的限制否灾,h2 可以同時發(fā)起多張圖片的請求,服務(wù)器可以同時響應(yīng)圖片的負載鸣奔,可以從下面的動圖中看到
一旦圖片下載完成墨技,瀏覽器就會繪制出它們惩阶,然而,小圖片下載后會渲染地更快扣汪,但是如果一個大圖片恰好是初始的視圖断楷,那就會花費較長的時間加載,延遲視覺上的完成度崭别。
chrome bug
上面的動圖是在 Safari 上的測試結(jié)果冬筒,圖片最后都下載成功了,而我在 Chrome 上測試時后面的部分圖片直接掛了,都報 ERR_SPDY_PROTOCOL_ERROR
錯誤,而且是百分百復(fù)現(xiàn)
去看了下 ERR_SPDY_PROTOCOL_ERROR
出在哪犬第,發(fā)現(xiàn)是 Server reset stream,應(yīng)該是哪出錯了導(dǎo)致流提前終止
然后再研究了一下 HTTP/2 的幀序列匀奏,發(fā)出的請求都在 629 號消息中響應(yīng)成功了,但是返回的數(shù)據(jù)幀只有流 15 上的学搜,實際收到的圖片又不止流 15 對應(yīng)的圖片,這是為什么?
后面我繼續(xù)測試發(fā)現(xiàn)連續(xù)請求幾張大圖片论衍,雖然 HEADERS 幀都打開的是不同的流瑞佩,返回的響應(yīng)的 HEADERS 幀也還是對應(yīng)前面的流 ID,但是響應(yīng)的 DATA 幀都是從第一個打開的流上返回的坯台。
如果是小圖片的話炬丸,一個請求響應(yīng)過后這個流就關(guān)閉了,下一張小圖是在其自己對應(yīng)的流上返回的蜒蕾。只有連續(xù)幾張大圖會出現(xiàn)上述情形稠炬,這個機制很奇怪,我暫時還沒有找到解釋的文檔咪啡。
至于 chrome 為什么出錯呢首启,看一下 TCP 報文就會發(fā)現(xiàn)所有數(shù)據(jù)在一個連接上發(fā)送,到后面 TCP 包會出現(xiàn)各種問題撤摸,丟包毅桃、重傳、失序准夷、重包等等钥飞,不清楚 Safari 是否也是這樣,因為 wireshark 只能解 chrome 的包解不了 Safari 的包
《web 性能權(quán)威指南》中提及 HTTP/2 中一個 TCP 可能會造成的問題:
雖然消除了 HTTP 隊首阻塞現(xiàn)象衫嵌,但 TCP 層次上仍存在隊首阻塞問題读宙;如果 TCP 窗口縮放被禁用,那帶寬延遲積效應(yīng)可能會限制連接的吞吐量楔绞;丟包時 TCP 擁塞窗口會縮薪嵴ⅰ掖棉;
TCP 是一方面原因,還有另一方面應(yīng)該是瀏覽器策略問題膀估,估計也是 chrome bug幔亥,對比兩張動圖你會發(fā)現(xiàn),safari 接收負載是輪流接收察纯,我們幾個接收一點然后換幾個人接收帕棉,直到所有都接受完;而 chrome 則是按順序接收饼记,這個接收完才輪到下一個接收香伴,結(jié)果后面的圖片可能長時間未響應(yīng)就掛了。
使用漸進式圖片
漸進式 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 轉(zhuǎn)換成 progressive jpeg,interlace 參數(shù):
$ convert -strip -interlace Plane source.jpg destination.jpg // 還可以指定質(zhì)量 -quality 90
// 批量處理
$ for i in ./*.jpg; do convert -strip -interlace Plane $i $i; done
也可以轉(zhuǎn)換 PNG 和 GIF匪凡,但是我試過 convert -strip -interlace Plane source.png destination.png
但轉(zhuǎn)換后的圖片往往會更大膊畴,不推薦這么用,可以 convert source.png destination.jpg
ImageMagic 還有很多強大的功能
// 圖片縮放
$ convert -resize 50%x50% source.jpg destination.jpg
// 圖片格式轉(zhuǎn)換
$ convert source.jpg destination.png
// 配合 find 命令病游,將當前目錄下大于 100kb 的圖片按 75% 質(zhì)量進行壓縮
$ find -E . -iregex '.*\.(jpg|png|bmp)' -size +100k -exec convert -strip +profile “*” -quality 75 {} {} \;
png 壓縮推薦使用 pngquant
另外 photoshop 保存圖片時也可以設(shè)置漸進或交錯:
漸進式圖片:選擇圖片格式為 JPEG => 選中“連續(xù)”
交錯式圖片:選擇圖片格式為 PNG/GIF => 選中“交錯”
SPDY 與 HTTP2 的關(guān)系
SPDY 是 HTTP2 的前身唇跨,大部分特性與 HTTP2 保持一致,包括服務(wù)器端推送衬衬,多路復(fù)用和幀作為傳輸?shù)淖钚挝宦虿5?SPDY 與 HTTP2 也有一些實現(xiàn)上的不同,比如 SPDY 的頭部壓縮使用的是 DEFLATE 算法滋尉,而 HTTP2 使用的是 HPACK 算法玉控,壓縮率更高。
QUIC 協(xié)議
Google 的 QUIC(Quick UDP Internet Connections) 協(xié)議兼砖,繼承了 SPDY 的特點奸远。QUIC 是一個 UDP 版的 TCP + TLS + HTTP/2 替代實現(xiàn)。
QUIC 可以創(chuàng)建更低延遲的連接讽挟,并且也像 HTTP/2 一樣懒叛,通過僅僅阻塞部分流解決了包裹丟失這個問題,讓連接在不同網(wǎng)絡(luò)上建立變得更簡單 - 這其實正是 MPTCP 想去解決的問題耽梅。
QUIC 現(xiàn)在還只有 Google 的 Chrome 和它后臺服務(wù)器上的實現(xiàn)薛窥,雖然有第三方庫 libquic,但這些代碼仍然很難在其他地方被復(fù)用。該協(xié)議也被 IETF 通信工作組引入了草案诅迷。
Caddy: 基于 Go 語言開發(fā)的 Web Server佩番, 對 HTTP/2 和 HTTPS 有著良好的支持,也開始支持 QUIC 協(xié)議 (試驗性)
推薦工具
- Chrome 插件: HTTP/2 and SPDY indicator
如果你訪問的站點開啟了 HTTP/2罢杉,圖標會亮起趟畏,而且點擊會進入 chrome 內(nèi)置的 HTTP/2 監(jiān)視工具
- 命令行工具: nghttp2
C 語言實現(xiàn)的 HTTP/2,可以用它調(diào)試 HTTP/2 請求
直接 brew install nghttp2
就可以安裝滩租,安裝好后輸入 nghttp -nv https://nghttp2.org
就可以查看 h2 請求
除 nghttp2 外還可以用 h2i 測試 http2: https://github.com/golang/net/blob/master/http2/h2i/README.md
還可以用 wireshark 解 h2 的包赋秀,不過得設(shè)置瀏覽器提供的對稱協(xié)商密鑰或者服務(wù)器提供的私鑰,具體方法看此文: 使用 Wireshark 調(diào)試 HTTP/2 流量
如果無法解包看一下 sslkeylog.log 文件有沒有寫入數(shù)據(jù)律想,如果沒有數(shù)據(jù)說明瀏覽器打開方式不對猎莲,得用命令行打開瀏覽器,這樣才能讓瀏覽器讀取環(huán)境變量然后向 sslkeylog 寫入密鑰技即,另外此方法好像支持谷歌瀏覽器和火狐著洼,對 Safari 無效
如果 sslkeylog.log 有數(shù)據(jù),wireshark 還是無法解包而叼,打開設(shè)置的 SSL 選項重新選擇一下文件試試身笤,如果還是不行也用命令行打開 Wireshark
一次不行多試幾次
- h2o: 優(yōu)化的 HTTP Server,對 HTTP/2 的支持性做的比較好