文章目錄
《HTTP/2基礎教程》協議、特性、詳解
前言
這篇博文講述筆者在閱讀《HTTP/2基礎教程》這本書時的讀書筆記,同時也會講述一些個人見解。
第一章 HTTP進化史
- HTTP/0.9
HTTP在1989年左右開始進入人們的視野,最初的HTTP/0.9只有一個方法(GET),0.9的規範大概只有一頁。 - HTTP/1.0
1996年發佈了1.0的標準,新增大量內容:首部、響應嗎、重定向、更多的請求方法等。對於0.9來講,1.0已經是一個巨大的飛躍。但是1.0仍存在很多瑕疵,尤其不能讓多個請求公用一個鏈接、缺少強制的Host首部、並且緩存的選擇也相當簡陋,這三點極大地影響了web可擴展的方式。 - HTTP/1.1
1999年發佈了1.1的標準。
【1】強制要求客戶端提供Host首部,所以虛擬主機託管成爲可能,也就是在一個IP上提供多個Web服務。
【2】允許在同一個連接上發送多個請求 - SPDY
2009年由Google工程師開發,它證明了人們想要更高效的協議。在SPDY之前,人們普遍認爲在商業應用中沒有必要對HTTP/1.1做出突破性的,不兼容的改變。要兼容瀏覽器、服務器、網絡代理和其他各種各樣的中間件,代價極其高昂。 - HTTP/2
2012年HTTP小組啓動了開發下一個HTTP版本的工作。HTTP/2於2015年5月14日發佈。
第二章 HTTP/2 快速入門
這章主要介紹通過簡單的方法啓動和運行一個h2服務器。主要以下兩步
1、獲取並安裝一個支持h2的Web服務器
2、下載並安裝一張TLS證書,讓瀏覽器和服務器通過h2連接。
最簡單的方法就是對於步驟1用nghttpd,對於步驟2直接用openssl
生成自簽名證書。具體過程不做贅述。
第三章 Web優化“黑魔法”的動機與方式
本章主要介紹:
1、HTTP關注的性能指標,比如延遲、首字節時間、內容下載時間等
2、HTTP/1的問題
3、Web性能優化的手段,即爲滿足關注的性能指標而優化HTTP/1的不足所採用的手段
HTTP/1的問題
- 隊頭阻塞
衆所周知,HTTP/1允許同一個連接上處理多個請求,但是一旦某個請求和響應發生阻塞,那後面的請求將會一直處於阻塞狀態。這就是隊頭阻塞
,即使HTTP/1優化過程中有了並行連接和管道化連接的手段,但再多的連接有可能存在隊頭阻塞的問題。 - 低效的TCP利用
TCP連接有慢啓動
過程,另外還有延遲確認、Nagle算法等,每個HTTP連接都會經歷慢啓動的過程,從而沒有最大程度地利用帶寬。 - 臃腫的消息頭部
雖然H1提供了壓縮被請求內容的機制,但是消息首部卻無法壓縮。消息首部可不能忽略,儘管它比響應資源小得多,但它可能佔據請求的絕大部分,如果算上cookie,有個幾千字節就很正常了。然而很多請求的報文都是重複的。 - 受限的優先級設置
H1沒有優先級設置,這樣可能導致重要的資源排在不重要資源的後面,從而影響用戶體驗
第四章 HTTP/2遷移
在本書出版的時候,大約80%的瀏覽器可以在一定程度上支持h2,常見的Chrome、Firefox、Edge、safari等,這章介紹了網站從h1向h2遷移過程中的注意事項,不必在意。
第五章 HTTP/2協議
作爲一個網絡開發者,主要關注的其實也就是這一章。
這章全面探討了HTTP/2的低層工作原理,深入到數據層傳輸的幀及其通信方式。這也是本文闡述的重點。書中對連接、流、消息、幀的介紹比較混亂,筆者將統一介紹這些關鍵內容。
5.1 HTTP/2分層
h2的關鍵特性之一就是在HTTP與TCP之間增加一個二進制分幀層。分幀層這三個字乍看上難以理解,我是這樣理解的:分、即分開、劃分,幀指的是H2中的幀,層與網絡層應用層的層對應。即,在網絡層和應用層之間又抽象出了一個層用來劃分HTTP的幀,叫做分幀層。
例如:原來的HTTP協議中一個HTTP響應包含一到多個TCP段,而h2中的一個HTTP響應可能包含1到多個HTTP幀,一般一個HTTP幀對應一個TCP段。
主要特性:
- 二進制協議
h2的分幀層是基於幀的二進制協議。這方便了機器解析,但是肉眼識別起來比較困難。 - 首部壓縮
h2的首部會被深度壓縮,顯著減少傳輸中的冗餘字節。 - 多路複用
同一個連接可以被多個請求和響應即多個流複用 - 加密傳輸
線上傳輸的絕大部分數據都是加密過的,所以在中途讀取會更加困難,更具有安全性。
5.2 連接、流、消息、幀
連接:即傳統意義上客戶端與服務端的一個TCP連接,源IP,源端口,目的IP,目的端口四個元素定義一條獨一無二的鏈接。
流:一個連接上可以有多個流,一個流通常對應一次HTTP請求和響應。
消息:一個流上可以有多個消息,消息分爲兩種,請求消息和響應消息。
幀:一個消息由一個或多個幀組成,每個幀對應一個流,一個流裏有多個幀。
1、各個流之間是相互獨立的,沒有影響
2、其中一個流阻塞,不會影響其他流
3、流可以有優先級,約定優先級高的流優先處理
1、流是一個邏輯上的概念
2、實際上連接裏只看到幀,每個幀都有一個流ID用於標識該幀所屬的流。
5.3 抓包分析
如圖所示,11,13,15,17,19這樣的數字就是流ID,每一個幀都擁有一個流ID標識該幀所屬的流。
在這幾個流中,11,13,15,17,19的請求是按順序發送的,但是相應確是亂序的,說明各個流之間互不影響。
15這個流中,包含一個請求幀,兩個響應幀。
5.4 幀結構
幀首部字段
名稱 | 長度 | 描述 |
---|---|---|
Length | 3字節 | 表示幀的負載的長度,取值範圍爲0~-1字節。請注意,字節是默認的最大幀大小,如果需要更大的幀,必須在SETTINGS幀中設置。 |
Type | 1字節 | 當前幀的類型,見下表 |
Flags | 1字節 | 具體幀類型的標識 |
R | 1位 | 保留位,不要設置,否則會帶來嚴重後果 |
Stream Identifier | 31位 | 每個流的唯一ID |
Frame Payload | 長度可變 | 真實的幀內容,長度是在Length字段中設置的 |
幀類型
名稱 | ID | 描述 |
---|---|---|
DATA | 0X0 | 傳輸流的核心內容 |
HEADERS | 0x1 | 包含HTTP首部,和可選的優先級參數 |
PRIORITY | 0x2 | 指示或者更改流的優先級和依賴 |
RST_STREAM | 0x3 | 允許一端停止流(通常是由於錯誤導致的) |
SETTINGS | 0x4 | 協商連接級參數 |
PUSH_PROMISE | 0x5 | 提示客戶端,服務端要推送些東西 |
PING | 0x6 | 測試連接性和往返時延 |
GOAWAY | 0x7 | 告訴另一端,目前端已經結束 |
WINDOW_UPDATE | 0x8 | 協商一端將要接收多少字節(用於流量控制) |
CONTINUATION | 0x9 | 用於擴展HEADER數據塊 |
5.5 流量控制
h2提供了客戶端和服務端調整傳輸速度的能力。WINDOW_UPDATE幀用來指示流量控制信息。WINDOW_UPDATE發送方告訴接收方,發送方能夠接收多少字節。當發送方接收並消費接收到的數據時,發送方再發出一個WINDOW——UPDATE幀以支持更新後的處理字節的能力。
例如:
在流建立的時候,窗口的默認大小都是65535()字節。假設客戶端A支持該默認值,他的另一端(B)發送了10000字節,B也會關注窗口大小(現在有55535字節了)。現在A花時間處理了5000字節,還剩下5000字節,然後它會發送一個WINDOW_UPDATE幀,說明它現在的窗口大小是60535字節。B收到這個幀後,開始發送了一個大文件(比如4GB大小)。在這個場景下,在B等A準備好接收更多的數據之前,B能發送的數據量就是當前窗口的大小,即60535字節。通過這種方式,A可以控制B發送數據的最大速率。
5.6 優先級
H2的優先級通過HEADERS幀和PRIORITY幀實現,客戶端可以明確地和服務端溝通它需要什麼,以及它需要這些資源的順序。這是通過聲明依賴關係樹和樹立的相對權重實現的。
- 依賴關係爲客戶端提供了一種能力,通過指明某些對象對另一些對象有依賴,告知服務器這些對象應該優先傳輸。
- 權重讓客戶端告訴服務器如何確定具有共同依賴關係的的對象的優先級。
* index.html
- style.css
- critical.js
- less_critical.js(weight 20)
- photo.jpg(weight 8)
- hearder.jpg(weight 8)
- ad.js(weight 4)
例如:可以利用依賴關係和權重,實現上述的依賴關係樹,不同層級的資源優先級不同。對於同一層級的資源,權重不同,意味着重要程度不同。服務端將會參考依賴關係樹中的關係對資源進行處理。
但是需要注意的是,依賴關係樹和權重也只是客戶端的建議,具體做什麼以及如何處理優先級,還是得聽服務器的。處理優先級的智能水平,可能會是決定各種支持h2的Web服務器性能優劣的因素。
說了這麼多,讓我們來看看HEADERS幀和PRIORITY幀是如何實現優先級的。
HEADERS 幀結構
其中各字段的含義如下:
命長 | 長度 | 描述 |
---|---|---|
Pad Length(填充長度) | 1字節 | 填充字節的長度;幀首部的PADDED標識設置爲1時纔會有該字段 |
E | 1位 | 標識流依賴是否爲專用;只有設置了PRIORITY標識纔會有該字段 |
Stream Dependency(流依賴) | 31位 | 標識當前流所依賴的流,如果有的話,只有設置了PRIORITY標識纔會有該字段 |
Weight(權重) | 1字節 | 當前流的相對權重;只有設置了PRIORITY標識纔會有該字段 |
Header Block Fragment()首部塊片段 | 長度可變 | 消息的首部 |
Padding(填充數據) | 長度可變 | 長度爲Pad Length字段的值,所有的字節被設置爲0,據說是爲了安全 |
5.7 服務端推送
提升單個對象性能的最佳方式,就是在它被用到之前就放到瀏覽器的緩存裏面。這正是HTTP/2的服務端推送的目的。推送是服務器能夠主動將對象發送給客戶端,這可能是因爲它知道客戶端不久將用到該對象。
如果服務端要推送一個對象,會構造一個PUSH_PROMISE幀。這個幀有很多重要屬性:
- PUSH_PROMISE幀首部中的流ID用來相應相關聯的請求。推送的相應一定會對應到客戶端已經發送的某個請求。
- PUSH_PROMISE幀會指示將要發送的響應所使用的流ID(也就是說,PUSH_PROMISE幀有兩個流ID,幀首部中的流ID與客戶端請求的流ID對應,幀內容中的流ID指示將要發送的響應所使用的流ID,也就是說,相應將從其他流發送給客戶端。)。另外,客戶端會從1開始設置流ID,之後每開啓一個流,就會增加2,一直是奇數。而服務端設置的流ID從2開始,一直是偶數。
- :method首部的值必須確保安全。安全的方法就是冪等的那些方法,這是一種不改變任何狀態的方法。例如,GET請求被認爲是冪等的,因爲它通常只是獲取對象。而POST請求被認爲是非冪等的,因爲它可能會改變服務端的狀態。
- 如果客戶端對PUSH——PROMISE的任何元素不滿意,就可以按照拒收原因選擇重置這個流(使用RST_STREAM),或者發送PROTOCOL_ERROR(在GOAWAY幀中)。
5.8首部壓縮
首部壓縮算法HPACK非常的複雜。但簡單來看就類似查表法,服務端和客戶端各保存一張表,當客戶端發送請求時,如果首部已經在表中存在,那就只需要發送一個首部索引,服務端拿到索引之後再查表從而得到真正的首部。