>From: https://www.ibm.com/developerworks/cn/web/wa-http2-under-the-hood/index.html
HTTP/2 的首要目標是改善 Web 應用程序用戶的體驗。作為一個二進制協議,它擁有包括輕量型、安全和快速在內的所有優勢。HTTP/2 保留了原始 HTTP 協議的語義,但更改了在系統之間傳輸數據的方式。這些復雜細節主要由客戶端和服務器管理,所以網站和應用程序無需重大更改即可享受 HTTP/2 的優勢。
本文將概述 HTTP/2,包括它試圖解決的問題,以及它的大量新的性能增強特性 — 包括請求/響應復用、報頭壓縮和服務器推送。
## HTTP 的歷史
在深入介紹 HTTP/2 協議的細節之前,讓我們回到過去并回顧一下HTTP 中的起源。
該協議于 1989 年首次曝光,以 HTTP 0.9 的形式面世。Timothy Berners-Lee 在瑞士日內瓦附近的 CERN 上首次提到它時,它僅包含 1 行代碼。唯一的方法是 `GET`,還有一個像下面這個示例這樣簡單的請求:`GET /index.html`。響應同樣很簡單,僅包含所請求的文件。
HTTP 0.9 不是一個正式標準,通過這種方式引用它是為了將它與隨后的正式版本區分開。1996 年,推出了 HTTP 1.0 作為 IEFT 標準(依據 [RFC 1945](https://tools.ietf.org/search/rfc1945))。1999 年,在 [RFC 2616](https://tools.ietf.org/search/rfc2616) 中發布了 HTTP 1.1。第一個主要版本中的缺點促使人們在 1999 年進行了一次小幅修訂,引入了大量可選特性和零碎細節 — 并消除了一些不好的方面。
幾乎沒有瀏覽器(或服務器)實現會采用該協議的每個方面,這導致不同瀏覽器間的用戶體驗不一致。顯然,瀏覽器供應商無法實現 HTTP 1.1 中引入的 HTTP 管道的性能增強特性。
**您可以親自查看**:在最近的一次[演示](https://blog.cloudflare.com/http-2-demo-under-the-hood/)中,Cloudflare 通過 HTTP 1.1 和 HTTP/2 加載了 200 個圖像切片,然后比較了加載時間。
隨著網絡的使用變得更加普遍,性能需求呈指數級增長,而對 HTTP 的需求阻礙了性能提升。開發人員開始創建工具來克服該協議的不足。例如,HTTP 對 TCP 套接字的低效使用限制了性能,所以開發人員退而使用精心設計的服務器架構(racks of servers)來滿足應用程序需求。從這個角度講,未能讓管道正常工作,促使人們開始著重反思 HTTP 的需求。
15 年后,才成立了 [HTTPbis](https://datatracker.ietf.org/wg/httpbis/charter/) 工作組來正式識別該協議的棘手問題,并最終起草對 HTTP/2 的預期。懷著顯著改善最終用戶對 HTTP 1.1 延遲的認知的使命,該工作組的協議推薦包含針對 “線頭阻塞” 問題、報頭壓縮和服務器推送的精選解決方案。[RFC 7540](https://tools.ietf.org/html/rfc7540) (HTTP/2) 和 [7541](https://tools.ietf.org/html/rfc7541) (HPACK) 的結合,預示著 Web 應用程序性能將急劇提升。
## HTTP 的現狀
自萬維網誕生以來,網頁變得愈加復雜。[第一批網頁](http://info.cern.ch/hypertext/WWW/TheProject.html)非常簡單,僅包含文本:沒有圖像,沒有 CSS,沒有 JavaScript,只有普通的 HTML。快進到今天,平均每個網頁就包含 100 多個下載資源,大小約為 2,500 KB。總傳輸大小自 [2012 年 5 月](http://httparchive.org/trends.php?s=All&minlabel=May+1+2012&maxlabel=May+1+2017#bytesTotal&reqTotal)以來增長了 250%,這種持續增長沒有出現緩和跡象。
##### 圖 1. 總傳輸大小和總請求數 (2012-2017),來源:[HTTPArchive](http://httparchive.org/trends.php?s=All&minlabel=May+1+2012&maxlabel=May+1+2017#bytesTotal&reqTotal)

### 工具和變通方案
盡管互聯網確實能快速提供高度復雜的內容,但出現這樣的結果并不是因為 HTTP 1.1 協議(*盡管*采用了該協議)。在當前版本中,HTTP 無法滿足如今的 Web 體驗需求。因此,Web 開發人員針對這些性能問題提供了一系列變通方案。讓我們來看一些比較流行的工具和它們修補的問題。
#### 線頭阻塞
HTTP 1.0 僅允許通過一個 TCP 連接發出一個請求。這引發了所謂的 “線頭阻塞” 問題,迫使瀏覽器等待緩慢的響應。HTTP 1.1 通過*管道*解決了這個問題,管道使瀏覽器能并行發出多個請求。但是,瀏覽器供應商很難實現管道,而且大多數瀏覽器(包括 Firefox)在發布時都會默認禁用該特性。Chrome 甚至完全刪除了它。
#### 多個 TCP 連接
打開 TCP 連接需要很高的成本,而且我們對客戶端應如何使用它們知之甚少。唯一的協議規定是,每個主機最多可以打開 [2](https://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html#sec8.1.4) 個連接。由于只有 2 個 TCP 連接,開發人員為了能夠展示一個現代頁面需要競爭這兩個名額 — 所以他們找到了一種方法來繞過這一限制。
通過使用一種稱為*域分片(domain sharding)*的流行技術,開發人員能創建多個主機,每個主機提供一個網站所需資源的一部分。切分已變得非常普遍,網頁加載期間打開的平均 TCP 連接數量也因此達到約 35 個(來源:[HTTPArchive](http://httparchive.org/trends.php?s=All&minlabel=May+1+2012&maxlabel=May+1+2017#_connections))。
瀏覽器供應商不甘示弱,他們也違反了該協議,任意增加瀏覽器實現中允許的開放連接數量。這有助于并行化各個瀏覽器中的資源加載,但沒有充分利用 TCP 套接字。下表顯示了每個主機名允許打開的端口的最高數量,以及最流行的 3 個瀏覽器在這方面的不同。
##### 表 1. 并行打開的 TCP 連接的最大數量(來源:[browserscope.org](https://www.browserscope.org/))
| 瀏覽器 | 每個主機名的最大并行連接數 |
| --- | --- |
| Chrome | 24 |
| Firefox | 6 |
| Internet Explorer 12 | 11 |
瀏覽器實現中的不一致意味著,用戶沖浪體驗的質量取決于他們選擇的瀏覽器,而不是網站的設計和構思有多精巧。
#### 資源內聯和級聯
為了追求更高性能,Web 應用程序開發人員采用的聰明技巧并不只有域切分。
* **文件串聯**創建一個包含全部所需資源的大文件。為網站的所有 CSS 創建一個文件,為 JavaScript 創建一個文件,為包含網站圖標的圖像子畫面表創建另一個文件。
* **資源內聯**將 CSS 和 JavaScript 直接嵌入在 HTML 中,這使得嵌入圖像也成為可能。對圖像進行 base64 編碼,然后在加載網頁時進行解碼。
這些技術都不可取,尤其是從設計角度講。在這兩種情況下,頁面的結構都與樣式組合在一起,圖像解碼也會消耗很多時間。緩存也無法輕松實現。
但是,如果目標只是減少請求的文件數量,那么這些變通方案是成功的。隨著文件請求減少,需要打開的 TCP 套接字也會減少。
## 最吸引人的特性
HTTP/2 的大多數實用特性歸功于 Google 在 SPDY 協議上開展的工作。在 HTTPbis 工作組開始起草 HTTP/2 RFC 的第一個版本時,SPDY 已證明一個主要 HTTP 版本更新切實可行。因為已經部署并開始采用 SPDY,所以有證據表明更新的協議在自然環境下具有更高的性能。
HTTP/2 成功的關鍵在于,它實現了顯著的性能改善,同時保持了 HTTP 范例,以及 HTTP 和 HTTPS 模式。該工作組規定,向 HTTP/2 的遷移必須透明,而且使用者不會受到任何影響。
該協議最吸引人的特性包括:
* 新升級路徑
* 二進制分幀
* 請求/響應復用
* 報頭壓縮
* 流優先化
* 服務器推送
* 流控制
讓我們來查看每個特性。
### 新升級路徑
HTTP/2 升級路徑與標準路徑稍有不同,省去了一些協商。對于基于 HTTP/2 的安全連接,無法通過升級標頭請求切換協議,并收到一條讓人安心的“101 switching”HTTP 狀態。相反,通過使用一個名為應用層協議協商 (ALPN) 的新擴展,客戶端向服務器告知它能理解的通信協議(按偏好排序)。服務器然后使用該列表中它也理解的第一個協議作為響應。
SPDY 需要一個安全連接,雖然社區迫于壓力會建立這樣的連接,但 HTTP/2 規范沒有強制要求這么做。但是,所有主要瀏覽器供應商都僅在 TLS 上實現 HTTP/2,而且不支持不安全的連接。這實際上會迫使 Web 應用程序實現者對所有 HTTP/2 流量使用 TLS(來源:[caniuse.com](http://caniuse.com/#search=http2))。curl 用戶仍可采用通過 HTTP 升級標頭的升級路徑,因為它將實現既明確又安全的連接。
### 二進制協議
或許 HTTP/2 的最重要改變是轉換為二進制協議。對于開發人員,這可以說是性能增強的焦點。新協議稱為*二進制分幀層(binary framing layer)*,它重新設計了編碼機制,而沒有修改方法、動詞和標頭的熟悉語義。
最重要的是,所有通信都在單個 TCP 連接上執行,而且該連接在整個對話期間一直處于打開狀態。這可能得益于二進制協議將通信分解為幀的方式:這些幀交織在客戶端與服務器之間的雙向邏輯流中。
#### 連接的拓撲結構
正如我提到的,在 HTTP/2 的新范例中,僅在客戶端與服務器之間建立了一個 TCP 連接,而且該連接在交互持續期間一直處于打開狀態。在此連接上,消息是通過邏輯流進行傳遞的。一條*消息*包含一個完整的幀序列。在經過整理后,這些幀表示一個響應或請求。
圖 2 演示了連接組件之間的關系,展示了一個用于建立多個流的連接。在流 1 中,發送了一條請求消息,并返回了相應的響應消息。
##### 圖 2. HTTP/2 連接的拓撲結構

我們將分別查看每個概念。
#### 連接和流
僅與一個對等節點建立一個連接,并在該連接上傳輸多個流。因為流可以交織,所以可以同時快速的傳輸多個流。
#### 消息
*消息*是一組幀。在對等節點上重建這些幀時,它們形成一個完整的請求或響應。特定消息的幀在同一個流上發送,這意味著一個請求或響應只能映射到一個可識別的流。
#### 幀
*幀*是通信的基本單位。每個幀有一個標頭,其中包含幀的長度和類型、一些布爾標志、一個保留位和一個流標識符,如圖 3 所示。
##### 圖 3. 幀分解

[點擊查看大圖](https://www.ibm.com/developerworks/cn/web/wa-http2-under-the-hood/index.html#N10174)
#### 長度
*length 字段*記錄幀的大小,它最多可在一個 `DATA` 幀中攜帶 224 個字節(約 16 MB),但默認的最大值設置為 214 個字節 (16 KB)。幀大小可以通過協商調得更高一點。
#### 類型
*type 字段*標識幀的用途,可以是以下 10 種類型之一:
* `HEADERS`:幀僅包含 HTTP 標頭信息。
* `DATA`:幀包含消息的所有或部分有效負載。
* `PRIORITY`:指定分配給流的重要性。
* `RST_STREAM`:錯誤通知:一個推送承諾遭到拒絕。終止流。
* `SETTINGS`:指定連接配置。
* `PUSH_PROMISE`:通知一個將資源推送到客戶端的意圖。
* `PING`:檢測信號和往返時間。
* `GOAWAY`:停止為當前連接生成流的停止通知。
* `WINDOW_UPDATE`:用于管理流的流控制。
* `CONTINUATION`:用于延續某個標頭碎片序列。
參見規范的 [11.2 節](https://tools.ietf.org/html/rfc7540#section-11.2)了解每種幀類型的功能的更多細節。
#### 標志
*flag 字段*是一個布爾值,指定幀的狀態信息:
* `DATA` 幀可定義兩個布爾標志:`END_STREAM` 和 `PADDED`,前者表示數據流結束,后者表示存在填充數據。
* `HEADERS` 幀可以將相同的標志指定為 `DATA` 幀,并添加兩個額外的標志:`END_HEADERS` 和 `PRIORITY`,前者表示標頭幀結束,后者表示設置了流優先級。
* `PUSH_PROMISE` 幀可以設置 `END_HEADERS` 和 `PADDED` 標志。
所有其他幀類型都無法設置標志。
#### 流標識符
*流標識符*用于跟蹤邏輯流的幀成員關系。成員每次僅屬于一條消息和流。流可以提供優先級建議,這有助于確定分配給它的網絡資源。我稍后會更詳細地解釋流優先化。
### 請求/響應復用
單一 TCP 連接的問題在于,一次只能發出一個請求,所以客戶端必須等到收到響應后才能發出另一個請求。這就是 “線頭阻塞” 問題。正如之前討論的,典型的變通方案是打開多個連接;每個請求一個連接。但是,如果可以將消息分解為更小的獨立部分并通過連接發送,此問題就會迎刃而解。
這正是 HTTP/2 希望達到的目標。將消息分解為幀,為每幀分配一個流標識符,然后在一個 TCP 連接上獨立發送它們。此技術實現了完全雙向的請求和響應消息復用,如下圖所示。
##### 圖 4. 在 TCP 連接上交織的幀

圖 4 中的圖解顯示在一個連接上快速傳輸了 3 個流。服務器發送兩個響應,客戶端發送一個請求。
在流 1 中,服務器為一個響應發送 `HEADERS` 幀;在流 2 中,它為另一個響應發送 `HEADERS` 幀,隨后為兩個響應發送 `DATA` 幀。兩個響應按如圖所示的方式交織。在服務器發送響應的過程中,客戶端發送一條新消息的 `HEADERS` 和 `DATA` 幀作為請求。這些幀也與響應幀交織在一起,如下圖所示。
##### 圖 5. HTTP/2 將請求/響應幀交織在一起

[點擊查看大圖](https://www.ibm.com/developerworks/cn/web/wa-http2-under-the-hood/index.html#N1021F)
所有幀在另一端重新組裝,以形成完整的請求或響應消息。
幀交織有許多好處:
* 所有請求和響應都在一個套接字上發生。
* 所有響應或請求都無法相互阻塞。
* 減少了延遲。
* 提高了頁面加載速度。
* 消除了對 HTTP 1.1 工具的需求。
##### 圖 6. 將 HTTP 請求映射到 HTTP/2 幀

我們將左側的一個 HTTP 請求映射到右側的一個 `HEADERS` 幀。
在 `HEADERS` 幀中,設置了兩個標志。第一個是 `END_STREAM`,它設置為 true(由加號表示),表明該幀是給定請求的最后一幀。`END_HEADERS` 標志也設置為 true,表明該幀是流中最后一個包含標頭信息的幀。
`HEADERS` 幀中的標頭屬性反映了 HTTP 1.1 請求中設置的屬性。因為 HTTP/2 一定要保持 HTTP 協議的語義,所以必須這么做。
接下來,讓我們來看看該請求的響應。
#### 將 HTTP 請求映射到幀
圖 7 的左側是一個 HTTP 1.1 標頭響應。右側是使用兩個 HTTP/2 幀表示的同一個響應:`HEADERS` 和 `DATA`。
##### 圖 7. 將 HTTP 響應映射到 HTTP/2 幀

在 `HEADERS` 幀中,`END_STREAM` 表明該幀不是流中的最后一幀,而 `END_HEADER` 表明它是最后一個包含標頭信息的幀。在 `DATA` 幀中,`END_STREAM` 表明它是最后一幀。
### 報頭壓縮
HTTP/2 協議擁有配套的 HPACK。HPACK 的目的是減少客戶端請求與服務器響應之間的標頭信息重復所導致的開銷。*報頭壓縮*的實現方式是,要求客戶端和服務器都維護之前看見的標頭字段的列表。未來在構建引用了已看見標頭列表的消息時可以使用此列表。
##### 圖 8. 壓縮同一個連接上的兩個請求的標頭

在圖 8 中的兩個請求中,標頭信息是重復的。唯一的不同在請求的資源上(已采用黃色突出顯示)。HPACK 報頭壓縮可以在這里派上用場。在第一個請求后,它僅需發送與前一個標頭的不同之處,因為服務器保留著以前看見的標頭的列表。除非設置了標頭值,否則會假設后續請求擁有與之前的請求相同的標頭值。
### 流優先化
消息幀通過流進行發送。每個流都分配了一個優先級,用于確定它的處理順序,以及它將收到的資源量。
將該優先級輸入到給定流的標頭幀或優先級幀中,優先級可以是 0 到 256 之間的任何數字。
可以定義依賴關系,允許在一個資源之前加載另一個資源。也可以將優先級組合到一個*依賴樹*中,讓開發人員對分配給每個流的重要性有更多控制權。
##### 圖 9. 用于流優先化的依賴樹

在圖 9 中,字母表示流標識符,數字表示分配給每個流的權重。樹的根是流 A,首先會向它分配資源,然后才向依賴它的流 B 和 C 分配資源。為流 B 分配了 40% 的可用資源,流 C 收到了 60% 的可用資源。流 C 是流 D 和 E 的父流,二者分別從其父流收到相同的資源配額。
流優先級僅是對服務器的建議,可以動態更改或完全忽略。在起草 HTTP/2 協議的過程中,工作組認為允許客戶端強迫服務器遵守特定資源分配是不對的。相反,服務器可以自由調整優先級,使其與自己的能力匹配。
### 服務器推送
*服務器推送*使服務器能預測客戶端請求的資源需求。然后,在完成請求處理之前,它可以將這些資源發送到客戶端。
要了解服務器推送的好處,可以考慮一個包含圖像和其他依賴項(比如 CSS 和 JavaScript 文件)的網頁。客戶端發出一個針對該網頁的請求。服務器然后分析所請求的頁面,確定呈現它所需的資源,并主動將這些資源發送到客戶端的緩存。在執行所有這些操作的同時,服務器仍在處理原始網頁請求。客戶端收到原始網頁請求的響應時,它需要的資源已經位于緩存中。
那么 HTTP/2 如何管理服務器推送而不會讓客戶端過載?針對希望發送的每個資源,服務器會發送一個 `PUSH_PROMISE` 幀,但客戶端可通過發送 `RST_STREAM` 幀作為響應來拒絕推送(例如,如果瀏覽器的緩存中已包含該資源)。重要的是所有 `PUSH_PROMISE` 都在響應數據之前發送,所以客戶端知道它需要請求哪些資源。
### 流控制
*流控制*管理數據的傳輸,使發送者不會讓接收者不堪重負。它允許接收者停止或減少發送的數據量。例如,參閱一個提供點播視頻的流媒體服務。觀看者觀看一個視頻流時,服務器正在向客戶端發送數據。如果視頻暫停,客戶端會通知服務器停止發送視頻數據,以避免耗盡它的緩存。
打開一個連接后,服務器和客戶端會立即交換 `SETTINGS` 幀來確定流控制窗口的大小。默認情況下,該大小設置為約 65 KB,但可通過發出一個 `WINDOW_UPDATE` 幀為流控制設置不同的大小。
## HTTP/2 的普及情況
供應商幾乎都采用了 HTTP/2。在瀏覽器領域,[所有主要瀏覽器](http://caniuse.com/#search=http2)目前都只支持基于 TLS 的新協議。在編寫本文時,全球的支持率已超過 80%。
服務器支持率也有所增加,所有主要服務器系列的當前版本都支持 HTTP/2。您的托管服務提供商很可能已支持 HTTP/2。可以在 HTTP/2 規范的 [Wiki 頁面](https://github.com/http2/http2-spec/wiki/Implementations)上跟蹤它的所有已知服務器實現。
工具支持也很豐富,所有您[最喜歡的實用工具](https://github.com/http2/http2-spec/wiki/Tools)都支持 HTTP/2。Wireshark 對希望調試服務器與客戶端之間的 HTTP/2 通信的開發人員最重要。
## HTTP/2 與您的關系
Web 用戶不關心您使用何種協議來提供內容,只要它速度夠快就行。您可能已通過優化網站加載資源的方式,努力為客戶提供他們想要的資源。借助 HTTP/2,您不再需要串聯文件,將圖標整理到一個圖像中,設置大量域,或者內聯資源。
簡言之,HTTP/2 避免了對變通方案的需求。事實上,繼續使用我在本文中介紹的性能工具,可能阻礙您的網站從 HTTP/2 性能增強中受益。
所以對大多數開發人員而言,最重要的問題是:現在是否適合針對 HTTP/2 重構我的網站?在我看來,這很大程度上取決于與應用程序組成和所使用的瀏覽器相關的因素。以下是一個平衡法則:您不希望不公平對待使用舊瀏覽器的用戶,但希望提供更快的整體用戶體驗。
針對 HTTP/2 的優化是一個未知領域,尤其是在最佳實踐方面。它不僅僅是消除變通方案并期待獲得最佳成果的一種途徑。我們每個人都必須親自研究。在此過程中,我們會發現提升性能的新方法、HTTP/2 在自然環境下的運行效果,哪個服務器擁有最高性能的實現,等等。
對 Web 開發而言,HTTP/2 代表著一個美好的新世界。大膽的開發人員在接受它帶來的挑戰的同時也將獲得收益。