深入淺出:HTTP/2


概述

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 系列文章:

參考文章 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM