爲了下降加載時間,相信大多數人都作過以下嘗試javascript
不能否認,這些優化在必定程度上下降了網站加載時間,但對於一個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
數據來源git
能夠看到國內有58.55%的瀏覽器已經徹底支持HTTP/2,而全球的支持度更是高達85.66%。這麼高的支持度,so,你心動了嗎github
咱們知道HTTP/1.1的頭信息確定是文本(ASCII編碼),數據體能夠是文本,也能夠是二進制(須要作本身作額外的轉換,協議自己並不會轉換)。而在HTTP/2中,新增了二進制分幀層,將數據轉換成二進制,也就是說HTTP/2中全部的內容都是採用二進制傳輸。web
使用二進制有什麼好處嗎?固然!效率會更高,並且最主要的是能夠定義額外的幀,若是用文本實現幀傳輸,解析起來將會十分麻煩。HTTP/2共定義了十種幀,較爲常見的有數據幀、頭部幀、PING幀、SETTING幀、優先級幀和PUSH_PROMISE幀等,爲未來的高級應用打好了基礎。算法
如上圖,Binary Framing就是新增的二進制分幀層。express
二進制分幀層把數據轉換爲二進制的同時,也把數據分紅了一個一個的幀。幀是HTTP/2中數據傳輸的最小單位;每一個幀都有stream_ID
字段,表示這個幀屬於哪一個流,接收方把stream_ID
相同的全部幀組合到一塊兒就是被傳輸的內容了。而流是HTTP/2中的一個邏輯上的概念,它表明着HTTP/1.1中的一個請求或者一個響應,協議規定client發給server的流的stream_ID
爲奇數,server發給client的流ID是偶數。須要注意的是,流只是一個邏輯概念,便於理解和記憶的,實際並不存在。
理解了幀和流的概念,完整的HTTP/2的通訊就能夠被形象地表示爲這樣:
能夠發現,在一個TCP連接中,能夠同時雙向地發送幀,並且不一樣流中的幀能夠交錯發送,不須要等某個流發送完,才發送下一個。也就是說在一個TCP鏈接中,能夠同時傳輸多個流,便可以同時傳輸多個HTTP請求和響應,這種同時傳輸不須要遵循先入先出等規定,所以也不會產生阻塞,效率極高。
在這種傳輸模式下,HTTP請求變得十分廉價,咱們不須要再時刻顧慮網站的http請求數是否太多、TCP鏈接數是否太多、是否會產生阻塞等問題了。
爲何須要壓縮?
在 HTTP/1 中,HTTP 請求和響應都是由「狀態行、請求 / 響應頭部、消息主體」三部分組成。通常而言,消息主體都會通過 gzip 壓縮,或者自己傳輸的就是壓縮事後的二進制文件(例如圖片、音頻),但狀態行和頭部卻沒有通過任何壓縮,直接以純文本傳輸。
隨着 Web 功能愈來愈複雜,每一個頁面產生的請求數也愈來愈多,根據 HTTP Archive 的統計,當前平均每一個頁面都會產生上百個請求。愈來愈多的請求致使消耗在頭部的流量愈來愈多,尤爲是每次都要傳輸 UserAgent、Cookie 這類不會頻繁變更的內容,徹底是一種浪費。
爲了減小冗餘的頭部信息帶來的消耗,HTTP/2採用HPACK 算法壓縮請求和響應的header。下面這張圖很是直觀地表達了HPACK頭部壓縮的原理:
具體規則能夠描述爲:
當要發送一個請求時,會先將其頭部和靜態表對照,對於徹底匹配的鍵值對,能夠直接使用一個數字表示,如上圖中的2:method: GET
,對於頭部名稱匹配的鍵值對,能夠將名稱使用一個數字傳輸,如上圖中的19:path: /resource
,同時告訴服務端將它添加到動態表中,之後的相同鍵值對就用一個數字表示了。這樣,像cookie這些不常常變更的值,只用發送一次就行了。
在開始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>
複製代碼
/user.html
/user.html
發給瀏覽器/user.html
,發現還須要請求/user.js
和style.css
靜態資源/user.js
和style.css
至此,這個頁面才加載完畢,能夠被用戶看到。能夠發如今步驟3和4中,服務器一直處於空閒等待狀態,而瀏覽器到第6步才能獲得資源渲染頁面,這使頁面的首次加載變得緩慢。
而HTTP/2的server push容許服務器在未收到請求時就向瀏覽器推送資源。即服務器發送/user.html
時,就能夠主動把/user.js
和style.css
push給瀏覽器,使資源提早達到瀏覽器;除了靜態文件,還能夠推送比較耗時的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
複製代碼
雖然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文件夾中,稍後會用到。
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/
,打開控制檯能夠看全部的請求和它們的瀑布圖:
能夠清楚地看到,當第一個請求,也就是對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的瀑布圖:
很明顯,被push的靜態資源能夠很快地被使用,而沒有被push的資源,如log1.js
和log2.js
則須要通過較長的時間才能被使用。
瀏覽器控制檯看到的東西畢竟頗有限,咱們來玩點更有意思的~
wireshark是一款能夠識別HTTP/2的抓包工具,它的原理是直接讀取並分析網卡數據,咱們用它來驗證是否真正實現了HTTP/2以及其底層通訊原理。
首先去官網下載安裝包並安裝wireshark,這一步沒啥好說的。
咱們知道,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流量了,具體作法以下:
SSLKEYFILELOG
,指向第一步建立的文件這時用Chrome或Firefox訪問任何一個https頁面,ssl.log中應該就有寫入的祕鑰數據了。
解密完成後,咱們就能夠看到HTTP/2的包了
下圖是在demo的主頁面抓取的包,能夠清楚地看到有哪些HTTP/2請求。
HTTP/2協議中的流和能夠在一個TCP鏈接中交錯傳輸,只需創建一個TCP鏈接就能夠完成和服務器的全部通訊,咱們來看下在demo中的HTTP/2是否是這樣的:
wireshark下方還有一個面板,裏面有當前包的具體信息,如大小、源IP、目的IP、端口、數據、協議等,在Transmission Control Protocol下有一個[Stream index],以下圖,它是TCP鏈接的編號,表明當前包是從哪一個TCP鏈接中傳輸的。觀察demo頁面請求產生的包,能夠發現它們的stream index 都相同,說明這些HTTP/2請求和響應是在一個TCP鏈接中被傳輸的,這麼多流的確複用了一個TCP鏈接。
除了多路複用外,咱們還能夠經過抓包來觀察HTTP/2的頭部壓縮。下圖是當前路由下的第一個請求,實際被傳輸的頭部數據有253bytes,解壓後的頭部信息有482bytes。壓縮後的大小減小了幾乎一半
但這只是第一個請求,咱們看看後來的請求,如第三個,實際傳輸的頭部大小隻有30bytes,而解壓後的大小有441byte,壓縮後的體積僅爲原來的1/14!現在web應用單是一個頁面就動輒幾百的請求數,HPACK能節約的流量可想而知。
在文章開篇,咱們列舉了HTTP1.x時代的困境,引入並簡要說明了HTTP/2的起源;而後對比着HTTP1.x,介紹了HTTP/2的諸多優秀特性,來講明爲何選擇HTTP/2;在文章的最後一部分,介紹瞭如何一步一步搭建一個HTTP/2實例,並抓包觀察,驗證了HTTP/2的多路複用,頭部壓縮等特性。最後,您是否也被這些高效特性吸引了呢?動手試試吧~
參考: