Web技術(五):HTTP/2 是如何解決HTTP/1.1 性能瓶頸的?

一、HTTP/2 概覽

前篇博文:圖解HTTP中介紹了HTTP的演進史,同時也提到了HTTP/1.1 版本存在的性能瓶頸,比如服務器因爲只能逐個順序響應HTTP請求可能引起的隊頭阻塞問題,因爲每個HTTP 報文都要傳輸臃腫的首部字段導致的網絡效率降低等問題。

解決上面的問題也有相應的思路,比如HTTP 響應的隊頭阻塞問題可以讓多個請求/響應報文併發複用一個TCP連接,不至於因爲某個請求報文響應阻塞而影響後續所有請求報文的響應。HTTP報文重複傳輸臃腫的首部字段的問題,前篇博文也給出了相應的解決思路,通信雙方可以都維護一張HTTP 首部字段索引列表,報文中只傳輸對應字段的索引值,就能大大壓縮報文首部的長度,提高網絡利用率。

2009年,Google提出了一種HTTP/1.1 的替代方案SPDY (發音同Speedy)來改善HTTP/1.1 遺留的性能瓶頸問題,經過幾年的驗證改進後,SPDY 帶來了顯著的網絡訪問效率提升。因其出色的性能,SPDY 成爲HTTP/2 開發的基礎,並證明了前面提出的一些解決思路的合理性,比如TCP連接多路複用、二進制編碼和首部壓縮等。

2012 年,HTTP 工作組(IETF 工作組中負責HTTP 規範的小組)啓動了開發下一個HTTP 版本的工作,並最終決定使用SPDY 作爲HTTP/2.0 的起點。2015年,HTTP/2 作爲正式協議發佈於RFC7540
HTTP/2概覽
HTTP/2 是一個徹徹底底的二進制協議,頭信息和數據包體都是二進制的,統稱爲“幀”。對比HTTP/1.1 中,頭信息是文本編碼(ASCII編碼),數據包體可以是二進制也可以是文本。HTTP/2 使用二進制作爲協議實現方式的好處是便於計算機直接處理,但不方便我們肉眼識別。

HTTP/2 中的基本協議單元是一個幀,在 HTTP/2 中定義了 10 種不同類型的幀,每種幀類型都有不同的用途。比如 HEADERS 和 DATA 幀構成了 HTTP 請求和響應的基礎;其它幀類型(比如 PRIORITY、SETTINGS、PUSH_PROMISE、WINDOW_UPDATE 等 )用於支持其它 HTTP/2 功能。
HTTP/2多路複用流
HTTP/1.1 中由於服務器只能逐個順序響應請求報文,前一個響應未完成就只能一直阻塞等待而無法傳輸下一個響應報文,這就白白浪費了TCP 連接的帶寬資源,同時帶來了隊頭阻塞問題。HTTP/2 支持多個資源請求/響應報文在同一個TCP連接上併發傳輸,可以充分利用TCP連接的帶寬資源,也不會因爲某個報文響應一直阻塞等待而影響其它報文的傳輸,同時解決了隊頭阻塞問題。

HTTP/2 中不同資源的數據包在同一個TCP連接上是亂序傳輸的,怎麼判斷哪些數據包是同一個資源的呢?怎麼將收到的數據包正確組合成目標資源呢?這就要求每個數據包都包含相應的標識信息,用來標識它屬於哪個資源或者哪個請求/響應報文。

HTTP/2 把每個 request 和 response 的數據包稱爲一個數據流(stream),每個數據流都有自己全局唯一的編號,每個數據包在傳輸過程中都需要標記它屬於哪個數據流 ID。規定,客戶端發出的數據流 ID 一律爲奇數,服務器發出的數據流 ID 一律爲偶數。
HTTP/2首部壓縮
HTTP/2 在客戶端與服務器端都維護了一張首部字段索引列表, header 字段列表是以key - value 鍵值對元素構成的有序集合,每個header 字段元素都映射爲一個索引值,報文中使用header 字段的索引值進行二進制編碼傳輸,顯然比HTTP/1.1 直接使用header 字段ASCII 編碼傳輸,數據量小得多,這種減少header 字段傳輸開銷的技術可以稱爲首部壓縮HPACK。

爲了保證header 字段的索引值能正確解碼,客戶端與服務器端的header 字段列表索引映射關係應該完全一致。爲了進一步降低header 字段的傳輸開銷,這些 header 字段表可以在編碼或解碼新 header 字段時進行增量更新,新的header 字段採用Huffman 編碼(摩斯電碼就採用了霍夫曼編碼)可以進一步降低編碼後的字節數。

再回顧下 HTTP/2 解決了HTTP/1.1 的哪些性能瓶頸或者帶來了哪些性能提升:

HTTP/1.1 性能瓶頸 HTTP/2 改進優化
存在隊頭阻塞問題,降低了TCP連接利用率 通過多數據流併發複用TCP連接,不僅解決了隊頭阻塞問題,還大大提高了TCP連接的利用率
重複傳輸臃腫的首部字段,降低了網絡資源利用率 通過首部壓縮,大大減少了需要傳輸的首部字段字節數,進一步提高了網絡資源利用率
報文各字段長度不固定,增加了報文解析難度,只能串行解析 整個報文都採用二進制編碼,且每個字段長度固定,可以並行處理,提高了報文處理效率
只能客戶端發起請求,服務器響應請求,服務器端的數據更新不能及時反饋給客戶端 支持服務器端向客戶端推送資源,服務器端的數據更新可以及時反饋給客戶端,也可以通過預判客戶端需求提前向客戶端推送相應資源,提高客戶端的訪問響應效率

二、HTTP/2 協議原理

HTTP/2 是爲了解決HTTP/1.1 的性能瓶頸問題誕生的,在提供更高網絡訪問效率的同時,自然需要向前兼容HTTP/1.1。HTTP/2 和HTTP/1.1 使用相同的 “http” 和 “https” URI scheme,共享相同的默認端口號: “http” URI 爲 80,“https” URI 爲 443。既然從URL 上無法區分HTTP/2 和HTTP/1.1,我們只能從報文標頭信息辨識網站使用的是哪個協議版本了(比如報文中的“協議版本”字段)。

前面已經簡單介紹過HTTP/2 相比HTTP/1.1 比較重要的幾個特性:二進制幀層、多數據流併發、Header 壓縮、服務端推送等,下面分別對其介紹。

2.1 Binary frame layer

前面介紹了,HTTP/2 是基於幀 (frame)的協議,採用分幀是爲了將重要信息都封裝起來,讓協議的解析方可以輕鬆閱讀、解析並還原信息。 相比之下,HTTP/1.x 不是基於幀的,而是以文本分隔。 解析這種文本分割的數據往往速度慢且容易出錯,你需要不斷讀入字節,直到遇到分隔符爲止(這裏指[CRLF]),這會帶來以下弊端:

  • 一次只能處理一個請求或響應,完成之前不能停止解析,只能串行解析報文;
  • 無法預判解析需要多少內存。這會帶來一系列問題:你要把一行讀到多大的緩衝區裏;如果行太長而緩衝區不夠用,應該增加並重新分配內存還是返回400 錯誤;這些問題顯然會降低內存處理的效率和速度。

HTTP/2 使用幀來封裝各字段信息,幀中包含表示整幀長度的字段,每個字段也有固定的長度,處理幀協議的程序就能預先知道會收到哪些字段信息,每個字段佔用多少內存空間,也就可以並行處理數據幀。HTTP/2 的幀結構如下圖示:
HTTP/2幀結構
前9 個字節對於每個幀都是一致的,解析時只需要讀取這些字節,就可以準確地知道在整個幀中期望的字節數。幀首部各字段說明見下表:

幀字段名稱 長度 描述
Length 3字節 表示幀負載的長度(取值範圍爲214 ~ 224-1 字節),
值得注意的是,214 字節是默認的最大幀大小,
如果需要更大的幀,可在SETTINGS 幀中設置
Type 1字節 當前幀類型
Flags 1字節 具體幀類型的標識,不同的幀類型標識不一樣
R 1位 保留位,不要設置,否則可能帶來嚴重後果
Stream Identifier 31位 每個流的唯一ID
Frame Payload 長度可變 真實的幀內容,長度是在Length 字段中設置的

如果讀者對TCP/IP 協議棧比較瞭解的話,會發現HTTP/2 的幀結構與TCP/UDP/IP數據報文格式有很大的相似之處,報文中都包含了總長度字段信息,每個字段的長度都是固定的,計算機處理這種報文/幀會更加簡單高效。

得益於幀結構的優勢,HTTP/2 可以並行處理多個數據幀,再借助Stream Identifier 字段標識每個請求/響應數據流,可以讓不同數據流的數據幀交錯的在TCP連接上傳輸(藉助Stream ID,即便交錯傳輸也可以重新組裝),這就實現了多個數據流併發複用同一個TCP連接的效果。

多數據流併發複用同一個TCP連接,有點類似於多線程併發複用同一個處理器資源,不會因爲某一個數據流的阻塞等待而影響其它數據流的傳輸,既提高了TCP連接的利用效率,又解決了HTTP/1.1 中的隊頭阻塞問題。可以說,HTTP/2 幀結構是數據流多路複用的基礎,也是解決隊頭阻塞問題的關鍵。

HTTP/2 中定義了 10種不同的幀類型,每種幀類型都有不同的用途,其中 HEADERS 和 DATA 幀構成了 HTTP 請求和響應的基礎,這十種幀類型及功能描述如下:

幀類型名稱 ID 描述
DATA 0x0 傳輸流的核心內容
HEADERS 0x1 包含HTTP 首部,和可選的優先級參數
PRIORITY 0x2 指示或者更改流的優先級和依賴
RST_STREAM 0x3 允許一端停止流(通常由於錯誤導致的)
SETTINGS 0x4 協商連接級參數
PUSH_PROMISE 0x5 提示客戶端,服務器要推送些東西
PING 0x6 測試連接可用性和往返時延(RTT)
GOAWAY 0x7 告訴另一端,當前端已結束
WINDOW_UPDATE 0x8 協商一端將要接收多少字節(用於流量控制)
CONTINUATION 0x9 用以擴展HEADER 數據塊

可以說,HTTP/2 所有性能增強的核心在於新的二進制分幀層,它定義瞭如何封裝 HTTP 消息並在客戶端與服務器之間傳輸。這裏所謂的“層”,指的是位於套接字接口與應用可見的高級 HTTP API 之間一個經過優化的新編碼機制:HTTP 的語義(包括各種動詞、方法、標頭)都不受影響,不同的是傳輸期間對它們的編碼方式變了。 HTTP/1.x 協議以換行符作爲純文本的分隔符,而 HTTP/2 將所有傳輸的信息分割爲更小的消息和幀,並採用二進制格式對它們編碼。
HTTP/2數據幀傳輸請求響應報文
限於篇幅,沒法對這十種類型的幀定義進行全面介紹,下面選擇構成HTTP/2 請求響應基礎的HEADERS 幀和 DATA 幀爲例,簡單介紹完整的幀定義格式。

2.1.1 DATA幀定義

DATA幀(類型Type = 0x0)比較簡單,主要是用來封裝報文主體數據的,DATA幀結構定義如下(HTTP/2幀首部定義前面介紹過了,這裏的DATA幀結構屬於HTTP/2幀結構中的Frame Payload部分,其餘類型幀結構定義也是如此):
DATA幀結構
DATA 類型的幀包含的字節長度不定,如果超出幀容許的最大長度,資源數據會被切分到一個或者多個幀裏面去。上圖中的填充長度(Pad Length)字段和填充數據(Padding)屬於可選字段,可以藉助填充數據隱藏真實的消息大小(出於安全方面的考慮)。DATA幀字段及對應的幀類型標識如下:

DATA幀標識位Flags名稱 描述
END_STREAM 0x1 表明這是流中最後的幀(流終止)
PADDED 0x8 表明此幀添加了填充數據,要處理Pad Length 和Padding 字段
DATA幀字段名稱 長度 描述
Pad Length 1字節 填充字節的長度;
在幀首部Flags字段的PADDED 標識設置爲1 的時候纔會有該字段
DATA 長度可變 幀的內容
Padding 長度可變 長度爲Pad Length 字段的值,所有的字節被設置爲0;
在幀首部Flags字段的PADDED 標識設置爲1 的時候纔會有該字段

由於資源數據可能會被切分到多個DATA幀中,需要對最後一個DATA幀進行標識,以便處理程序快速獲知該資源數據已接收完畢。DATA幀中用於標識最後幀的Flags是END_STREAM,表示當前資源數據發送結束(即 EOS,End of Stream),相當於 HTTP/1.x 中的 Chunked 分塊結束標誌(“0\r\n\r\n”)

2.1.2 HEADERS幀定義

HEADER幀(類型Type = 0x1)相對比較複雜,主要是用來封裝報文首部數據的,相當於HTTP/1.1 中的 start line + header,HEADER幀結構定義如下:
HEADERS幀結構
HEADERS幀結構中最主要的字段Header Block Fragment 在下文介紹首部壓縮時再展開,這裏先看HEADERS幀類型標識及各字段描述如下(填充相關的字段作用與DATA幀中的一致):

HEADERS幀標識位Flags名稱 描述
END_STREAM 0x1 表明這是流中最後的幀(流終止)
END_HEADERS 0x4 表明這是流中最後一個HEADERS 幀;
如果此標識未設置,表示隨後會有CONTINUATION 幀
PADDED 0x8 表明此幀添加了填充數據,要使用Pad Length 和Padding 字段
PRIORITY 0x20 如果設置了此標識,就表示要使用E、Stream Dependency 以及Weight 字段
HEADERS幀字段名稱 長度 描述
Pad Length 1字節 填充字節的長度;
幀首部的PADDED 標識設置爲1 時纔會有該字段
E 1位 表示流依賴是否爲專用的;
只有設置了PRIORITY 標識才會有該字段
Stream Dependency 31位 表示當前流所依賴的流,如果有的話;
只有設置了PRIORITY 標識才會有該字段
Weight 1字節 當前流的相對權重;
只有設置了PRIORITY 標識才會有該字段
Header Block Fragment 長度可變 消息的首部,包含各首部字段信息
Padding 長度可變 長度爲Pad Length 字段的值,所有的字節被設置爲0;
幀首部的PADDED 標識設置爲1 時纔會有該字段

HEADERS幀結構中的Header Block Fragment 字段字節長度不定,有些報文首部長度可能超出幀容許的最大長度,後面會以CONTINUATION幀的形式繼續封裝剩下的附加首部字段,CONTINUATION幀實際上只有一個Header Block Fragment 字段(可變長度),CONTINUATION幀標識位Flags只有一個END_HEADERS。HEADERS幀與CONTINUATION幀標識位END_HEADERS 表示標頭數據結束,相當於 HTTP/1.x 中報文頭部後的空行(“\r\n”)

HEADERS幀結構中的E、Stream Dependency、Weight 三個可選字段都依賴於PRIORITY幀設置,PRIORITY 幀是爲了標識流的優先級,下文介紹流時再展開。

2.2 Streams and Multiplexing

HTTP/2 規範對流(stream)的定義是:“HTTP/2 連接上獨立的、雙向的幀序列交換”,你可以將流看作在連接上的一系列幀,它們構成了單獨的HTTP 請求和響應。如果客戶端想要發出請求,它會開啓一個新的流,服務器將在這個流上回復。這與HTTP/1.x 的請求、響應流程類似,重要的區別在於,HTTP/2 有分幀層和流 ID,所以多個請求和響應報文可以交錯在TCP連接上傳輸,而不會互相阻塞,這就實現了多流併發複用的效果。

Stream 流是用來傳輸一對兒請求、響應消息(相當於HTTP/1.x 中的報文)的,一個消息至少由HEADERS 幀(它初始化流)組成,並且可以另外包含CONTINUATION 和DATA 幀。客戶端到服務器的HTTP/2 連接建立之後,通過發送HEADERS 幀(如果首部過長可能還會發送CONTINUATION 幀)來啓動新的流。後續有新的流啓動時,會發送一個帶有遞增流 ID(每對兒請求與響應消息都有一個不同的流 ID,客戶端會從1 開始設置流ID,之後每新開啓一個流就會增加2,之後一直使用奇數)的新HEADERS 幀。
GET/POST請求響應消息
前面介紹過,Stream ID 使用無符號的 31 位整數標識,由客戶端發起的流使用奇數編號,由服務器發起的流使用偶數編號。流標識符零(0x0)用於連接控制消息,零流標識符不能用於建立新的 stream 流。

Stream ID可以說是多流併發(也稱多路複用)的關鍵,多個流的數據幀在同一個TCP連接上交錯/併發傳輸,接收端主要就是根據這個stream ID 來辨識每個數據幀屬於哪個流,並依序組裝復原消息的(這就要求同一個 stream 內的數據幀必須是有序的)。

在一個TCP連接上支持的最大併發流數量由SETTINGS幀的SETTINGS_MAX_CONCURRENT_STREAMS 參數控制,前面也介紹過HTTP/2 幀負載的長度如果超過 214 字節就需要在SETTINGS幀中設置,這裏簡單介紹下SETTINGS幀結構及參數列表如下:
SETTINGS幀結構及參數列表
SETTINGS 幀包含了若干有序的 ID / Value 鍵值對(數量根據需要而定),每個鍵值對長度爲6字節(ID佔2 字節,Value佔4 字節)。如果一端接收並處理了SETTINGS 幀,就必須返回一個SETTINGS 幀,在幀首部中Flags字段帶上ACK 標識(0x1),這是SETTINGS 幀裏定義的唯一的幀類型標識位。這樣發送端就知道接收端收到了新的SETTINGS 幀,並會遵守SETTINGS 幀的設置。

2.2.1 Stream 流量控制

藉助分幀層和Stream ID,HTTP/2 可以在一個TCP 連接上實現多流併發,但多個數據流之間會相互競爭,某個數據流過大會阻塞其它stream 流的傳輸。我們怎麼確保同一條連接上的流不會相互阻塞呢?我們很容易想到TCP 協議中的流量控制方案(可參考博文:網絡傳輸管理之TCP協議中的流量控制部分),通信雙方藉助一個接收窗口來同步雙方當前的發送與接收能力,提高對網絡資源的利用效率。

HTTP/2 也採用類似的思路,使用WINDOW_UPDATE 幀來同步通信雙方的發送與接收能力,數據流接收方通過WINDOW_UPDATE 幀向發送方更新流量控制窗口的大小,發送方傳輸數據流的速率受到該流量控制窗口大小的限制。當接收端處理完接收到的數據,它會發出一個WINDOW_UPDATE 幀來告訴發送方自己新的接收窗口大小。

你可能會疑惑,TCP 已經提供了流量控制功能,HTTP 通信是基於TCP 連接的,爲何還需要HTTP/2 再提供流量控制功能呢?還記得HTTP/2 的多流併發複用同一個TCP 連接嗎?TCP 流量控制的精度是針對一個TCP 連接的,單靠TCP 的流量控制並不能解決一個TCP 連接內的多流爭用阻塞問題。HTTP/2 流量控制的精度是針對一個stream 流的,可以精細控制同一個TCP連接內的每個流中的數據傳輸速率,自然就能解決同一個TCP 連接上多個流相互干擾阻塞的問題了。

HTTP/2 的流量控制是基於WINDOW_UPDATE 幀的,但僅針對直接建立 TCP 連接的兩端有效,如果對端是代理服務器,代理服務器不需要向上遊轉發 WINDOW_UPDATE 幀(代理兩端的吞吐能力不同可能適用的流量控制窗口也有差別)。同時,爲了確保重要的控制幀不被流量控制阻擋,流量控制目前只對DATA 幀有效。WINDOW_UPDATE 幀結構及其字段描述如下:
WINDOW_UPDATE幀結構
WINDOW_UPDATE 幀只有一個字段Window Size Increment 用來更新當前窗口大小的增量,既然是增量就有對應的基準或初始值,初始窗口大小的默認值由SETTINGS幀的SETTINGS_INITIAL_WINDOW_SIZE參數限制(默認值爲65535字節),可以通過SETTINGS幀來設置新的初始窗口大小(可設置的最大窗口大小爲231-1)。

WINDOW_UPDATE 幀沒有定義任何 Flags 標誌,主要對DATA 幀首部 stream ID 所標識的數據流進行流量控制窗口大小更新。如果 stream ID 值爲“0”,則表示該數據幀所在的整個TCP 連接都受WINDOW_UPDATE 幀更新的流量控制窗口大小限制。

2.2.2 Stream 優先級管理

當我們訪問一個網頁時,平均會請求上百個資源,這些資源之間往往存在一定的依賴關係,比如瀏覽器請求到HTML後,需要收到CSS和關鍵的JavaScript 資源後才能開始頁面渲染,也即其它資源的佈局和渲染依賴於CSS和JavaScript 資源。如果瀏覽器以最優順序獲取資源,就可以帶來網頁訪問效率的提升,這個獲取資源的最優順序該如何實現呢?

要想讓服務器按照客戶端想要的順序響應資源,比較容易想到的方法就是對各資源進行優先級管理,對優先級高的資源優先響應。說到優先級管理,很容易想到RTOS 操作系統線程調度器的優先級管理方案,但這裏我們不能簡單地使用一個優先級變量來管理所有的流,我們是想管理同屬於一個網頁的不同資源的優先級關係,而不是跨網頁對所有的數據流統一進行優先級管理。怎麼實現只管理同屬於一個網頁的不同資源的優先級關係呢?

既然同屬於一個網頁的不同資源之間存在一定的依賴關係,我們可以從依賴關係入手來描述不同資源的相對優先級。HTTP/2 正是採用依賴關係樹和樹裏的相對權重實現網頁內不同資源的相對優先級管理的:

  • 依賴關係:客戶端通過指明某些資源對另一些資源有依賴,告知服務器這些資源應該優先傳輸;
  • 權重:讓客戶端告訴服務器如何確定具有共同依賴關係的資源的相對優先級。

優先級樹

比如上圖中優先級樹,以第三棵樹爲例,資源 C 依賴資源 D,服務器優先響應資源 D,待資源 D 響應完畢後再響應資源 C;資源 A 和 B 共同依賴資源 C,服務器優先響應資源 C,待資源 C 響應完畢後再響應資源 A 和 B;資源 A 和 B 處於相同的依賴層級,那就看二者的相對權重值,資源 A 和 B 的權重比值爲 3 :1,服務器爲了響應資源 A 和 B 分配給二者的時間和空間資源比例爲 3 :1,也即服務器爲響應資源 A 花費的努力程度是資源 B 的三倍。

有了優先級樹來描述不同資源的相對優先級,客戶端還需要相應的幀(PRIORITY幀)來告訴服務器,請求的不同資源的優先級關係。前面介紹HEADERS 幀時,有三個字段(E、Stream Dependency、Weight)都依賴於HEADERS幀標識位Flags 的PRIORITY 標識,這三個字段也正是PRIORITY幀的字段,下面給出PRIORITY 幀結構及其字段描述如下:
PRIORITY幀結構及其字段描述
HTTP/2 中每個資源的請求/響應消息構成一個流,不同資源之間的依賴關係也就等同於不同流之間的依賴關係。PRIORITY 幀中的Stream Dependency 和 Weight 字段可以完全描述不同資源之間的依賴關係和相對權重,也就可以描述頁面內不同資源的相對優先級了。

PRIORITY 幀沒有定義任何 Flags 標誌,客戶端可以多次發送PRIORITY 幀來動態調整不同資源的優先級關係,但後面指定的優先級會覆蓋之前的。客戶端可以通過PRIORITY 幀告訴服務器應該按照怎樣的優先級關係來響應衆多請求,但具體如何處理優先級,服務器會根據情況酌情調整,因此不能保證資源的響應順序跟PRIORITY 幀中描述的完全一致。

HEADERS 幀中本就包含PRIORITY 幀中的三個字段,也就是說在啓動一個流時,就可以在HEADERS 幀中給出該流的依賴關係和相對權重了(需要設置HEADERS幀標識PRIORITY)。

2.3 Server Push

HTTP/1.x 協議主要是爲通過網絡共享超文本內容設計的,早期Web 應用比較簡單,HTTP 協議也採用了簡單的客戶端請求 / 服務器響應模型,請求只能從客戶端開始,客戶端不可以接收除響應以外的指令。隨着Web 應用越來越豐富,更新越來越頻繁,客戶端對訪問信息的實時性要求越來越高,怎麼快速高效的將服務器上的內容更新同步到客戶端呢?

爲了讓服務器能夠及時將內容的更新同步到客戶端,最容易想到的方法就是讓服務器支持主動向客戶端推送數據的能力。但HTTP/1.x 的客戶端不接收除響應以外的指令數據,服務器也不支持主動推送數據的能力,這也是HTTP/1.x 的性能瓶頸之一。在HTTP/2 誕生之前,人們爲HTTP/1.x 設計了不少優化手段來臨時解決其性能瓶頸問題。

  • Short Polling Via AJAX:就是讓HTTP協議頻繁從客戶端向服務器詢問是否有內容更新,該方案雖然勉強實現了服務器內容更新同步到客戶端的效果,但實時性較差,且產生大量的無效請求,進一步降低網絡利用效率;
  • Comet Long Polling:服務器收到更新詢問請求後,若沒有更新暫時不響應,直到有更新纔將更新內容響應給客戶端,該方案減少了輪詢次數且提升了實時性,但服務器需要保持大量連接,增加了服務器的負擔;
  • Server-Sent Events:SSE是 HTML 5公佈的一種服務器向瀏覽器客戶端發起數據傳輸的技術,一旦客戶端創建了初始連接,服務器到客戶端的單向事件流將保持打開狀態,並支持連續不斷的單向發送流信息,直到客戶端關閉。SSE流信息的本質是下載,因此只能服務器向瀏覽器單向發送事件流信息(Content-Type必須指定 MIME 類型爲event-steam),該方案實時性較高,新版瀏覽器大多已經支持該方案;
  • WebSocket:是HTML 5開始提供的一種獨立在單個 TCP 連接上進行全雙工(full-duplex)通訊的有狀態協議(它不同於無狀態的 HTTP),WebSocket 沒有了 Request 和 Response 的概念,兩者地位完全平等,連接一旦建立,就建立了真正的持久性連接,雙方可以隨時向對方發送數據。

目前看來,WebSocket 是可以完美替代 AJAX 短輪詢和 Comet 長輪詢的,但是某些場景還是不能替代 SSE,WebSocket 和 SSE 各有所長。WebSocket 是與HTTP 同屬於應用層級的獨立且並列的協議,爲了加快普及,使用與HTTP相同的端口資源,也需要藉助HTTP報文建立連接,WebSocket 建立連接後就不需要依賴HTTP協議而可以獨立工作了。SSE可與HTTP較好的配合,彌補HTTP協議服務器向客戶端發送流信息能力不足的弱點,SSE 還能提供 WebSockets 不具備的各種功能,比如自動重新連接、事件 ID 以及發送任意事件的能力。SSE與WebSocket 的原理示意圖如下:
SSE與WebSockets原理示意圖
HTTP/2 設計時自然也要嘗試解決HTTP/1.x 中服務器不能主動向客戶端推送數據的弱點,SSE是創建一個從服務器到客戶端的單向事件流實現服務器主動推送功能,WebSocket 是在TCP連接上創建一個雙向數據流實現客戶端與服務器的全雙工通信,由於WebSocket 與HTTP 已經是兩個完全不同又相互獨立的協議,HTTP/2 更多的可從SSE 的實現思路獲得啓發。

HTTP/2 基於二進制數據幀通信,請求/響應消息或報文以數據流爲單位進行管理,服務器向客戶端主動推送資源或更新數據是通過PUSH_PROMISE 幀實現的,該幀也要依賴於某個數據流,也即服務器標識出要推送的資源或數據是屬於哪個數據流的,服務器不能創建或初始化一個數據流。HTTP/2 服務端藉助PUSH_PROMISE 幀向客戶端推送數據的圖示如下:
服務端推送消息處理
PUSH_PROMISE 幀的首部塊與客戶端請求資源時發送的HEADERS 幀首部塊相似,二者都包含stream ID 字段,由於PUSH_PROMISE 幀都是由服務器發送給客戶端,因此PUSH_PROMISE 幀中stream ID 字段總是爲偶數。

服務端發送PUSH_PROMISE 幀來告訴客戶端,它將發送一份客戶端尚未明確請求的資源,PUSH_PROMISE 幀實際上是對客戶端發送的HEADERS 幀的補充,PUSH_PROMISE 幀結構及其字段定義如下:
PUSH_PROMISE 幀結構
PUSH_PROMISE 幀結構中的字段Header Block Fragment 跟HEADERS 幀類似,PUSH_PROMISE 幀類型標識及各字段描述如下(填充相關的字段作用與DATA幀中的一致):

PUSH_PROMISE幀
標識位Flags名稱
描述
END_HEADERS 0x4 表明這是流中最後一個PUSH_PROMISE 幀或HEADERS 幀;
如果此標識未設置,表示隨後會有CONTINUATION 幀
PADDED 0x8 表明此幀添加了填充數據,要使用Pad Length 和Padding 字段
PUSH_PROMISE幀字段名稱 長度 描述
Pad Length 1字節 填充字節的長度;
幀首部的PADDED 標識設置爲1 時纔會有該字段
R 1位 保留位,不必設置
Promised Stream ID 31位 告知發送端將要使用的流ID;
總是偶數,因爲是由服務端發送的
Header Block Fragment 長度可變 消息的首部,包含各首部字段信息
Padding 長度可變 長度爲Pad Length 字段的值,所有的字節被設置爲0;
幀首部的PADDED 標識設置爲1 時纔會有該字段

在客戶端接收到 PUSH_PROMISE 幀後,它可以根據自身情況選擇拒絕數據流,比如服務器推送的資源已經在客戶端緩存中了,客戶端可通過RST_STREAM 幀來拒絕或結束服務器通過PUSH_PROMISE 幀推送的數據流。如果客戶端想禁用服務器的推送功能,可以將SETTINGS 幀的SETTINGS_ENABLE_PUSH 字段設置爲 0,SETTINGS 幀各參數字段的作用在前面已經介紹過了。

值得注意的是,服務器可以在PUSH_PROMISE 發送後立即啓動推送流,因此拒收正在進行的推送可能仍然無法避免推送大量資源。推送正確的資源是不夠的,還需要保證只推送正確的資源,這是重要的性能優化手段。

2.4 Header Compression (HPACK)

前面已經提到過HTTP/1.x 重複傳輸臃腫的首部字段,降低了網絡利用效率,因此迫切需要一種能對這些臃腫的首部字段進行壓縮後傳輸的技術,以解決HTTP/1.x 的性能瓶頸問題。Google 開發的SPDY 協議也提供了相應的首部壓縮格式deflate與gzip,但該方案存在被CRIME 攻擊的漏洞,因此HTTP/2 設計了新的首部壓縮格式 HPACK,這種壓縮格式採用兩種簡單但強大的技術(HPACK 發佈於RFC7541):

  • HPACK 支持通過靜態霍夫曼編碼對傳輸的標頭字段進行壓縮,從而減小了各個字段傳輸的大小;
  • HPACK 要求客戶端和服務器同時維護和更新一個包含之前見過的標頭字段的索引列表,若待傳輸的字段已在該列表中,則只需要傳輸該字段對應的索引編號即可,接收方可根據自己維護的索引列表和收到的索引編號重構完整的標頭鍵值對。

HPACK首部壓縮原理圖示
HPACK 是一種表查找壓縮方案,它利用霍夫曼編碼對傳輸的標頭字段進行壓縮,客戶端和服務器同時維護和更新的索引列表實際上由一張靜態表和一張動態表組合而成:

  • Static Table:在規範中定義,由61 個最常用的HTTP 標頭字段的鍵值組合而成,該表是有序、只讀、始終可訪問的,可以在所有編解碼上下文之間共享,比如 “:method: GET” 在靜態表中索引爲2;
  • Dynamic Table:最初爲空,將根據在特定連接內交換的HTTP標頭字段值進行更新,該表是動態變化的,只能應用於特定的編解碼上下文,比如"user-agent: Mozilla/5.0…"首次出現時採用Huffman 編碼壓縮傳輸,客戶端與服務器同步將該標頭字段的鍵值對更新到索引列表中。

在 HTTP/2 中,請求和響應標頭字段的定義保持不變,僅有一些微小的差異:所有標頭字段名稱均爲小寫,請求行現在拆分成各個 :method、:scheme、:authority 和 :path 等僞標頭字段。

下面使用一個示例來展示 HPACK 首部壓縮的原理:
HPACK維護索引列表示例
上圖中,Request #1 是新建TCP 連接上的首個請求報文,此時動態索引列表爲空,需要傳輸未出現在靜態索引列表中的標頭字段鍵值信息(經過靜態Huffman 編碼壓縮)。Request #2 是同一個TCP 連接上的第二個請求報文,此時動態索引列表已經維護了之前出現過的HTTP 標頭字段,該請求報文中的絕大部分字段都與Request #1 中的標頭字段一致,只有":path" 字段不一樣,Request #2中只需要傳輸":path" 字段的鍵值信息和其它標頭字段的索引編號即可,有效避免了通信中的重複字節的傳輸,顯著提高了網絡資源利用效率。

HTTP/2 允許的索引列表最大尺寸或字節數默認爲4096個,若想改變索引列表最大尺寸,可通過SETTINGS 幀的SETTINGS_HEADER_TABLE_SIZE 字段設置。

HTTP/2 提倡使用盡可能少的TCP 連接數,儘可能在一個TCP連接上實現多流併發複用傳輸,頭部壓縮是其中一個重要的原因:在同一個連接上產生的請求和響應越多,動態索引列表累積的越全,頭部壓縮的效果就越好。據統計,HPACK 首部壓縮相比HTTP/1.x 可以減少85% ~ 95% 的HTTP 標頭數據傳輸量。

三、HTTP/2 連接管理

HTTP/2 報文采用二進制編碼機制,HTTP/1.x 協議是無法理解HTTP/2 報文的,客戶端與服務器要想正常使用HTTP 協議進行通信,雙方必須能夠互相理解對方的數據報文編碼格式,也即雙方必須使用相同版本的HTTP 協議。HTTP/2 帶來的性能提升挺顯著的,各大網站肯定都要逐漸遷移到HTTP/2 協議的,客戶端瀏覽器一般支持HTTP/2 協議比各大網站服務端更快,問題是客戶端怎麼與服務器協商使用的HTTP 版本呢?

HTTP 協議都是由客戶端向服務器發起請求,前面的問題也就轉換爲客戶端怎麼判斷服務端是否支持HTTP/2 協議(這個過程也稱爲協議發現)?由於HTTP/1.x 有HTTP 和 HTTPS 兩個版本,HTTP/2 也分別爲其提供了兩種協議發現機制:

  • 對“http” URI 的協議發現:客戶端會利用Upgrade 首部來表明期望使用HTTP/2,如果服務器也可以支持HTTP/2,它會返回一個“101 Switching Protocols”(協議轉換)響應,這增加了一輪完整的請求-響應通信;
  • 對“https” URI 的協議發現:客戶端在TLS 握手報文ClientHello 中設置ALPN(Application-Layer Protocol Negotiation)擴展來表明期望使用HTTP/2,服務器用同樣的方式回覆。如果使用這種方式,那麼HTTP/2 在創建TLS 握手的過程中完成協商,不需要多餘的網絡通信。

3.1 從"http" URI 啓動HTTP/2

對於沒有使用TLS 協議的HTTP/1.x 來說,要進行協議切換通常藉助HTTP 協議提供的通用首部字段Upgrade,比如從HTTP/1.1 切換到WebSocket 協議就是藉助Upgrade 字段實現的,從HTTP/1.1 切換到HTTP/2 同樣藉助該字段實現。下面使用curl 命令工具集,看看從HTTP/1.1 切換到HTTP/2 都發送了哪些標頭字段:

Admin@DESKTOP-PAUL C:\Users\Admin\Downloads\curl-7.70.0-win64-mingw\bin
> curl -v --http2 http://cn.bing.com -o D:\index.html
......
*   Trying 202.89.233.101:80...
* Connected to cn.bing.com (202.89.233.101) port 80 (#0)
> GET / HTTP/1.1
> Host: cn.bing.com
> User-Agent: curl/7.70.0
> Accept: */*
> Connection: Upgrade, HTTP2-Settings
> Upgrade: h2c
> HTTP2-Settings: AAMAAABkAAQCAAAAAAIAAAAA
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Cache-Control: private, max-age=0
< Content-Length: 112488
< Content-Type: text/html; charset=utf-8
......
* Connection #0 to host cn.bing.com left intact

使用HTTP/2 訪問"http://cn.bing.com" URI,需要在curl 命令中加上參數"–http2",在URI 後面加上"-o " 參數是爲了將報文主體輸出到其它地方,我們在交互界面只關注請求報文與響應報文的標頭字段信息。

請求報文字段"Upgrade: h2c" 標識客戶端向服務器請求升級爲"h2c" 協議,“h2c” 表示通過明文運行的HTTP/2 協議。從 HTTP/1.1 升級到 HTTP/2 的請求還必須包含一個 “HTTP2-Settings” 標頭字段,HTTP2-Settings 標頭字段中包含管理 HTTP/2 連接的參數。如果HTTP2-Settings 字段不存在,則服務器不得升級到 HTTP/2 的連接。

HTTP2-Settings: <base64url encoding of HTTP/2 SETTINGS payload>

HTTP2-Settings 標頭字段的內容是 SETTINGS 幀的有效負載,編碼爲 base64url 字符串(即 URL 的 Base64 編碼)。由於升級僅用於立即連接,因此發送 HTTP2-Settings header 字段的客戶端也必須在 Connection 頭字段中發送 “HTTP2-Settings” 作爲連接選項,以防止它被轉發。

雖然HTTP/2 的規範並不明確要求TLS,也支持以明文通信,但主流瀏覽器都不支持基於非TLS 的 “h2c” ,因此上面的示例代碼並沒有從HTTP/1.1 成功切換到“h2c” 協議(bing網站的服務器不支持 “h2c” 協議),仍然使用HTTP/1.1 響應報文繼續通信。如果協議切換成功(也即目標網站支持 “h2c” 協議),會通過"101 Switching Protocols"響應報文返回客戶端,響應報文字段如下:

< HTTP/1.1 101 Switching Protocols
< Connection: Upgrade
< Upgrade: h2c

[ HTTP/2 connection ...

在 HTTP/2 中,每個端點都需要發送Connection Preface 作爲正在使用的協議的最終確認,並建立 HTTP/2 連接的初始設置。因此,客戶端接收到"101 Switching Protocols"響應報文後,會立刻向服務器發送Connection Preface(其後還會緊跟一個SETTINGS 幀,用於連接的初始設置),客戶端發送的Connection Preface 以 24 個八位字節的序列開始,以十六進制表示法爲:

 0x505249202a20485454502f322e300d0a0d0a534d0d0a0d0a

解碼爲ASCII 是:

PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n

這個字符串的用處是,如果服務器(或者中間代理)不支持HTTP/2,就會產生並返回一個錯誤,讓HTTP/2 客戶端明確地知道發生了什麼錯誤。如果服務器支持HTTP/2,會聲明收到客戶端的SETTINGS 幀,並返回一個它自己的SETTINGS 幀(用於確認連接的初始設置),然後確認環境正常,就可以開始使用HTTP/2 進行通信了。

從上面"http" URI 啓動HTTP/2 的過程可以看出,中間涉及到HTTP/1.x 報文的明文傳輸,容易受到網絡攻擊,安全性肯定沒法保證。而且主流瀏覽器基本都不支持採用明文傳輸的HTTP/2(也即"h2c" 協議),這就導致從HTTP/1.x 切換到"h2c" 協議的成功率很低,且浪費了因切換協議額外增加的一輪請求 / 響應通信。因此,這種從"http" URI 升級爲"h2c" 協議的方案使用較少(該方案更多的用在從HTTP/1.x 升級到WebSocket 協議中)。

3.2 從"https" URI 啓動HTTP/2

HTTPS 基於 TLS 協議,TLS 協議在握手過程中可藉助ALPN(Application-Layer Protocol Negotiation)擴展來協商使用哪種應用層協議,客戶端與服務器協商應用層協議的任務在TLS 握手過程中完成,不需要增加額外的網絡通信。

從HTTPS 切換到HTTP/2 的任務也可以交由TLS 協議完成,客戶端在TLS 握手報文ClientHello 的ALPN 擴展中提交自己支持的應用層協議列表給服務器(可將HTTP/2 協議排在列表前面,表示優先期望使用HTTP/2 協議),服務器會根據自身的支持情況確定使用的應用層協議(是否使用HTTP/2,根據服務器支持情況而定)並使用相同的擴展(也即ALPN)回覆客戶端選定的應用層協議。

下面使用curl 命令工具集,看看從HTTPS 切換到HTTP/2 都發送了哪些標頭字段:

Admin@DESKTOP-PAUL C:\Users\Admin\Downloads\curl-7.70.0-win64-mingw\bin
> curl -v --http2 https://cn.bing.com -o D:\index.html
......
*   Trying 202.89.233.101:443...
* Connected to cn.bing.com (202.89.233.101) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: C:\Users\Admin\Downloads\curl-7.70.0-win64-mingw\bin\curl-ca-bundle.crt
  CApath: none
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384
* ALPN, server accepted to use h2
* Server certificate:
*  subject: CN=www.bing.com
*  start date: Apr 30 20:48:00 2019 GMT
*  expire date: Apr 30 20:48:00 2021 GMT
*  subjectAltName: host "cn.bing.com" matched cert's "*.bing.com"
*  issuer: C=US; ST=Washington; L=Redmond; O=Microsoft Corporation; OU=Microsoft IT; CN=Microsoft IT TLS CA 2
*  SSL certificate verify ok.
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x18dcd68b4c0)
> GET / HTTP/2
> Host: cn.bing.com
> user-agent: curl/7.70.0
> accept: */*
>
* Connection state changed (MAX_CONCURRENT_STREAMS == 100)!
< HTTP/2 200
< cache-control: private, max-age=0
< content-length: 112499
< content-type: text/html; charset=utf-8
......
* Connection #0 to host cn.bing.com left intact

使用HTTP/2 訪問"https://cn.bing.com" URI,需要在curl 命令中加上參數"–http2",其後的"-o" 參數作用前面介紹過了。

從上面的示例代碼可以看出,向目標主機的443端口發起連接後,客戶端先通過TLS 握手報文ClientHello中的ALPN 擴展告訴服務器,自己優先支持"h2" 應用層協議(使用TLS 的HTTP/2 協議),其次支持"http/1.1" 協議。

客戶端在TLS 握手報文ClientHello中也會告知服務器自己優先支持TLS 1.3(其次支持TLS 1.2,更低版本的目前已經被棄用),從上面的示例代碼可以看出,服務器選定的協議版本爲 TLS 1.2(可能是目標服務器不支持TLS 1.3),後續客戶端與服務器通過TLS 1.2協議完成握手。在博文TLS 1.2/1.3 握手過程中已經詳細介紹過TLS 協議的握手過程,下面再次展示TLS 1.2包含ALPN 擴展的完整握手過程如下:

   Client                                              Server

   ClientHello                     -------->       ServerHello
     (ALPN extension &                               (ALPN extension &
      list of protocols)                              selected protocol)
                                                   Certificate*
                                                   ServerKeyExchange*
                                                   CertificateRequest*
                                   <--------       ServerHelloDone
   Certificate*
   ClientKeyExchange
   CertificateVerify*
   [ChangeCipherSpec]
   Finished                        -------->
                                                   [ChangeCipherSpec]
                                   <--------       Finished
   Application Data                <------->       Application Data

TLS 1.2 協議完成後續的密鑰交換、身份認證、加密方案變更等握手過程,客戶端與服務器協商出的加密方案爲"TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384",協商使用的應用層協議爲"h2"(“ALPN, server accepted to use h2”),後續雙方使用HTTP/2 協議進行通信(“Using HTTP2, server supports multi-use”)。

借用TLS 協議的ALPN 擴展完成從HTTPS 升級爲 HTTP/2 的過程後,開始使用HTTP/2 進行通信前,依然需要雙方都發送Connection Preface 作爲正在使用的協議的最終確認(“Connection state changed (HTTP/2 confirmed)”)。Connection Preface 字符串的構成及含義前面已經介紹過了,這裏客戶端向服務器發送Connection Preface 字符串及其緊隨的SETTINGS 幀,服務器支持"h2" 協議並向客戶端返回SETTINGS 幀確認消息,客戶端與服務器完成HTTP/2 連接的初始設置(“Connection state changed (MAX_CONCURRENT_STREAMS == 100)”)。

客戶端與服務器完成HTTP/2 的最終確認和連接狀態變更後,就可以通過HTTP/2 二進制幀進行通信了,上面的示例代碼 curl 爲了更直觀的展示標頭字段內容,將HTTP/2 幀的標頭字段以類似HTTP/1.x 的ASCII 碼形式展示出來了,客戶端發送的請求行"GET / HTTP/2",服務器返回的響應行"HTTP/2 200",協議版本字段均爲HTTP/2。

藉助TLS 協議的ALPN 擴展從"https" URI 切換到HTTP/2 的過程不涉及 HTTP 報文的明文傳輸,顯著降低了被網絡攻擊的可能,網絡安全更有保障。通過ALPN 擴展協商應用層協議的過程直接在TLS 握手階段完成,不佔用額外的網絡通信,開銷也比通過Upgrade 標頭字段升級的方案更小。再加上目前主流瀏覽器實現的HTTP/2 都是基於TLS 協議的,不支持非TLS 的HTTP/2,所以使用TLS 協議的 ALPN 擴展來建立HTTP/2 連接的方案是絕對的主流。

四、HTTP/2 性能優化

HTTP 的性能優化可以從兩方面考慮:

  • 數據處理能力優化:HTTP/2 採用二進制幀來封裝數據報文,每個字段的長度固定,支持並行編解碼數據幀,對多線程處理器比較友好。HTTP/2 的標頭與主體數據壓縮算法的計算效率和對並行計算的支持,也是提高數據幀處理能力的一個方向;
  • 數據傳輸能力優化:HTTP 作爲網絡數據傳輸協議,提高數據傳輸性能是協議優化的關鍵環節,數據傳輸能力又可從往返時間RTT、傳輸數據量、網絡利用率三個方面着手。

HTTP 數據傳輸能力往往成爲制約整個協議性能提升的瓶頸,也是我們優化的重點,下面給出幾種優化思路:

優化項 優化方向
往返時間RTT 優化 影響網絡往返時間的主要有兩個因素:
1. 通信距離優化:可以使用前篇博文介紹的CDN(Content Delivery Network)技術實現;
2. 網絡擁塞控制:可以使用更合理的網絡擁塞控制算法改善擁塞程度,同時提高帶寬降低擁塞概率;
傳輸開銷優化 在保證通信對端能準確解析有效數據的前提下,儘量減少傳輸的數據量,也可從兩方面考慮:
1. 對傳輸數據進行壓縮:比如HTTP/2 中的標頭壓縮HPACK 和報文主體壓縮GZIP;
2. 對高頻數據進行緩存:對後續可能使用的數據進行緩存,可以減少重複傳輸的開銷;
網絡利用率優化 目前的HTTP協議都是基於TCP連接的,這裏給出幾個提高TCP連接利用率的優化建議:
1. TCP連接開銷優化:建立TCP連接是需要三次握手開銷的,應儘量減少建立TCP連接的數量並延長TCP連接時間,比如在一個TCP連接內併發傳輸多個數據流,達到多路複用一個TCP連接的效果;
2. TCP丟包重傳優化:設計TCP的初衷是優先保證數據傳輸的可靠性,丟包重傳機制對TCP的傳輸性能影響較大(特別在網絡環境較差時),算是TCP爲保證可靠性做的性能妥協,可通過使用UDP協議並在應用層重新設計丟包重傳機制實現性能優化,比如QUIC 協議便採用了這種優化思路;
3. TCP擁塞控制優化:可從改善網絡環境(比如提高帶寬),設計更優秀的擁塞控制算法,減少丟包重傳的概率(比如添加部分糾錯數據可恢復部分丟失的數據,不需要重傳丟失的數據包)等方向着手;

按照上面的優化方向考慮,HTTP/2 的設計還是相當優秀的,比如採用二進制幀封裝數據提高了數據幀的並行處理效率、標頭壓縮HPACK 的引入大幅降低了網絡傳輸開銷、數據流多路複用和服務端推送大幅提高了網絡利用效率等。看似HTTP/2 的設計很完美,充分發揮了TCP協議的性能,但TCP協議是以可靠性爲首要目標的,在優先保證可靠性的同時沒法過多兼顧性能提升,HTTP/2 自然也繼承了TCP 的性能瓶頸,比如丟包重傳機制會阻塞整個TCP連接。

前面介紹HTTP/2 通過多數據流併發複用同一個TCP連接解決了HTTP/1.x 的隊頭阻塞問題,在保證TCP連接不阻塞的情況下確實通過多路複用同一個TCP連接解決了隊頭阻塞問題,某一個數據流阻塞不會影響其它數據流的傳輸。但TCP連接不阻塞的前提並不總是成立,如果遇上丟包觸發了TCP的重傳機制,將會阻塞丟失數據包所在的整個TCP連接,該TCP連接內的所有併發數據流也都會被阻塞,隊頭阻塞問題又出現了(甚至因爲多路複用,阻塞的請求報文更多了)。TCP丟包重傳機制還會觸發慢啓動過程並縮小擁塞窗口,進一步降低網絡利用率。

怎麼徹底解決HTTP/2 的隊頭阻塞問題呢?可以通過重新設計TCP的丟包重傳機制來解決隊頭阻塞問題嗎?TCP 協議已經誕生四十多年,並應用在數以億萬計的各種網絡設備中,爲了保證現有的網絡設備能正常使用,TCP協議的更新必須保證前向兼容性,因此不可能對其進行重新設計。大多數現代操作系統爲了提供通用的網絡訪問能力,都在內核中提供了TCP/IP 協議棧的實現,如果要修改TCP 協議,通常需要更新操作系統內核,內核修改和操作系統更新的成本較高且頻率較低,也註定了TCP 協議的更新成本較高且更新頻率較低,因此大幅修改甚至重新設計TCP 協議的方案是不現實的。

既然不能從TCP 協議着手解決HTTP/2 的隊頭阻塞問題,那隻能放棄TCP協議,使用另一個傳輸層協議 – UDP 協議了。但UDP 協議的數據包相互獨立,沒有連接原語;沒有傳輸可靠性保證;沒有擁塞控制方案,無法適應不同的網絡條件。要保證HTTP 協議的可靠、高效傳輸,單靠UDP 協議是不夠的,我們需要在應用層實現類似TCP 的連接、可靠性、擁塞控制等特性。

基於UDP 在用戶空間重新實現TCP 協議棧的諸多特性,網絡開發者也就對應用數據的可靠傳輸、擁塞控制等方案的設計有了更大的控制權,吸收借鑑TCP 的優良特性,同時藉機重新設計制約TCP 性能的機制,比如前面提到的丟包重傳機制。在用戶空間實現可靠傳輸方案,也能降低了修改這些實現機制的成本,用戶只需要更新應用程序比如瀏覽器,就可以使用上更新後的實現機制。作爲下一代HTTP協議開發基礎的QUIC 協議就採用了上面的設計思想來進一步提升性能,具體的實現原理在下一篇博文:QUIC 是如何解決TCP 性能瓶頸的?中介紹。
QUIC 網絡層級結構圖

更多文章:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章