深入淺出:HTTP/2

huansky發表於2021-02-18

概述

HTTP/2 的目的就是通過支援請求與響應的多路複用來減少延遲,通過壓縮HTTP首部欄位將協議開銷降至最低,同時增加對請求優先順序和伺服器端推送的支援。為達成這些目標,HTTP/2 還會給我們帶來大量其他協議層面的輔助實現,比如新的流量控制、錯誤處理和更新機制。上述幾種機制雖然不是全部,但卻是最重要的,所有Web開發者都應該理解並在自己的應用中利用它們。

HTTP/2 不會改動HTTP的語義。HTTP方法、狀態碼、URI及首部欄位,等等這些核心概念一如往常。但是,HTTP/2 修改了格式化資料(分幀)的方式,以及客戶端與伺服器間傳輸這些資料的方式。這兩點統帥全域性,通過新的組幀機制向我們的應用隱藏了所有複雜性。換句話說,所有原來的應用都可以不必修改而在新協議執行。這當然是好事。

下面我們就來詳細介紹一下這些新的機制。

歷史及其與SPDY的淵源

SPDY是谷歌開發的一個實驗性協議,於2009年年中釋出,其主要目標是通過解決HTTP 1.1中廣為人知的一些效能限制,來減少網頁的載入延遲。大致上,這個專案設定的目標如下:

  • 頁面載入時間(PLT,Page Load Time)降低50%;

  • 無需網站作者修改任何內容;

  • 把部署複雜性降至最低,無需變更網路基礎設施;

  • 與開源社群合作開發這個新協議;

  • 收集真實效能資料,驗證這個實驗性協議是否有效。

2012年,這個新的實驗性協議得到了Chrome、Firefox和Opera的支援,很多大型網站(如谷歌、Twitter、Facebook)都對相容客戶端提供SPDY會話。換句話說,SPDY在被行業採用並證明能夠大幅提升效能之後,已經具備了成為一個標準的條件。最終,HTTP-WG(HTTP Working Group)在2012年初把HTTP/2 提到了議事日程,吸取SPDY的經驗教訓,並在此基礎上制定官方標準。

走向HTTP/2

從那時起,SPDY 已經經過了很多變化和改進,而且在 HTTP/2 官方標準公佈之前,還將有很多變化和改進。在此,有必要回顧一下HTTP/2 宣言草稿,因為這份宣言明確了該協議的範圍和關鍵設計要求:

HTTP/2 應該滿足如下條件:

  • 相對於使用TCP的HTTP 1.1,使用者在大多數情況下的感知延遲要有實質上、可度量的改進;

  • 解決HTTP中的“隊首阻塞”問題;

  • 並行操作無需與伺服器建立多個連線,從而改進TCP的利用率,特別是擁塞控制方面;

  • 保持HTTP 1.1的語義,利用現有文件,包括(但不限於)HTTP方法、狀態碼、URI,以及首部欄位;

  • 明確規定HTTP/2 如何與HTTP 1.x互操作,特別是在中間介質上;

  • 明確指出所有新的可擴充套件機制以及適當的擴充套件策略;

HTTP/2 特徵

二進位制分幀層

HTTP/2 效能增強的核心,全在於新增的二進位制分幀層(如下圖所示),它定義瞭如何封裝HTTP訊息並在客戶端與伺服器之間傳輸。

這裡所謂的“層”,指的是位於套接字介面與應用可見的高層HTTP API之間的一個新機制:HTTP的語義,包括各種動詞、方法、首部,都不受影響,不同的是傳輸期間對它們的編碼方式變了。HTTP 1.x以換行符作為純文字的分隔符,而HTTP/2 將所有傳輸的資訊分割為更小的訊息和幀,並對它們採用二進位制格式的編碼。

這樣一來,客戶端和伺服器為了相互理解,必須都使用新的二進位制編碼機制:HTTP 1.x客戶端無法理解只支援HTTP/2 的伺服器,反之亦然。不過不要緊,現有的應用不必擔心這些變化,因為客戶端和伺服器會替它們完成必要的分幀工作。

首部壓縮

HTTP的每一次通訊都會攜帶一組首部,用於描述傳輸的資源及其屬性。在HTTP 1.x中,這些後設資料都是以純文字形式傳送的,通常會給每個請求增加500~800位元組的負荷。如果算上HTTP cookie,增加的負荷通常會達到上千位元組。為減少這些開銷並提升效能,HTTP/2會用 “HPACK” 演算法來壓縮頭部資料。

“HPACK”演算法是專門為壓縮 HTTP 頭部定製的演算法,與 gzip、zlib 等壓縮演算法不同,它是一個“有狀態”的演算法,需要客戶端和伺服器各自維護一份“索引表”,也可以說是“字典”(這有點類似 brotli),壓縮和解壓縮就是查表和更新表的操作。

  • HTTP/2 在客戶端和伺服器端使用“首部表”來跟蹤和儲存之前傳送的鍵-值對,對於相同的資料,不再通過每次請求和響應傳送;

  • 首部表在HTTP/2 的連線存續期內始終存在,由客戶端和伺服器共同漸進地更新;

  • 每個新的首部鍵-值對要麼被追加到當前表的末尾,要麼替換表中之前的值。

於是,HTTP/2 連線的兩端都知道已經傳送了哪些首部,這些首部的值是什麼,從而可以針對之前的資料只編碼傳送差異資料,具體如下圖所示。

請求與響應首部的定義在HTTP/2 中基本沒有改變,只是所有首部鍵必須全部小寫,而且請求行要獨立為:method 、:scheme 、:host 和:path 這些鍵-值對。

在前面的例子中,第二個請求只需要傳送變化了的路徑首部(:path ),其他首部沒有變化,不用再傳送了。這樣就可以避免傳輸冗餘的首部,從而顯著減少每個請求的開銷。

通訊期間幾乎不會改變的通用鍵-值對(使用者代理、可接受的媒體型別,等等)只需傳送一次。事實上,如果請求中不包含首部(例如對同一資源的輪詢請求),那麼首部開銷就是零位元組。此時所有首部都自動使用之前請求傳送的首部!

二進位制幀

頭部資料壓縮之後,HTTP/2 就要把報文拆成二進位制的幀準備傳送。

HTTP/2 的幀結構有點類似 TCP 的段或者 TLS 裡的記錄,但報頭很小,只有 9 位元組,非常地節省(可以對比一下 TCP 頭,它最少是 20 個位元組)。

二進位制的格式也保證了不會有歧義,而且使用位運算能夠非常簡單高效地解析。

幀開頭是 3 個位元組的長度(但不包括頭的 9 個位元組),預設上限是 2^14,最大是 2^24,也就是說 HTTP/2 的幀通常不超過 16K,最大是 16M。

長度後面的一個位元組是幀型別,大致可以分成資料幀和控制幀兩類,HEADERS 幀和 DATA 幀屬於資料幀,存放的是 HTTP 報文,而 SETTINGS、PING、PRIORITY 等則是用來管理流的控制幀。

HTTP/2 總共定義了 10 種型別的幀,但一個位元組可以表示最多 256 種,所以也允許在標準之外定義其他型別實現功能擴充套件。這就有點像 TLS 裡擴充套件協議的意思了,比如 Google 的 gRPC 就利用了這個特點,定義了幾種自用的新幀型別。

第 5 個位元組是非常重要的幀標誌資訊,可以儲存 8 個標誌位,攜帶簡單的控制資訊。常用的標誌位有END_HEADERS表示頭資料結束,相當於 HTTP/1 裡頭後的空行(“\r\n”),END_STREAM表示單方向資料傳送結束(即 EOS,End of Stream),相當於 HTTP/1 裡 Chunked 分塊結束標誌(“0\r\n\r\n”)。

報文頭裡最後 4 個位元組是流識別符號,也就是幀所屬的“流”,接收方使用它就可以從亂序的幀裡識別出具有相同流 ID 的幀序列,按順序組裝起來就實現了虛擬的“流”。

流識別符號雖然有 4 個位元組,但最高位被保留不用,所以只有 31 位可以使用,也就是說,流識別符號的上限是 2^31,大約是 21 億。

流、訊息和幀

新的二進位制分幀機制改變了客戶端與伺服器之間互動資料的方式(如下圖所示)。為了說明這個過程,我們需要了解HTTP/2 的兩個新概念。

  • 流:已建立的連線上的雙向位元組流。

  • 訊息:與邏輯訊息對應的完整的一系列資料幀。

  • 幀:HTTP/2 通訊的最小單位,每個幀包含幀首部,至少也會標識出當前幀所屬的流。

所有HTTP/2 通訊都在一個連線上完成,這個連線可以承載任意數量的雙向資料流。相應地,每個資料流以訊息的形式傳送,而訊息由一或多個幀組成,這些幀可以亂序傳送,然後再根據每個幀首部的流識別符號重新組裝。”

這簡簡單單的幾句話裡濃縮了大量的資訊,我們再重申一次。要理解HTTP/2 ,就必須理解流、訊息和幀這幾個基本概念。

  • 所有通訊都在一個TCP連線上完成。
  • 流是連線中的一個虛擬通道,可以承載雙向的訊息;每個流都有一個唯一的整數識別符號(1、2...N)。

  • 訊息是指邏輯上的HTTP訊息,比如請求、響應等,由一或多個幀組成。

  • 幀是最小的通訊單位,承載著特定型別的資料,如HTTP首部、負荷,等等。

簡言之,HTTP/2 把HTTP協議通訊的基本單位縮小為一個一個的幀,這些幀對應著邏輯流中的訊息。相應地,很多流可以並行地在同一個TCP連線上交換訊息。

多向請求與響應

在HTTP 1.x中,如果客戶端想傳送多個並行的請求以及改進效能,那麼必須使用多個TCP連線。這是HTTP 1.x交付模型的直接結果,該模型會保證每個連線每次只交付一個響應(多個響應必須排隊)。更糟糕的是,這種模型也會導致隊首阻塞,從而造成底層TCP連線的效率低下。

HTTP/2 中新的二進位制分幀層突破了這些限制,實現了多向請求和響應:客戶端和伺服器可以把HTTP訊息分解為互不依賴的幀(如下圖所示),然後亂序傳送,最後再在另一端把它們重新組合起來。

 

上包含了同一個連線上多個傳輸中的資料流:客戶端正在向伺服器傳輸一個DATA幀(stream 5),與此同時,伺服器正向客戶端亂序傳送stream 1和stream 3的一系列幀。此時,一個連線上有3個請求/響應並行交換!

把HTTP訊息分解為獨立的幀,交錯傳送,然後在另一端重新組裝是HTTP/2 最重要的一項增強。事實上,這個機制會在整個Web技術棧中引發一系列連鎖反應,從而帶來巨大的效能提升,因為:

  • 可以並行交錯地傳送請求,請求之間互不影響;

  • 可以並行交錯地傳送響應,響應之間互不干擾;

  • 只使用一個連線即可並行傳送多個請求和響應;

  • 消除不必要的延遲,從而減少頁面載入的時間;

  • 不必再為繞過HTTP 1.x限制而多做很多工作。

  • ……

總之,HTTP/2 的二進位制分幀機制解決了HTTP 1.x中存在的隊首阻塞問題,也消除了並行處理和傳送請求及響應時對多個連線的依賴。結果,就是應用速度更快、開發更簡單、部署成本更低。

支援多向請求與響應,可以省掉針對HTTP 1.x限制所費的那些腦筋和工作,比如拼接檔案、圖片精靈、域名分割槽。類似地,通過減少TCP連線的數量,HTTP/2 也會減少客戶端和伺服器的CPU及記憶體佔用。

請求優先順序

把HTTP訊息分解為很多獨立的幀之後,就可以通過優化這些幀的交錯和傳輸順序,進一步提升效能。為了做到這一點,每個流都可以帶有一個31位元的優先值:

  • 0 表示最高優先順序。
  • 231 -1表示最低優先順序。

有了這個優先值,客戶端和伺服器就可以在處理不同的流時採取不同的策略,以最優的方式傳送流、訊息和幀。具體來講,伺服器可以根據流的優先順序,控制資源分配(CPU、記憶體、頻寬),而在響應資料準備好之後,優先將最高優先順序的幀傳送給客戶端。

瀏覽器在渲染頁面時,並非所有資源都具有相同的優先順序:HTML文件本身對構建DOM不可或缺,CSS對構建CSSOM不可或缺,而DOM和CSSOM的構建都可能受到JavaScript資源的阻塞(參見10.1節的附註欄“DOM、CSSOM和JavaScript”),其他資源(如圖片)的優先順序都可以降低。

為加快頁面載入速度,所有現代瀏覽器都會基於資源的型別以及它在頁面中的位置排定請求的優先次序,甚至通過之前的訪問來學習優先順序模式——比如,之前的渲染如果被某些資源阻塞了,那麼同樣的資源在下一次訪問時可能就會被賦予更高的優先順序。

在HTTP 1.x中,瀏覽器極少能利用上述優先順序資訊,因為協議本身並不支援多路複用,也沒有辦法向伺服器通告請求的優先順序。此時,瀏覽器只能依賴並行連線,且最多隻能同時向一個域名傳送6個請求。於是,在等連線可用期間,請求只能在客戶端排隊,從而增加了不必要的網路延遲。理論上,HTTP管道可以解決這個問題,只是由於缺乏支援而無法付諸實踐。

HTTP/2 一舉解決了所有這些低效的問題:瀏覽器可以在發現資源時立即分派請求,指定每個流的優先順序,讓伺服器決定最優的響應次序。這樣請求就不必排隊了,既節省了時間,也最大限度地利用了每個連線。

HTTP/2 沒有規定處理優先順序的具體演算法,只是提供了一種賦予資料優先順序的機制,而且要求客戶端與伺服器必須能夠交換這些資料。這樣一來,優先值作為提示資訊,對應的次序排定策略可能因客戶端或伺服器的實現而不同:客戶端應該明確指定優先值,伺服器應該根據該值處理和交付資料。

在這個規定之下,儘管你可能無法控制客戶端傳送的優先值,但或許你可以控制伺服器。因此,在選擇HTTP/2 伺服器時,可以多留點心!為說明這一點,考慮下面幾個問題。

  • 如果伺服器對所有優先值視而不見怎麼辦?

  • 高優先值的流一定優先處理嗎?

  • 是否存在不同優先順序的流應該交錯的情況?

如果伺服器不理睬所有優先值,那麼可能會導致應用響應變慢:瀏覽器明明在等關鍵的CSS和JavaScript,伺服器卻在傳送圖片,從而造成渲染阻塞。不過,規定嚴格的優先順序次序也可能帶來次優的結果,因為這可能又會引入隊首阻塞問題,即某個高優先順序的慢請求會不必要地阻塞其他資源的交付。

伺服器可以而且應該交錯傳送不同優先順序別的幀。只要可能,高優先順序流都應該優先,包括分配處理資源和客戶端與伺服器間的頻寬。不過,為了最高效地利用底層連線,不同優先順序的混合也是必需的。

有了新的分幀機制後,HTTP/2 不再依賴多個TCP連線去實現多流並行了。現在,每個資料流都拆分成很多幀,而這些幀可以交錯,還可以分別優先順序。於是,所有HTTP/2 連線都是持久化的,而且客戶端與伺服器之間也只需要一個連線即可。

實驗表明,客戶端使用更少的連線肯定可以降低延遲時間。HTTP/2 傳送的總分組數量比HTTP差不多要少40%。而伺服器處理大量併發連線的情況也變成了可伸縮性問題,因為HTTP/2 減輕了這個負擔。——HTTP/2.0 Draft 2”

每個來源一個連線顯著減少了相關的資源佔用:連線路徑上的套接字管理工作量少了,記憶體佔用少了,連線吞吐量大了。此外,從上到下所有層面上也都獲得了相應的好處:

  • 所有資料流的優先次序始終如一;

  • 壓縮上下文單一使得壓縮效果更好;

  • 由於TCP連線減少而使網路擁塞狀況得以改觀;

  • 慢啟動時間減少,擁塞和丟包恢復速度更快。

大多數HTTP連線的時間都很短,而且是突發性的,但TCP只在長時間連線傳輸大塊資料時效率才最高。HTTP/2 通過讓所有資料流共用同一個連線,可以更有效地使用TCP連線。

HTTP/2 不僅能夠減少網路延遲,還有助於提高吞吐量和降低運營成本!

等一等,我聽你說了一大堆每個來源一個TCP連線的好處,難道它就一點壞處都沒有嗎?有,當然有。

  • 雖然消除了HTTP隊首阻塞現象,但TCP層次上仍然存在隊首阻塞(參見2.4節“隊首阻塞”);
  • 如果TCP視窗縮放被禁用,那頻寬延遲積效應可能會限制連線的吞吐量;
  • 丟包時,TCP擁塞視窗會縮小。

上述每一點都可能對HTTP/2 連線的吞吐量和延遲效能造成不利影響。然而,除了這些侷限性之外,實驗表明一個TCP連線仍然是HTTP/2 基礎上的最佳部署策略:

目前為止的測試表明,壓縮和優先順序排定帶來的效能提升,已經超過了隊首阻塞(特別是丟包情況下)造成的負面效果。

流量控制

在同一個TCP連線上傳輸多個資料流,就意味著要共享頻寬。標定資料流的優先順序有助於按序交付,但只有優先順序還不足以確定多個資料流或多個連線間的資源分配。為解決這個問題,HTTP/2 為資料流和連線的流量控制提供了一個簡單的機制:

  • 流量控制基於每一跳進行,而非端到端的控制;

  • 流量控制基於視窗更新幀進行,即接收方廣播自己準備接收某個資料流的多少位元組,以及對整個連線要接收多少位元組;

  • 流量控制視窗大小通過WINDOW_UPDATE 幀更新,這個欄位指定了流ID和視窗大小遞增值;

  • 流量控制有方向性,即接收方可能根據自己的情況為每個流乃至整個連線設定任意視窗大小;

  • 流量控制可以由接收方禁用,包括針對個別的流和針對整個連線。

HTTP/2 連線建立之後,客戶端與伺服器交換SETTINGS 幀,目的是設定雙向的流量控制視窗大小。除此之外,任何一端都可以選擇禁用個別流或整個連線的流量控制。

上面這個列表是不是讓你想起了TCP流量控制?應該是,這兩個機制實際上是一樣的。然而,由於TCP流量控制不能對同一條HTTP/2 連線內的多個流實施差異化策略,因此光有它自己是不夠的。這正是HTTP/2 流量控制機制出臺的原因。

HTTP/2 標準沒有規定任何特定的演算法、值,或者什麼時候傳送WINDOW_UPDATE 幀。因此,實現可以選擇自己的演算法以匹配自己的應用場景,從而求得最佳效能。

優先順序可以決定交付次序,而流量控制則可以控制HTTP/2 連線中每個流佔用的資源:接收方可以針對特定的流廣播較低的視窗大小,以限制它的傳輸速度。

伺服器推送

HTTP/2 新增的一個強大的新功能,就是伺服器可以對一個客戶端請求傳送多個響應。換句話說,除了對最初請求的響應外,伺服器還可以額外向客戶端推送資源(如下圖所示),而無需客戶端明確地請求。

建立HTTP/2 連線後,客戶端與伺服器交換SETTINGS 幀,藉此可以限定雙向併發的流的最大數量。因此,客戶端可以限定推送流的數量,或者通過把這個值設定為0而完全禁用伺服器推送。

為什麼需要這樣一個機制呢?

通常的Web應用都由幾十個資源組成,客戶端需要分析伺服器提供的文件才能逐個找到它們。那為什麼不讓伺服器提前就把這些資源推送給客戶端,從而減少額外的時間延遲呢?伺服器已經知道客戶端下一步要請求什麼資源了,這時候伺服器推送即可派上用場。事實上,如果你在網頁裡嵌入過CSS、JavaScript,或者通過資料URI嵌入過其他資源,那你就已經親身體驗過伺服器推送了。

把資源直接插入到文件中,就是把資源直接推送給客戶端,而無需客戶端請求。在HTTP/2 中,唯一的不同就是可以把這個過程從應用中拿出來,放到HTTP協議本身來實現,而且還帶來了如下好處:

  • 客戶端可以快取推送過來的資源;
  • 客戶端可以拒絕推送過來的資源;
  • 推送資源可以由不同的頁面共享;
  • 伺服器可以按照優先順序推送資源。

所有推送的資源都遵守同源策略。換句話說,伺服器不能隨便將第三方資源推送給客戶端,而必須是經過雙方確認才行。

有了伺服器推送後,HTTP 1.x時代的大多數插入或嵌入資源的做法基本上也就過時了。唯一有必要直接在網頁中插入資源的情況,就是該資源只供那一個網頁使用,而且編碼代價不大;除此之外,所有應用都應該使用HTTP/2 伺服器推送。

 

關於 HTTP 系列文章:

參考文章 

相關文章