1. 程式人生 > >HTTP/2技術整理

HTTP/2技術整理

1. HTTP協議發展

1.1. HTTP的歷史

HTTP於1989年正式釋出,也就是HTTP/1啦,在經歷10年後於1999年更新出了HTTP/1.1,也是我們現在普遍使用的版本。

2015年初HTTP/2標準正式發表,取代HTTP1.1成為HTTP的實現標準。也就是說,到現在HTTP/2才出現不到3年。

(具體的發展可參考維基百科:

1.2. 瞭解HTTP

1.2.1. 討論環境:

相對於我們後臺開發來說,對前端這塊的概念相對比較薄弱,比如我們開發了一個BOS物流專案,已經放在tomcat上面,現在我們可以通過http://localhost:8080/bos/index.html來進行訪問。那麼客戶端瀏覽器是怎麼拿到首頁的資源的?瀏覽器和伺服器直接究竟是如何通訊的呢?

1.2.2. HTTP通訊過程

在這裡主要有3個過程:

建立TCP連線:也就是瀏覽器與伺服器的3次握手.

客戶端請求: 建立TCP連線後,客戶端就會向伺服器傳送一個HTTP請求資訊(比如請求HTML資源,我們暫且就把這個稱為“HTML請求”)

伺服器響應: 伺服器接收到請求後進行處理將HTML響應回去。

當然,接下來還有瀏覽器解析渲染的過程~等等~我們才能最終看到頁面~~

接下來我們著重看下HTTP/1.0和HTTP/1.1在這三個過程中的不同之處:

1.3. HTTP/1.0的通訊

HTTP/1.0下,每完成一次請求和響應,TCP連線就會斷開。但我們知道,客戶端傳送一個請求只能請求一個資源,而我們的首頁

index.html不可能只有單單一個HTML檔案吧?至少還要有CSS吧?還要有圖片吧?於是又要一次TCP連線,然後請求和響應。

下圖展示了HTTP/1.0請求一個HTML和一個CSS需要經歷的兩次TCP連線:

1.4. HTTP/1.1的通訊

要知道,TCP連線有RTT(RoundTripTime,即往返時延)的,每請求一個資源就要有一次RTT,使用者可是等不得這種慢節奏的響應的。於是到了HTTP/1.1,TCP可以持久連線了,也就是說,一次TCP連線要等到同域名下的所有資源請求/響應完畢了連線才會斷開。恩!聽起來情況好像好了很多,請求同域名下的n個資源,可以節約(n-1)*RTT的時間。

下圖展示了

HTTP/1.1時請求一個HTML和一個CSS只需要經歷一次TCP連線:

1.5. HTTP優化

但前面提到了,客戶端傳送一個請求只能請求一個資源,那麼我們會產生如下疑問:

1.5.1. 為什麼不一次傳送多個請求?

事實上,HTTP/1.x多次請求必須嚴格滿足先進先出(FIFO)的佇列順序:傳送請求,等待響應完成,再發送客戶端隊伍中的下一個請求。也就是說,每個TCP連線上只能同時有一個請求/響應。這樣一來,伺服器在完成請求開始回傳到收到下一個請求之間的時間段處於空閒狀態。

1.5.2. 有什麼辦法去改變嗎?

“HTTP管道”技術實現了客戶端向伺服器並行傳送多個請求。而伺服器也是可以並行處理多個請求的。這麼一來,不就可以多路複用了嗎?但是,HTTP/1.x有嚴格的序列返回響應機制,通過TCP連線返回響應時,就是必須一對一,前一個響應沒有完成,下一個響應就不能返回。所以使用“HTTP管道”技術時,萬一第一個響應時間很長,那麼後面的響應處理完了也無法傳送,只能被快取起來,佔用伺服器記憶體,這就是傳說中的“隊首阻塞”。

1.5.3. 既然一個TCP連線解決不了問題,那麼可以開多個嗎?

既然一條通道(TCP連線)通訊效率低,那麼就開多條通道唄!的確,HTTP/1.1下,瀏覽器是支援同時開啟多個TCP會話的(一般為6個)。一個TCP只能響應一個請求,那麼六個TCP豈不就能達到六倍速?想想還有點兒小激動!但事情往往不是這麼簡單。開啟多個TCP會話,無疑會給客戶端和伺服器都帶來負擔,比如快取、CPU時鐘週期等,而且並行的TCP也會競爭頻寬,並行能力也是受限制的,往往無法達到理想狀態下的六倍速。

從下面這張谷歌瀏覽器的網路監控可以看出每次請求6個:

 

2. HTTP的使命

可見,我們採取了許多方法,希望可以並行處理請求/響應,但都不能從根本上解決問題。況且,很多方法與HTTP/1.x的設計理念是背道而馳的,在HTTP/1.x下,卻沒有正確利用好HTTP/1.x的特性。

於是,HTTP/2帶著提高效能的使命,應運而生。

那麼HTTP/2做了什麼改變?

先對HTTP/2產生的影響有一個直觀的認識:

這裡有個Akamai公司(全球最大的CDN服務商)的一個官方演示,他是分別使用HTTP/1.1和HTTP/2請求379張小圖片,最終拼成一副大圖片,然後對比消耗時間。

大家可以點開觀察一下效果。這裡我擷取一下我電腦的結果:

 

大家可以自己試試,我這個結果有點逗逼,測試了幾次都是HTTP/1.1在20s以上,HTTP/22s以內。和網上別人的HTTP/1.1在7s以內差距有點大。但更可以明顯看出,HTTP/2下載入時間和HTTP/1.1都不在一個數量級,那麼HTTP/2到底為什麼這麼快?我們還是從它的新特性來進行全面的瞭解。

以下著重介紹五個特性:二進位制分幀層多向請求與響應優先順序和依賴性首部壓縮伺服器推送

3. HTTP/2五大特性

3.1. 二進位制分幀層

二進位制分幀層(BinaryFramingLayer)指的是位於套接字介面與應用可見的高層HTTP API之間的一個新機制:HTTP的語義,包括各種動詞、方法、首部,都不受影響,不同的是傳輸期間對它們的編碼方式變了。

在新引進的二進位制分幀層上,HTTP/2將所有傳輸的資訊分割為更小的訊息和幀,且都採用二進位制格式的編碼。

說了那麼多都什麼gui,也沒聽懂,還是看圖吧:


從圖上可以看到:在上面HTTP API和下面TCP連線中引入了一個二進位制分幀層;在二進位制分幀層上,它將我們以前普通的HTTP請求的請求頭和請求正文分割成了兩個部分:HEADERS幀和DATA幀。請求頭起始行、首部被分割到HEADERS幀,實體正文被分割到DATA幀。

接下來,我們再深入地瞭解下這些被分割後的二進位制幀是怎麼工作的:

HTTP/2同域名的所有通訊都是在一個TCP連線上完成,這個連線可以承載任意數量的雙向資料流。而每個資料流都是以訊息的形式傳送的,訊息由一個幀或多個幀組成。

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

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

幀:HTTP/2通訊的最小單位,每個幀包含幀首部

好像很複雜的樣子,咱們來捋一捋:

TCP連線在客戶端和伺服器間建立了一條運輸的通道,可以雙向通行,當一端要向另一端傳送訊息時,會先把這個訊息拆分成幾部分(幀),然後通過發起一個流對這些幀進行傳送,最後在另一端將同一個流的幀重新組合。

這個過程就好像我們在搬家的時候,會把一個桌子先拆散成零部件,然後通過幾次的搬運,到了新家後,再把桌子重新拼裝起來。

下圖展示了流、訊息與幀的關係(注意到沒,HEADERS幀總是在最前面的):


HTTP/2規範一共規定了10種不同的幀,其中最基礎的兩種分別對應於HTTP/1.1的DATA幀和HEADERS幀。

3.2. 多向請求與響應(多路複用)

多路複用允許同時通過單一的TCP連線發起多重的請求/響應訊息,客戶端和伺服器可以把HTTP訊息分解為互不依賴的幀,然後亂序傳送,最後再在另一端根據 StreamID 把它們重新組合起來。

前面提到的一端傳送訊息會先對訊息進行拆分,與此同時,也會給同一個訊息拆分出來的幀帶上一個編號(StreamID),這樣在另一端接收這些幀後就可以根據編號對它們進行組合。

也正是有了這種編號的方式,當某一端傳送訊息時,可以傳送多個訊息拆分出來的多個幀(發起多個流),且這些幀可以亂序傳送,因為這些幀都有自己的編號,它們之間互不影響。

下圖展示了單一的TCP連線上有多個請求/響應並行交換:


從圖上可以看出,伺服器向客戶端傳送stream1的多個DATA幀(說明HEADERS幀已傳送完畢)與stream3的HEADERS幀和DATA幀,客戶端正在向伺服器傳送stream5的DATA幀,可見,幀的傳送是亂序的,且請求/響應是並行的。

細心的你會發現,stream1中有多個DATA幀,這是為什麼呢?因為有DATA幀有長度的控制(2的14次方-1位元組,約16383個位元組),應用資料過大時,會被拆分成多個DATA幀(還記得講二進位制分幀層展示的HTTP/1.1的請求被分割成更小的幀嗎?DATA幀就是用來攜帶應用資料的)。

3.3. 優先順序和依賴性

新建流的終端可以在報頭幀中包含優先順序資訊來對流標記優先順序。

優先順序的目的是允許終端表達它如何讓對等端管理併發流時分配資源。更重要的是,在傳送容量有限時優先順序能用來選擇流來傳輸幀。

HTTP/2中,流可以有一個優先順序屬性(即“權重”):

可以在HEADERS幀中包含優先順序priority屬性;

可以單獨通過PRIORITY幀專門設定流的優先順序屬性。

流的優先順序用於發起流的終端(客戶端/伺服器)向對端(接收的一方)表達需要多大比重的資源支援,但這只是一個建議,不能強制要求對端一定會遵守。

藉助於 PRIORITY幀,客戶端同樣可以告知伺服器當前的流依賴於其他哪個流。該功能讓客戶端能建立一個優先順序“樹”,所有“子流”會依賴於“父流”的傳輸完成情況。

不依賴任何流的流的流依賴為 0x0。換句話說,不存在的流標識0組成了樹的根。

我們通過以下幾個例子來理解下優先順序“樹”:


第一種情況:流A和流B不依賴流,即為0x0;流A的權重為12,流B的權重為4;則流A分配到的資源佔比為12/(12+4)=12/16,流B分配到的資源佔比為4/(12+4)=4/16。

第二種情況:流D為0x0,流C依賴於流D;流D能被分配到全額資源,等到流D關閉後,依賴於流D的流C也會被分配到全額資源(它是唯一依賴於流D的流,它的權重的大小此時並不重要,因為沒有競爭的流)。

第三種情況:流D為0x0,流C依賴於流D,流A和流B依賴於流C;流D能被分配到全額資源,等到流D關閉後,依賴於流D的流C也會被分配到全額資源;等到流C關閉後,依賴於流C的流A和流B根據權重分配資源(3:1)。

第四種情況:流D為0x0,流C和流E依賴於流D,流A和流B依賴於流C;流D能被分配到全額資源,等到流D關閉後,依賴於流D的流C的流E和流B根據權重分配資源(1:1);等到流C關閉後,依賴於流C的流A和流B根據權重分配資源(3:1)。

前面說到,“可以單獨通過PRIORITY幀專門設定流的優先順序屬性”,也就是說可以對原本沒有優先順序屬性(包括依賴關係)的流進行設定,也可以對原本已有優先順序屬性的流進行修改。因此,優先順序可以在傳輸過程中被動態的改變。

3.4. 首部壓縮

HPACK是專門為HTTP/2量身定製的為有效地表示HTTP首部欄位的壓縮技術。

在伺服器和客戶端各維護一個“首部表”,表中用索引代表首部名,或者首部鍵-值對,上一次傳送兩端都會記住已傳送過哪些首部,下一次傳送只需要傳輸差異的資料,相同的資料直接用索引表示即可。

具體實現如下圖所示:


這個過程比較容易理解:通過索引表的對應關係,來標記首部表中的不同資訊。

同一個域名下的請求/響應的首部往往有很多重複的資訊,當客戶端要向伺服器傳送某個請求時,通過查詢索引表,發現該資訊的首部已經發送過,此時伺服器端的索引表也應該有對應的資訊,則不需要再次傳送;若查詢發現部分首部資訊不在索引表中,則傳送該部分信首部息即可。

如在上圖的示例中,第二個請求只需要傳送變化了的路徑首部(:path),其他首部沒有變化,就不用再發送了。

比如我第一次請求index.html,那麼我攜帶所有的資訊過去,第二次如果我請求該伺服器下面的login.html,那麼只需要攜帶這個路徑:path/login.html就行了,別的資訊不用攜帶,減少資料傳送量。

3.5. 伺服器推送

伺服器推送(ServerPush),伺服器可以對一個客戶端請求傳送多個響應。也就是說,除了對最初請求的響應外,伺服器還可以額外向客戶端推送資源。

在瞭解“二進位制分幀層”的時候我們提到,“HTTP/2規範規定了10種不同的幀”,其中有一種名為“PUSH_PROMISE”,就是在伺服器推送的時候傳送的。當客戶端解析幀時,發現它是一個PUSH_PROMISE型別,便會準備接收服務端要推送的流。


從上圖可以看出,當伺服器響應了HTML請求後,可以知道客戶端接下來要傳送JS請求、CSS請求,於是伺服器通過推送的方式(主動發起新流,而不是等客戶端請求然後再響應),向客戶端發出要約(PUSH_PROMISE)。當然,客戶端可以選擇快取這個資源,也可以拒絕這個資源。

這個過程有點類似於我們常用的資源內嵌的手段:將一個圖片資源轉為base64編碼嵌入CSS檔案中,當客戶端發起CSS請求時,也會請求該圖片。因此在響應CSS請求後,伺服器會強制(客戶端是無法拒絕的)向客戶端傳送圖片響應。但內嵌資源是無法被單獨快取的,而伺服器推送的資源是可以被快取的。

需要注意,伺服器必須遵循請求-響應的迴圈,只能藉著請求的響應來推送資源,也就是說,如果客戶端沒有傳送請求,伺服器是沒法先手推送的。而且,如上圖中stream4,PUSH_PROMISE幀必須在返回響應(DATA幀)之前傳送,因為萬一客戶端請求的恰好是伺服器打算推送的資源,那傳輸過程就會混亂了。

注:由客戶端發起的流StreamID為奇數,由伺服器發起的流StreamID為偶數,回顧上面的圖就能發現啦!

4. 小結

u HTTP/2通過二進位制分幀與多路複用機制,有效解決了HTTP/1.x下請求/響應延遲的問題。

新的首部壓縮技術使HTTP/1.x首部資訊臃腫的問題得到解決。

優先順序和依賴性與伺服器推送使得我們可以更有效地利用好這個單一的TCP連線。

可見,HTTP/2在HTTP/1.1的基礎上有了一個較大的效能提升。這時候你會發現,我們針對HTTP/1.x的一些優化手段(如上文提到的資源內嵌)似乎有點不適用了。