從理論到實踐 全面理解HTTP/2

前言

爲了下降加載時間,相信大多數人都作過以下嘗試javascript

  • Keep-alive: TCP持久鏈接,增長了TCP鏈接的複用性,但只有當上一個請求/響應徹底完成後,client才能發送下一個請求
  • Pipelining: 可同時發送多個請求,可是服務器必須嚴格按照請求的前後順序返回響應,若第一個請求的響應遲遲不能返回,那後面的響應都會被阻塞,也就是所謂的隊頭阻塞
  • 請求合併:雪碧圖,css/js內聯、css/js合併等,然而請求合併又會帶來緩存失效、解析變慢、阻塞渲染、木桶效應等諸多問題
  • 域名散列:繞過了同域名最多6個TCP的限制,但增長了DNS開銷和TCP開銷,也會大幅下降緩存的利用率
  • ……

不能否認,這些優化在必定程度上下降了網站加載時間,但對於一個web應用龐大的請求量來講,這些只是冰上一角、隔靴搔癢。css

以上問題歸根結底是HTTP1.1協議自己的問題,若要從根本上解決HTTP1.1的低效,只能從協議自己入手。爲此Google開發了SPDY協議,主要是爲了下降傳輸時間;基於SPDY協議,IETF和SPDY組全體成員共同開發了HTTP/2,並在2015年5月以RFC 7504正式發表。SPDY或者HTTP/2並非一個全新的協議,它只是修改了HTTP的請求與應答在網絡上的傳輸方式,增長了一個spdy傳輸層,用於處理、標記、簡化和壓縮HTTP請求,因此它們並不會破壞現有程序的工做,對於支持的場景,使用新特性能夠得到更快的速度,對於不支持的場景,也能夠實現平穩退化。html

HTTP/2繼承了spdy的多路複用、優先級排序等諸多優秀特性,也額外作了很多改進。其中較爲顯著的改進是HTTP/2使用了一份通過定製的壓縮算法,以此替代了SPDY的動態流壓縮算法,用於避免對協議的Oracle攻擊。java

多數主流瀏覽器已在2015年末支持了該標準(劃重點)。具體支持度以下:node

HTTP/2支持度

數據來源git

能夠看到國內有58.55%的瀏覽器已經徹底支持HTTP/2,而全球的支持度更是高達85.66%。這麼高的支持度,so,你心動了嗎github

why HTTP/2

二進制格式傳輸

咱們知道HTTP/1.1的頭信息確定是文本(ASCII編碼),數據體能夠是文本,也能夠是二進制(須要作本身作額外的轉換,協議自己並不會轉換)。而在HTTP/2中,新增了二進制分幀層,將數據轉換成二進制,也就是說HTTP/2中全部的內容都是採用二進制傳輸。web

使用二進制有什麼好處嗎?固然!效率會更高,並且最主要的是能夠定義額外的幀,若是用文本實現幀傳輸,解析起來將會十分麻煩。HTTP/2共定義了十種幀,較爲常見的有數據幀、頭部幀、PING幀、SETTING幀、優先級幀和PUSH_PROMISE幀等,爲未來的高級應用打好了基礎。算法

HTTP/2

如上圖,Binary Framing就是新增的二進制分幀層。express

多路複用

二進制分幀層把數據轉換爲二進制的同時,也把數據分紅了一個一個的幀。幀是HTTP/2中數據傳輸的最小單位;每一個幀都有stream_ID字段,表示這個幀屬於哪一個流,接收方把stream_ID相同的全部幀組合到一塊兒就是被傳輸的內容了。而流是HTTP/2中的一個邏輯上的概念,它表明着HTTP/1.1中的一個請求或者一個響應,協議規定client發給server的流的stream_ID爲奇數,server發給client的流ID是偶數。須要注意的是,流只是一個邏輯概念,便於理解和記憶的,實際並不存在。

理解了幀和流的概念,完整的HTTP/2的通訊就能夠被形象地表示爲這樣:

HTTP/2幀和流通訊示意圖

能夠發現,在一個TCP連接中,能夠同時雙向地發送幀,並且不一樣流中的幀能夠交錯發送,不須要等某個流發送完,才發送下一個。也就是說在一個TCP鏈接中,能夠同時傳輸多個流,便可以同時傳輸多個HTTP請求和響應,這種同時傳輸不須要遵循先入先出等規定,所以也不會產生阻塞,效率極高。

在這種傳輸模式下,HTTP請求變得十分廉價,咱們不須要再時刻顧慮網站的http請求數是否太多、TCP鏈接數是否太多、是否會產生阻塞等問題了。

HPACK 首部壓縮

爲何須要壓縮?

在 HTTP/1 中,HTTP 請求和響應都是由「狀態行、請求 / 響應頭部、消息主體」三部分組成。通常而言,消息主體都會通過 gzip 壓縮,或者自己傳輸的就是壓縮事後的二進制文件(例如圖片、音頻),但狀態行和頭部卻沒有通過任何壓縮,直接以純文本傳輸。

隨着 Web 功能愈來愈複雜,每一個頁面產生的請求數也愈來愈多,根據 HTTP Archive 的統計,當前平均每一個頁面都會產生上百個請求。愈來愈多的請求致使消耗在頭部的流量愈來愈多,尤爲是每次都要傳輸 UserAgent、Cookie 這類不會頻繁變更的內容,徹底是一種浪費。

爲了減小冗餘的頭部信息帶來的消耗,HTTP/2採用HPACK 算法壓縮請求和響應的header。下面這張圖很是直觀地表達了HPACK頭部壓縮的原理:

HAPCK原理

圖片來源: Velocity 2015 • SC 會議分享

具體規則能夠描述爲:

  • 通訊雙方共同維護了一份靜態表,包含了常見的頭部名稱與值的組合
  • 根據先入先出的原則,維護一份可動態添加內容的動態表
  • 用基於該靜態哈夫曼碼錶的哈夫曼編碼數據

當要發送一個請求時,會先將其頭部和靜態表對照,對於徹底匹配的鍵值對,能夠直接使用一個數字表示,如上圖中的2:method: GET,對於頭部名稱匹配的鍵值對,能夠將名稱使用一個數字傳輸,如上圖中的19:path: /resource,同時告訴服務端將它添加到動態表中,之後的相同鍵值對就用一個數字表示了。這樣,像cookie這些不常常變更的值,只用發送一次就行了。

server push

在開始HTTP/2 server push 前,咱們先來看看一個HTTP/1.1的頁面是如何加載的。

<!DOCTYPE html>
<html>
<head>
  <link rel="stylesheet" href="style.css">
  <script src="user.js"></script>
</head>
<body>
  <h1>hello http2</h1>
</body>
</html>
複製代碼
  1. 瀏覽器向服務器請求/user.html
  2. 服務器處理請求,把/user.html發給瀏覽器
  3. 瀏覽器解析收到的/user.html,發現還須要請求/user.jsstyle.css靜態資源
  4. 分別發送兩個請求,獲取/user.jsstyle.css
  5. 服務器分別響應兩個請求,發送資源
  6. 瀏覽器收到資源,渲染頁面

至此,這個頁面才加載完畢,能夠被用戶看到。能夠發如今步驟3和4中,服務器一直處於空閒等待狀態,而瀏覽器到第6步才能獲得資源渲染頁面,這使頁面的首次加載變得緩慢。

而HTTP/2的server push容許服務器在未收到請求時就向瀏覽器推送資源。即服務器發送/user.html時,就能夠主動把/user.jsstyle.csspush給瀏覽器,使資源提早達到瀏覽器;除了靜態文件,還能夠推送比較耗時的API,只是須要提早將參數和cookie等信息經過某個方式告知服務端(如和路由關聯)。Apache、GO的net/http、node-spdy都實現了server push(但ngnix沒有=_=),本文後面的實踐部分用node-spdy寫了一個極爲簡陋的例子,有興趣的小夥伴能夠動手嘗試一下。

Server push是HTTP/2協議裏面惟一一個須要開發者本身配置的功能。其餘功能都是服務器和瀏覽器自動實現,無需開發者介入。

在HTTP1.1時代,也有提早獲取資源的方法,如preload和prefetch,前者是在頁面解析初期就告訴瀏覽器,這個資源是瀏覽器立刻要用到的,能夠馬上發送對資源的請求,當須要用到該資源時就能夠直接用而不用等待請求和響應的返回了;後者是當前頁面用不到但下一頁面可能會用到的資源,優先級較低,只有當瀏覽器空閒時纔會請求prefetch標記的資源。從應用層面上看,preload和server push並無什麼區別,可是server push減小瀏覽器請求的時間,略優於preload,在一些場景中,能夠將二者結合使用。

實戰

紙上談兵終覺淺,來實踐一下吧!親手搭建本身的 HTTP/2 demo,並抓包驗證。

spdy這個庫實現了 HTTP/2,同時也提供了對express的支持,因此這裏我選用spdy + express搭建demo。demo源碼

路徑說明:

- ca/  證書、祕鑰等文件
- src/
    - img/
    - js/
    - page1.html
- server.js
複製代碼

HTTPS 祕鑰和證書

雖然HTTP/2有加密(h2)和非加密(h2c)兩種形式,但大多主流瀏覽器只支持h2-基於TLS/1.2或以上版本的加密鏈接,因此在搭建demo前,咱們首先要自頒發一個證書,這樣就能夠在瀏覽器訪問中使用https了,你能夠自行搜索證書頒發方法,也能夠按照下述步驟去生成

首先要安裝open-ssl,而後執行如下命令

$ openssl genrsa -des3 -passout pass:x -out server.pass.key 2048
....
$ openssl rsa -passin pass:x -in server.pass.key -out server.key
writing RSA key
$ rm server.pass.key
 $ openssl x509 -req -sha256 -days 365 -in server.csr -signkey server.key -out server.crt
....
$ openssl x509 -req -sha256 -days 365 -in server.csr -signkey server.key -out server.crt
複製代碼

而後你就會獲得三個文件server.crt, server.csr, server.key,將它們拷貝到ca文件夾中,稍後會用到。

搭建HTTP/2服務

express是一個Node.js框架,這裏咱們用它聲明瞭路由/,返回的html文件page1.html中引用了js和圖片等靜態資源。

// server.js
const http2 = require('spdy')
const express = require('express')
const app = express()
const publicPath = 'src'

app.use(express.static(publicPath))

app.get('/', function (req, res) {
    res.setHeader('Content-Type', 'text/html')
    res.sendFile(__dirname + '/src/page1.html')
})

var options = {
    key: fs.readFileSync('./ca/server.key'),
    cert: fs.readFileSync('./ca/server.crt')
}
http2.createServer(options, app).listen(8080, () => {
    console.log(`Server is listening on https://127.0.0.1:8080 .`)
})
複製代碼

用瀏覽器訪問https://127.0.0.1:8080/,打開控制檯能夠看全部的請求和它們的瀑布圖:

沒有push的瀑布圖

能夠清楚地看到,當第一個請求,也就是對document的請求徹底返回並解析後,瀏覽器纔開始發起對js和圖片等靜態資源的的請求。前面說過,server push容許服務器主動向瀏覽器推送資源,那麼是否能夠在第一個請求未完成時,就把接下來所需的js和img推送給瀏覽器呢?這樣不只充分利用了HTTP/2的多路複用,還減小了服務器的空閒等待時間。

對路由的處理函數進行改造:

app.get('/', function (req, res) {
+   push('/img/yunxin1.png', res, 'image/png')
+   push('/img/yunxin2.png', res, 'image/png')
+   push('/js/log3.js', res, 'application/javascript')
    res.setHeader('Content-Type', 'text/html')
    res.sendFile(__dirname + '/src/page1.html')
})

function push (reqPath, target, type) {
    let content = fs.readFileSync(path.join(__dirname, publicPath, reqPath))
    let stream = target.push(reqPath, {
        status: 200,
        method: 'GET',
        request: { accept: '*/*' },
        response: {
            'content-type': type
        }
    })
    stream.on('error', function() {})
    stream.end(content)
}
複製代碼

來看下應用了server push的瀑布圖:

server push 瀑布圖

很明顯,被push的靜態資源能夠很快地被使用,而沒有被push的資源,如log1.jslog2.js則須要通過較長的時間才能被使用。

瀏覽器控制檯看到的東西畢竟頗有限,咱們來玩點更有意思的~

wireshark 抓包驗證

wireshark是一款能夠識別HTTP/2的抓包工具,它的原理是直接讀取並分析網卡數據,咱們用它來驗證是否真正實現了HTTP/2以及其底層通訊原理。

首先去官網下載安裝包並安裝wireshark,這一步沒啥好說的。

咱們知道,http/2裏的請求和響應都被拆分紅了幀,若是咱們直接去抓取HTTP/2通訊包,那抓到的只能是一幀一幀地數據,像這樣:

未解密的HTTP/2抓包圖

能夠看到,抓到的都是TCP類型的包(紅色方框);觀察前三個包的內容(綠色方框),分別是SYN、[SYN, ACK]和ACK,這就咱們所熟知的TCP三次握手;右下角的黃色小方框是請求當前頁面後抓到的TCP包的總數,其實這個頁面只有七八個請求,但抓到的包的數量卻有334個,這也驗證了HTTP/2的請求和響應的確是被分紅了一幀一幀的。

抓HTTP1.1的包,咱們能夠清楚地看到都有哪些請求和響應,它們的協議、大小等,而HTTP/2的數據包倒是一幀一幀地,那麼怎麼看HTTP/2都有哪些請求和響應呢?其實wireshark會自動幫咱們重組擁有相同stream_ID的幀,重組後就可看到實際有哪些請求和響應了,可是由於咱們用的是https,全部的數據都被加密了,wireshark就不知道該怎麼去重組了。

有兩個辦法能夠在wireshark中解密 HTTPS 流量:第一若是你擁有 HTTPS 網站的加密私鑰,能夠用加密私鑰來解密這個網站的加密流量;2)某些瀏覽器支持將 TLS 會話中使用的對稱密鑰保存在外部文件中,可供 Wireshark 解密使用。

可是HTTP/2爲了前向安全性,不容許使用RAS祕鑰交換,全部咱們沒法使用第一個方法來解密HTTP/2流量。介紹第二種方法:當系統環境變量中存在SSLKEYFILELOG時,Chrome和firefox會將對稱祕鑰保存在該環境變量指向的文件中,而後把這個文件導入wireshark,就能夠解密HTTP/2流量了,具體作法以下:

  1. 新建ssl.log文件
  2. 添加系統環境變量SSLKEYFILELOG,指向第一步建立的文件
  3. 在wireshark中打開 preferences->Protocols,找到SSL,將配置面板的 「(Pre)-Master-Secret log filename」選中第一步建立的文件

SSL配置面板圖

這時用Chrome或Firefox訪問任何一個https頁面,ssl.log中應該就有寫入的祕鑰數據了。

解密完成後,咱們就能夠看到HTTP/2的包了

下圖是在demo的主頁面抓取的包,能夠清楚地看到有哪些HTTP/2請求。

demo https圖

HTTP/2協議中的流和能夠在一個TCP鏈接中交錯傳輸,只需創建一個TCP鏈接就能夠完成和服務器的全部通訊,咱們來看下在demo中的HTTP/2是否是這樣的:

wireshark下方還有一個面板,裏面有當前包的具體信息,如大小、源IP、目的IP、端口、數據、協議等,在Transmission Control Protocol下有一個[Stream index],以下圖,它是TCP鏈接的編號,表明當前包是從哪一個TCP鏈接中傳輸的。觀察demo頁面請求產生的包,能夠發現它們的stream index 都相同,說明這些HTTP/2請求和響應是在一個TCP鏈接中被傳輸的,這麼多流的確複用了一個TCP鏈接。

TCP id圖

除了多路複用外,咱們還能夠經過抓包來觀察HTTP/2的頭部壓縮。下圖是當前路由下的第一個請求,實際被傳輸的頭部數據有253bytes,解壓後的頭部信息有482bytes。壓縮後的大小減小了幾乎一半

HTTP/2 HPACK

但這只是第一個請求,咱們看看後來的請求,如第三個,實際傳輸的頭部大小隻有30bytes,而解壓後的大小有441byte,壓縮後的體積僅爲原來的1/14!現在web應用單是一個頁面就動輒幾百的請求數,HPACK能節約的流量可想而知。

HTTP/2 HPACK

結語

在文章開篇,咱們列舉了HTTP1.x時代的困境,引入並簡要說明了HTTP/2的起源;而後對比着HTTP1.x,介紹了HTTP/2的諸多優秀特性,來講明爲何選擇HTTP/2;在文章的最後一部分,介紹瞭如何一步一步搭建一個HTTP/2實例,並抓包觀察,驗證了HTTP/2的多路複用,頭部壓縮等特性。最後,您是否也被這些高效特性吸引了呢?動手試試吧~

參考:

相關文章
相關標籤/搜索