從Chrome源碼看HTTP/2

我在《怎樣把網站升級到http/2》介紹了升級到http/2的方法,並說明了http/2的優勢:javascript

  • http頭部壓縮
  • 多路複用
  • Server Push

下面一一進行說明。css

1. 頭部壓縮

爲何要進行頭部壓縮呢?我在《WebSocket與TCP/IP》說過:HTTP頭是比較長的,若是發送的數據比較小時,也得發送一個很大的HTTP頭部,以下圖所示:html

當這種請求數不少的時候,會致使網絡的吞吐率不高。而且,比較大的HTTP頭部會迅速佔滿慢啓動過程當中的擁塞窗口,致使延遲加大。因此HTTP頭的壓縮顯得頗有必要,HTTP/2的前身SPDY引入了deflate的壓縮算法,可是聽說這種容易受攻擊,HTTP/2使用了新的壓縮方法,在規範RFC 7541進行了說明。關於頭部壓縮,規範的附錄舉了個很生動的例子。這裏用這個例子作爲說明,解釋能夠怎麼對HTTP頭部進行壓縮。java

首先對經常使用的HTTP頭部字段進行編號,用一個靜態表格表示:node

Index Header Name Header Value
1 :authority
2 :method GET
3 :method POST
4 :path /
5 :path /index.html
6 :scheme http
7 :scheme https
8 status 200
9 status 204
... ... ...
16 accept-encoding gzip, deflate
17 accept-language
... ... ...
61 www-authenticate

總共有61個,其中冒號開頭的如:method是請求行裏的,2就表示Method: POST,若是要表示Method: OPTION呢?用下面的表示:nginx

0206OPTIONgit

其中02表示在靜態表格的索引index,查一下這個表格可知道2表示的Header Name爲:method。接着的06表示method名的長度爲6,後面緊接着就是字段的內容即method名爲OPTION。那它怎麼知道02後面跟着的06不是表示index爲6的":scheme http"的頭字段呢?由於若是Header Name和Header Value都是用的這個表的,如Method POST表示爲:github

0x82web

而不是02了,這裏就是把第8位置成了1,變成了二進制的1000 0002,表示name/value徹底匹配。而若是第8位不是1,如0000 0002那麼value值就是自定義的,後面緊跟着的一個字節就是表示value的字符長度,而後再跟着相應長度的字符。算法

value字符是使用霍夫曼編碼的,規範根據字符的使用頻率高低定了一個編碼表,這個編碼表把經常使用的字符的大小控制在5 ~ 7位,比ASCII編碼的8位要小一些。根據編碼表:

sym code as bits code as hex len in bits
'O' 1101010 6a 7
'P' 1101011 6b 7
'T' 1101111 6f 7
'I' 1100100 64 7
'N' 1101001 69 7

OPTION會被編碼爲:6a6b 6f64 6a69,因此Method: OPTION最終被編碼爲:

0206 6a6b 6f64 6a69

一共是8個字節,原先用字符串須要14個字節。

還有,若是有屢次請求,後面的請求有一些頭部字段和前面的同樣,那麼會用一個動態表格維護相同的頭部字段。若是name/value是在上面說的靜態表格都有的就不會保存到動態表格。動態表格能夠用一個棧或者動態數組來存儲。

例如,第一次請求頭部字段"Method: OPTION"在靜態表格沒有,它會被壓到一個棧裏面去,此時棧只有一個元素,用索引爲62 = 61 + 1表示這個字段,在接下來的第二次、第三次請求若是用到了這個字段就用index爲62表示,即遇到了62就表示Method: OPTION。若是又有其它一個自定義字段被壓到這個棧裏面,這個字段的索引就爲62,而Method: OPTION就變成了63,越臨近壓進去的編號就越往前。

靜態表格的index是從1開始,動態表格是從62開始,而index爲0的表示自定義字段名,用key長度 + key + value長度 + value表示,當把它這個自定義字段壓到動態表格裏面以後,它就有index了。固然,能夠控制是否須要把字段壓到動態表格裏面,經過設定標誌位,這裏不展開說明。

這個算法叫作HPACK,更詳細的過程能夠查看RFC 7541:HPACK: Header Compression for HTTP/2(能夠直接拉到最後面看例子)。

Chrome是在src/net/http2/hpack這個目錄作的頭部解析,靜態表格是在這個文件hpack_static_table_entries,以下圖所示:

根據文檔,動態表格默認最多的字段數爲4096:

// The last received DynamicTableSizeUpdate value, initialized to
  // SETTINGS_HEADER_TABLE_SIZE.
  size_t size_limit_ = 4096;  // Http2SettingsInfo::DefaultHeaderTableSize();複製代碼

可在傳輸過程當中動態改變,受對方能力的限制,由於不只是存本身請求的字段,還要有一個表格存對方響應的字段。

Chrome裏的動態表格是用一個向量vector的數據結構表示的:

const std::vector<HpackStringPair>* const table_;
複製代碼

vector就是C++裏面的動態數組。每次插入的時候就在數組前面插入:

table_.push_front(entry);複製代碼

而查找的時候,直接用數組的索引去定位,下面是查找動態數組的:

// Lookup函數
index -= kFirstDynamicTableIndex; // kFirstDynamicTableIndex等於62
if (index < table_.size()) {
  const HpackDecoderTableEntry& entry = table_[index];
  return entry;
}
return nullptr;複製代碼

頭部壓縮最主要的內容就說到這裏了,接下來講一下更爲厲害的多路複用。

2. 多路複用

傳統的HTTP/1.1爲了提升併發性,得經過提升鏈接數,即同時多發幾個請求,由於一個鏈接只能發一個請求,因此須要多創建幾個TCP鏈接。創建TCP鏈接須要線程開銷,咱們知道Chrome同一個域最多同時只能創建6個鏈接。因此就有了雪碧圖、合併代碼文件等減小請求數的解決方案。

在HTTP/2裏面,一個域只須要創建一次TCP鏈接就能夠傳輸多個資源。多個數據流/信號經過一條信道進行傳輸,充分地利用高速信道,就叫多路複用(Multiplexing)。

在HTTP/1.1裏面,一個資源經過一個TCP鏈接傳輸,一個大的資源可能會被拆成多個TCP報文段,每一個報文段都有它的編號,按照從前日後依次增大的順序,接收方把收到的報文段按照順序依次拼接,就獲得了完整的資源。固然,這個是TCP傳輸天然的特性,和HTTP/1.1沒有直接關係。

那麼怎麼用一個鏈接傳輸多個資源呢?HTTP/2把每個資源的傳輸叫作流Stream,每一個流都有它的惟一編號stream id,一個流又可能被拆成多個幀Frame,每一個幀按照順序發送,TCP報文的編號能夠保證後發送的幀的順序比先發送的大。在HTTP/1.1裏面同一個資源順序是依次連續增大的,由於只有一個資源,而在HTTP/2裏面它極可能是離散變大的,中間會插着發送其它流的幀,但只要保證每一個流按順序拼接就行了。如下圖所示:

爲何叫它流呢,由於數據就像水流同樣會流動,因此叫它爲流/數據流,流的特色是有序的,它是數據的一個序列。它能夠從鍵盤傳到內存,再由內存傳輸到硬盤,或者傳輸到服務端。

在通訊裏面,流被分紅若干幀,HTTP/2規定了11種類型的幀,包括HEADERS/DATA/SETTINGS等,HEADERS是用來傳輸http頭部的,DATA是用來發送請求/響應數據的,SETTINGS是在傳輸過程當中用來作控制的。一個幀的格式以下圖所示:

幀的頭部有9個字節,前3個字節(24位)表示幀有效數據(Frame Payload)的長度,因此每一個幀最大能傳送的數據爲2 ^ 24 = 16MB,但標準規定默認最大爲2 ^ 14 = 16Kb,除非雙方經過settings幀進行控制。第4個字節Type表示幀類型,如0x0表示data,0x1是headers,0x4是settings。Flags是每種幀用來控制一些參數的標誌位,如在data幀裏面第一個標誌位打開0x1是表示END_STREAM,即當前數據幀是當前流的最後一個數據幀。Stream Identifier是流的標誌符即流的編號,它的首位R是保留位(留着之後用)。最後就是Payload,當前幀的有效負載。

每一個請求都會建立一個流,每一個流的建立都是請求方經過發送頭部幀,即頭部幀用來打開一個流,每一個流都有它的優先級,放在頭部幀裏面。流的頭部幀還包含了上面第1點提到的HTTP壓縮頭部字段。

每一個流都有一個半關閉的狀態,當一方收到END_STREAM的時候,當前流就處於半關閉(remote)的狀態,這個時候另外一方再也不發送數據了,當前方也發一個END_STREAM給對方的時候,這個時候流就處於徹底關閉的狀態。已關閉的流的編號在當前鏈接不能複用,避免在新的流收到延遲的相同編號的老的流的幀。因此流的編號是遞增的。

更詳細的描述能夠參考這個文檔:Hypertext Transfer Protocol Version 2 (HTTP/2)

咱們以訪問Walking Dog這個頁面作爲說明,看一下流和幀是怎麼傳輸的,這個頁面總共加載13個資源:

包括index.html、main.js、main.css和10張圖片。

Chrome解碼HTTP/2幀的目錄在src/net/http2,而編碼的目錄在src/net/spdy。

所謂解碼就是按照格式解析接收到的http/2的幀,在Chrome裏面經過打印Log觀察這個過程,以下代碼示例:

按照打印的順序一一說明:

(1)SETTINGS(stream_id = 0; flags = 0; length = 18)

先是收到了一個SETTINGS的幀,payload內容爲:

parameter=MAX_CONCURRENT_STREAMS(0x3), value=128
parameter=INITIAL_WINDOW_SIZE(0x4), value=65536
parameter=MAX_FRAME_SIZE(0x5), value=16777215

另外一方即服務端(nginx)設置了max_concurrent_streams爲128,表示stream的最多併發數爲128,即同時最多隻能有128個請求。window_size是用來作流控制(Flow Control)的,表示對方接收的緩衝容量,分爲全局的緩衝容量和單個流的緩衝容量,若是stream_id爲0則表示全局,若是非0的話則是相應stream,上面服務設置初始化的window size爲64KB,在發送過程當中可能會調整這個值,當接收方的緩存空間滿了可能會置爲0發給對方告訴對方不要再給我發了,這個和TCP的擁塞窗口很像,可是這個是在應用層作的控制,能夠方便對每一個流的接收進行控制,例如當緩存空間不足時,優先級高的流可能給的window_size會更大一點。max_frame_size表示每一個幀的payload最大值,這裏設置成了最大值16MB。與此同時瀏覽器也給服務發了本身的settings。若是settings不是使用的標準規定的默認值,那麼就會傳遞settings幀。

而後收到了第二幀:

(2)WINDOW_UPDATE (stream id = 0; flag = 0; length = 4)

window_update類型的幀是用於更新window_size的,payload內容爲:

window_size_increment=2147418112

這裏把window_size設置成了最大值2GB,在第一幀裏的max_frame_size也是最大值,能夠說明服務沒有限制接收速度。這裏的stream id爲0也是表示這個配置是全局的。

那爲何不直接初始化的時候直接設置window_size呢,實現上就是這樣的。能夠對比一下,在連谷歌的gstatic.com的時候收到的幀是這樣的:

INITIAL_WINDOW_SIZE, value=1048576

MAX_HEADER_LIST_SIZE, value=16384

window_size_increment=983041

這樣看起來比較合理一點(另外它還設置了header頭部字段數最大值)。

在Chrome源碼裏面我只看到了一個地方使用到了window size,那就是當對方的window size爲0時,流就會排隊:

if (session_->IsSendStalled() || send_window_size_ <= 0) {
    return Requeue;
  }複製代碼

而經過nginx源碼,咱們發現nginx會在每發送一個幀的時候window size就會減掉當前幀大小:

ngx_http_v2_queue_frame(h2c, frame);
h2c->send_window -= frame_size; 
stream->send_window -= frame_size;複製代碼

發送成功後進行cleanup又會把它加回來,若是send_window變成0,就會進行排隊。

(3)SETTINGS (stream id = 0; flag = 1; length = 0)

第三幀也是settings,可是此次沒有內容,長度爲0,設置flag爲1表示ACK,表示認同瀏覽器給它發的SETTINGS。flags在不一樣類型的幀有不一樣的含義,以下代碼所示:

enum Http2FrameFlag {
  END_STREAM = 0x01,   // DATA, HEADERS 表示當前流結束
  ACK = 0x01,          // SETTINGS, PING settings表示接受對方的設置,而ping是用來協助對方測量往返時間的
  END_HEADERS = 0x04,  // HEADERS, PUSH_PROMISE, CONTINUATION 表示header部分完了
  PADDED = 0x08,       // DATA, HEADERS, PUSH_PROMISE 表示payload後面有填充的數據
  PRIORITY = 0x20,     // HEADERS 表示有當前流有設置權重weight
};複製代碼

接着收到第4幀:

(4)HEADERS (stream id = 1; flag = 4; length = 107)

這個是請求響應頭,是inde.html的,flag爲4表明END_HEADERS,表示這個header只有一幀,若是flag爲0就說明後面還有。而後Chrome會對收到的頭部進行逐字節解析,按照上面提到的頭部壓縮的方式的逆過程。

先取出第一個字節,判斷是什麼類型的header,indexed或者非indexed,所謂indexed就是指查表能查到的,如第一個字節是0x82就是IndexedHeader。而後再把高位去掉,剩下0x02表示表的索引:

若是是IndexedHeader那麼就會去動態表和靜態表查:

不然的話就得去解析l字符串key/value和length,有可能使用了霍夫曼,也有可能沒有,代碼裏面作了判斷:

uint8_t h_and_prefix = db->DecodeUInt8();
bool huffman_encoded = (h_and_prefix & 0x80) == 0x80;
複製代碼

Chrome也是維護了一個霍夫曼表:

解析完一對key/value頭部字段以後,可能會把它壓入動態表裏面。接着繼續解析下一個,直到length完了。

(5)DATA (stream id = 1; flag = 1; length = 385)

header幀以後就是index.html的數據幀了,這裏flag爲1表示END_STREAM,長度爲385,由於數據比較小,一個幀就發送完了。

咱們把收到的payload直接打印出來是這樣的(我把gzip關了,否則打印出來是壓縮過的內容):

這個內容就是html文本,咱們看到HTTP/2並無對發送內容進行處理,只是對發送的形式進行了控制。常常說HTTP/2是二進制的,應該是說幀的頭部是二進制,可是內容該怎麼樣仍是怎麼樣。

(6)HEADERS(stream id = 3; flag = 4; length = 160)

接着又收到了一個頭部幀,這個是main.css的響應頭,stream id爲3.

(7)DATA (stream id = 3; flag = 1; length = 827)

這個是main.css的數據幀:

(8)HEADERS (stream id = 5; flag = 4; length = 171)

這個是main.js的響應頭

(9)DATA(DATA stream id = 5; flag = 1; length = 4793)

main.js的payload,以下圖所示:

(10)HEADERS(HEADERS stream id = 7; flag = 4; length = 163)

這個是0.png的響應頭

(11)DATA (stream id = 7; flag = 0; length = 8192)

0.png的payload:

注意這裏的flag是0,不是1,說明後面還有數據幀。緊接着又收到了一幀:

(12)DATA (stream id = 7; flag = 1; length = 2843)

這個的stream id仍是7,可是flag爲1表示END_STREAM。說明0.png(11KB)被拆成了兩幀發送。

咱們發現stream的id都是奇數的,這是由於這些stream都是瀏覽器建立的,主動鏈接一方stream id使用奇數,而另外一方觸發建立的stream使用偶數,主要經過Server Push。


如今把Chrome發送的幀加進來,有了上面的基礎再來理解Chrome的應該不難。

Chrome也會發送它的settings幀給對方,在初始化session的時候作的,代碼是在net/spdy/chromium/spdy_session.cc的SendInitialData函數裏面。咱們把感興趣的幀按順序打印出來:

(1)SETTINGS

內容以下:

HEADER_TABLE_SIZE, value = 65536

MAX_CONCURRENT_STREAMS, value = 1000

INITIAL_WINDOW_SIZE, value = 6291456

Chrome作爲接收方的時候流的最高併發數爲1000.

(2)WINDOW_UPDATE

新增的window size大小爲:

window_size_increment = 15663105

大概爲15Mb。

接着,Chrome的IO線程取出一個request任務,取到的第一個是請求index.html,打印以下:

../../net/spdy/chromium/spdy_http_stream.cc (95) SpdyHttpStream::InitializeStream : request url = www.rrfed.com/html/walking-dog/index.html

它先建立一個HEADERS的幀

(3)HEADERS(index.html stream_id = 1; weight = 256; dependency = 0; flag = 1)

咱們把幀頭部的另外兩個參數打印出來:weight權重和dependency依賴的流。權重爲256,而依賴的流爲0即沒有。權重用來作來優先級的參考,值的範圍爲1 ~ 256,因此當前流擁有最高的優先級。依賴下文再說起。

在收到了html以後,Chrome解析到了main.css的link標籤和main.js的script標籤,因而又再從新初始化兩個流,流都是經過發HEADERS的幀打開的。

(4)HEADERS(main.css stream_id = 3; weight = 256; dependency = 0; flag = 1 )

main.css也擁有最高的優先級

(6)HEADERS (main.js stream_id = 3; weight = 220; dependency = 3;flag = 1)

main.js的權重爲220,這個script文件的權重要比html/css的小,而且它的依賴於id爲3即main.css的流。

收到JS以後,解析JS,這個JS裏面又觸發加載了9張png圖片,接着Chrome一口氣初始化了9個流:

(7)~ (15)

0.png stream_id = 7, weight = 147, dependent_stream_id = 0, flags = 1

1.png stream_id = 9, weight = 147, dependent_stream_id = 7, flags = 1

2.png stream_id = 11, weight = 147, dependent_stream_id = 9, flags = 1

...

能夠看到圖片的權重又比script小,而且這些圖片的流有一個依賴關係,後一個流依賴於前一個流。

依賴關係是在HEADER的payload裏面指定的,以下圖所示:

優先級依賴priority dependencies和窗口大小window size是HTTP/2進行多路複用控制的兩個最主要的方法。這個依賴有什麼用呢?以下文檔的說明:

Inside the dependency tree, a dependent stream SHOULD only be allocated resources if either all of the streams that it depends on (the chain of parent streams up to 0x0) are closed or it is not possible to make progress on them.

意思是說一個依賴的子結點只有等到它全部的父結點都處理完了纔可以給它分配資源進行處理。換句話說在這棵優先級依賴樹裏面,父結點比子結點擁有更高的處理優先級。文檔裏面只說明瞭優先級依賴樹的一些特性,並無說明應該如何實現,只是說不一樣的場景能夠有不一樣的實現。Chrome又是怎麼實現的呢?

它是用一個二維數組,第一維是priority,即用一個數組放優先級相同的stream id,當初始化一個流的時候就會把它放到相應priority的數組裏面去。注意這裏的priority是指spdy3的屬性,從最高優化級的0到最低優化級7,和權重weight([1, 256])有一個轉化關係,這裏不討論是怎麼轉換的,它們表達的意思是同樣的。以下代碼所示,把stream添加到相應優先級數組的後面:

id_priority_lists_[priority].push_back(std::make_pair(stream_id, priority));複製代碼

可是這個二維數組並無創建父子結點的關係,只是藉助它能夠知道當前流的父結點應該是哪一個。計算當前流父結點的代碼實現邏輯是在http2_priority_dependencies.cc這個文件裏面,爲了方便理解,我把它轉成JS代碼,它的思想是要找到比當前流的優先級離得最近且不低於當前優先級的一個流,代碼以下所示:

let stream_id = 1, // 當前流的id,從外面傳進來
    priority = 0, // 當前流的優先級,從外面傳進來的
    id_priority_lists_ = []; // 它是一個二維數組
id_priority_lists_[0] = [];  // 這裏先初始化一下
const kV3HighestPriority = 0; // 最高優先級爲0

let dependent_stream_id = 0; // 父結點的stream id
for (let i = priority; i >= kV3HighestPriority; i--) {
    let length = id_priority_lists_[i].length;
    if (length) {
        dependent_stream_id = id_priority_lists_[i][length - 1];
        break;
    }
}
id_priority_lists_[priority].push(stream_id);複製代碼

這段代碼應該比較好理解,在for循環裏面從當前優先級一直往高優化級找,直到找到一個,若是沒有,那麼它的parent stream就是0,表示它是一個root結點。

另外,一旦流關閉了以後,就會把當前流從二維數組裏面移除。因此在收到html以後,html的流就被刪了,因此新建的CSS流就沒有依賴的父結點了,可是緊接着新建的JS流它的優先級比CSS流低,因此這個JS流的父結點就是CSS流。

咱們看到Chrome並無實際地存放這麼一棵樹,只是藉助這麼一個二維數組找到當前stream的父結點,設置在HEADER幀的dependency,而後傳給服務端,告訴服務端這些流的優先級依賴關係。

服務端又是怎麼利用這些優先級依賴關係的呢?咱們以nginx爲例,經過nginx的源碼,能夠大體知道nginx是怎麼操做的,nginx是有真正創建一棵依賴樹的,每個流都會對應一個node結點。每一個結點都會記錄它的父結點parent,它的子結點集children,以及當前結點的weight和rank,以下代碼所示:

struct ngx_http_v2_node_s {
    ngx_uint_t                       id;
    ngx_http_v2_node_t              *index;
    ngx_http_v2_node_t              *parent;
    ngx_queue_t                      queue;
    ngx_queue_t                      children;
    ngx_queue_t                      reuse;
    ngx_uint_t                       rank;
    ngx_uint_t                       weight;
    double                           rel_weight;
    ngx_http_v2_stream_t            *stream;
};複製代碼

在實際計算中是用的rel_weight相對權重和rank排名,這個相對權重和排名是利用weight和dependency計算的:

// 若是當前結點沒有父結點,那麼它的排名就爲第一
if (parent == null) {
    node->rank = 1;
    node->rel_weight = (1.0 / 256) * node->weight;
}
// 不然的話,它的排名就是父結點的排名加1
// 而它的相對權重就是父結點的 weight/256
else {
    node->rank = parent->rank + 1;
    node->rel_weight = (parent->rel_weight / 256) * node->weight;
}複製代碼

能夠看到子結點的相對權重rel_weight等於父結點的weight/256倍,注意weight <= 256,而子結點的排名rank是排在父結點的後一位。當前結點的weight和父結點是哪一個是瀏覽器經過HEADERS幀告訴的,也就是說nginx利用這兩個參數把當前流節點插入這個依賴樹裏面,經過這棵樹計算出當前結點的排名和相對權重。

知道這兩個有什麼用呢?

當流的併發數超過最高併發數max_concurrent_streams時,或者緩存空間buffer用完了,這個時候要把當前流放到waiting_queue裏面,這個隊列有一個順序,越靠前的元素就能越快處理,優先級越高就越靠前。當把一個須要waiting的stream插入到這個隊列的時候就須要用到優先級排名決定要插到哪一個位置,以下ngx_http_v2_waiting_queue函數的實現:

// stream表示要插入的流
stream->waiting = 1;

// 從waiting列隊的最後一個元素開始,依次往前遍歷,直到完了
for (q = ngx_queue_last(&h2c->waiting);
     q != ngx_queue_sentinel(&h2c->waiting);
     q = ngx_queue_prev(q))
{
    // 取出當前元素的數據
    s = ngx_queue_data(q, ngx_http_v2_stream_t, queue);

    // 這段代碼的核心在於這個判斷
    // 若是要插入的流的排名比當前元素的排名要靠後,
    // 或者排名相等可是相對權重比它小,就插到它後面。
    if (s->node->rank < stream->node->rank
        || (s->node->rank == stream->node->rank
            && s->node->rel_weight >= stream->node->rel_weight))
    {   
        break;
    }   
}

// 這裏執行插入,若是stream的優先級比隊列的任何一個元素
// 都要高的話,就插到隊首去了
ngx_queue_insert_after(q, &stream->queue);複製代碼

把當前stream插到比它優先級稍高的一個元素的後面去,利用了rank和rel_weight. rank是由dependency決定,而rel_weight主要是由weight決定。

咱們發現nginx在發送幀隊列的時候也是用的相似的判斷來決定幀的發送順序,以下ngx_http_v2_queue_frame函數代碼:

if ((*out)->stream->node->rank < frame->stream->node->rank
    || ((*out)->stream->node->rank == frame->stream->node->rank
        && (*out)->stream->node->rel_weight
           >= frame->stream->node->rel_weight))
{
    break;  
}複製代碼

HTTP/2的多路複用就介紹到這裏,上面說的流都是由瀏覽器主動打開的,而HTTP/2的Server Push的流是由服務觸發打開的。

3. Server Push

當咱們使用HTTP/1.1的時候,Chrome最多同時加載6個:

而當咱們使用HTTP/2的時候,就沒有這個限制,有的是流的最大併發數,如上面提到的100個,以下圖所示:

咱們觀察到圖片的加載完成時間是從上往下的,這個就應該是上面提到的優先級依賴影響的,後面的圖片會依賴於前面的圖片,因此前面的圖片優先級會更高一點,優先傳遞。時間線裏面綠色的是表示Waiting TTFB(Time To First Byte)的時間,即發出請求以後到收到第一個字節所須要的時間,藍色是內容下載時間。這裏能夠看到等待時間TTFB依次增加。

雖然使用了HTTP/2沒有了6個的限制,可是咱們發現css/js須要在html解析了以後才能觸發加載,而圖片是經過JS的new Image觸發加載,因此它們須要等到JS下載完並解析好了才能開始加載。

因此Server Push就是爲了解決這個加載延遲問題,提早把網頁須要的資源Push給瀏覽器。Nginx 1.13.9版本開始支持,是在最近(2018/2)纔有的。經過編譯一個新版本的nginx就能體驗Server Push的功能,給nginx.conf添加如下配置:

location = /html/walking-dog/index.html {
    http2_push /html/walking-dog/main.js?ver=1;
    http2_push /html/walking-dog/main.css;
    http2_push /html/walking-dog/dog/0.png;
    http2_push /html/walking-dog/dog/1.png;
    http2_push /html/walking-dog/dog/2.png;
    http2_push /html/walking-dog/dog/3.png;
    http2_push /html/walking-dog/dog/4.png;
    http2_push /html/walking-dog/dog/5.png;
    http2_push /html/walking-dog/dog/6.png;
    http2_push /html/walking-dog/dog/7.png;
    http2_push /html/walking-dog/dog/8.png;
}複製代碼

指定須要Push的資源,而後觀察加載的時間線:

咱們發現加載時間有了一個質的改變,基本上在100ms左右就加載完了。因此Sever Push用得好的話做用仍是挺大的。

Server Push的流是經過Push Promise類型的幀打開的,Promise的幀格式以下圖所示:

它其實就是一個請求的HEADER幀,可是它和HEADER又不太同樣,沒有weight/dependency那些東西。

按照上面的方式,觀察一下加上Push Promise以後,幀傳遞的過程是怎麼樣的。

瀏覽器在stream_id = 1的流裏面請求加載index.html,這個時候服務並無馬上響應頭部和數據,而是先連續返回了11個Push Promise的幀,stream的id分別爲二、四、6等,而後瀏覽器馬上建立了相應的stream,收到了2的promise以後就建立2的stream,收到4的以後就建立4的。這個時候流建立好了就開始加載,不用等到解析到html或者js以後纔開始。

在這個過程當中,Chrome會先解析promised stream id,以下圖所示:

而後再去解析Hpack的頭部,再用這個頭部去建立流。Server Push咱們就再也不深刻討論了,讀者能夠打開這個網址感覺一下。


綜上,咱們主要討論了HTTP/2的三大特性:

(1)頭部壓縮,經過規定頭部字段的靜態表格和實際傳輸過程當中動態建立的表格,減小多個類似請求裏面大量冗餘的HTTP頭部字段,而且引入了霍夫曼編碼減小字符串常量的長度。

(2)多路複用,只使用一個TCP鏈接傳輸多個資源,減小TCP鏈接數,爲了可以讓高優先級的資源如CSS等更先處理,引入了優先級依賴的方法。因爲併發數很高,同時傳遞的資源不少,若是網速很快的時候,可能會致使緩存空間溢出,因此又引入了流控制,雙方經過window size控制對方的發送。

(3)Server Push,解決傳統HTTP傳輸中資源加載觸發延遲的問題,瀏覽器在建立第一個流的時候,服務告訴瀏覽器哪些資源能夠先加載了,瀏覽器提早進行加載而不用等到解析到的時候再加載。

國內使用HTTP/2的還沒怎麼見到,淘寶以前是有開啓HTTP/2的,不知道爲何如今又下掉了, 國外使用HTTP/2的網站仍是不少的,像谷歌搜索、CSS-Tricks、Twitter、Facebook等都使用了HTTP/2。若是你有本身的網站的話,能夠嘗試一下。


相關閱讀:

  1. 怎樣把網站升級到http/2
  2. 從Chrome源碼看HTTPS
  3. 從Chrome源碼看HTTP
相關文章
相關標籤/搜索