Node HTTP/2 Server Push 從瞭解到放棄

前陣子,在Media看到一篇文章《Node.js can HTTP/2 push!》。看到push這個字眼時,我想到的是WebSocket消息推送。難不成HTTP/2還能像WebSocket那樣能夠服務端主動推送消息?好厲害,我就一會兒來了興趣。javascript

然而閱讀完文章以後,發現理想與現實略有差距。簡單的說,HTTP/2 所謂的server push實際上是當服務器接收一個請求時,能夠響應多個資源。舉個栗子:瀏覽器向服務器請求index.html,服務器不只把index.html返回了,還能夠把index.js,index.css等一塊兒推送給客戶端。最直觀的好處就是,瀏覽器不用解析頁面再發起請求來獲取數據,節約了頁面加載時間。css

雖然略有差距,但看起來仍是挺有意思的,值得去嘗試一下。畢竟紙上得來終覺淺,絕知此事要躬行!html

HTTP/2

我以前並未使用過HTTP/2,在進行實踐以前,總要先了解一下。關於HTTP/2,網上也有不少資料,我這裏就簡單說一下它最大的優勢:快!。這裏的快是相比HTTP 1.x 而言的,那爲何它會更快呢?前端

頭部壓縮

這裏的頭部指的是http請求頭headers。你們可能會想請求頭能有多大呢,跟資源相比算不上啥。其實否則,隨着互聯網的發展,請求頭裏攜帶的數據愈來愈多了,隨隨便一個「user-agent」就一長串。另外cookie也會被存放愈來愈多的信息。更煩的是,一個頁面全部的請求,都會帶上這些重複的請求頭數據。java

因此HTTP/2採用HPACK算法,能極大壓縮頭部數據,減小整體資源請求大小。大體的原理就是維護兩本字典,一本靜態字典,維護比較常見的頭部名稱。一本動態字典,維護不一樣請求的公共的頭數據。node

多路複用

咱們知道,在HTTP 1.x中,咱們是能夠並行請求的。可是,瀏覽器對於同一個域名的並行請求是有上限的(FireFox, Chrome上限6個 )。因此不少網站的靜態資源站可能會有多個。並且每次請求都要從新創建TCP鏈接,想必大部分web工程師都瞭解過TCP三次握手,這個握手的代價也是比較高的。git

雖然http1.x裏有keep-alive能夠避免TCP三次握手,可是keep-alive又是串行的。因此要麼並行多握手,要麼串行不握手,都不是最好的結果,咱們但願的是並行也不握手。程序員

幸運的是HTTP/2解決了這個問題。當客戶端與服務端創建鏈接後,就會在雙方創建一個雙向流通道。這個流通道,能夠同時包含多個消息(http請求),不一樣消息各自的數據幀在流裏能夠亂序並行的發送,不會互相影響與堵塞,從而實現了一個TCP連接,併發執行N個http請求。經過提升併發,減小TCP鏈接開銷,HTTP/2的速度獲得了很大提高,尤爲是在網絡延遲比較高的狀況下。github

這裏用展示兩張網絡請求時間瀑布流對比圖:web

HTTP 1.1

undefined

HTTP/2

undefined

Server Push

上文中,咱們描述了HTTP/2的鏈接會創建一個雙向流通道。Server Push就是在某次流中,能夠返回客戶端並無主動要的數據。

上述的頭部壓縮、多路複用,並不須要開發人員作什麼操做,只要開啓HTTP/2,瀏覽器也支持就能夠了。可是Server Push就須要開發人員編寫代碼去操做了。那咱們就動手,在Node上玩玩看。

Node HTTP/2 Server Push 實操

Node對HTTP/2支持狀況

在Node 8.4.0版本時,就對HTTP/2實驗性的支持了。2018年4月24日晚,Node v10終於發佈了,然而對於HTTP/2,仍是實驗性的支持。。。不過社區已經對HTTP/2移除實驗性進行討論了,相信在不遠的未來應該能看到Node對HTTP/2更好的支持。所以在這以前,咱們能夠先去掌握這個知識,作一些實踐。

依葫蘆畫瓢

咱們先根據Node文檔,建立一個HTTP/2服務。這裏須要提的一點就是,目前流行的瀏覽器都不支持未加密的、不安全的HTTP/2。因此咱們必須生成下證書與祕鑰,而後經過http2.createSecureServer建立安全的HTTP/2連接。

想本身實踐,生成本地證書的同窗能夠參考這裏:傳送門

// server.js
const http2 = require('http2')
const fs = require('fs')
const streamHandle = require('./streamHandle/sample')
const options = {
  key: fs.readFileSync('./ryans-key.pem'),
  cert: fs.readFileSync('./ryans-cert.pem'),
}
const server = http2.createSecureServer(options)
server.on('stream', streamHandle)
server.listen(8125)

而後咱們再照着文檔,編寫對流的處理,並推送一個url路徑爲 '/' 的數據。

// streamHandle/sample.js
module.exports = stream => {
  stream.respond({ ':status': 200 })
  stream.pushStream({ ':path': '/' }, (err, pushStream, headers) => {
    if (err) throw err
    pushStream.respond({ ':status': 200 })
    pushStream.end('some pushed data')
    pushStream.on('close', () => console.log('close'))
  })
  stream.end('some data')
}

而後咱們打開chrome,訪問https://127.0.0.1:8125 發現頁面顯示的一直是 some datasome pushed data這個主動推送的數據不知在哪裏。打開網路請求面板,也沒有任何其餘請求。

百思不得其解阿,但我又不想止步於此,怎麼辦呢?

從娃娃抓起

我決定先寫一個正常的HTTP/2業務請求,代碼以下:

module.exports = (stream, headers) => {
  const path = headers[':path']
  if (path.indexOf('api') >= 0) {
    // 請求api
    stream.respond({ 'content-type': 'application/json', ':status': 200 })
    stream.end(JSON.stringify({ success: true }))
  } else if (path.indexOf('static') >= 0) {
    // 請求靜態資源
    const fileType = path.split('.').pop()
    const contentType = fileType === 'js' ? 'application/javascript' : 'text/css'
    stream.respondWithFile(`./src${path}`, {
      'content-Type': contentType
    })
  } else {
    // 請求html
    stream.respondWithFile('./src/index.html')
  }
}

代碼大意就是,判斷請求連接,當請求地址帶有api字眼時就返回一個json,當請求地址帶有static時,就返回對應路徑的靜態資源。其餘狀況就返回一個html文件。

html文件內容爲:

<!DOCTYPE html>
<html>
<head>
  <meta charset=utf-8>
  <title>HTTP/2 Server Push</title>
  <link rel="shortcut icon" type=image/ico href=/static/favorite.ico>
  <link href=/static/css/app.css rel=stylesheet>
</head>
<body>
  <h1>HTTP/2 Server Push</h1>
  <script type=text/javascript src=/static/js/test.js></script>
</body>
</html>

運行後咱們再打開chrome,訪問https://127.0.0.1:8125 ,咱們能看到頁面正常渲染了,查看網絡面板,發現協議也已是HTTP/2。

undefined

這樣咱們就開發了一個很是簡單的HTTP/2應用。下一步,咱們再加上server push的功能,當訪問index.html的請求時,咱們主動將js的資源返回,看看瀏覽器是怎麼樣的響應狀況。

抓出一個葫蘆娃

module.exports = (stream, headers) => {
 const path = headers[':path']
 if (path.indexOf('api') >= 0) {
   // 請求api部分代碼-略
 } else if (path.indexOf('static') >= 0) {
   // 請求靜態資源部分代碼-略
 } else {
   // 請求html時 主動推送js文件
   stream.pushStream(
       { ':path': '/static/js/test.js' },
     (err, pushStream, headers) => {
       if (err) throw err
       pushStream.respondWithFile('./src/static/js/test2.js', {
         'content-type:': 'application/javascript'
       })
       pushStream.on('error', console.error)
     }
   )
   stream.respondWithFile('./src/index.html')
 }
}

代碼大意就是,當客戶端請求index.html時,服務端除了返回index.html文件,順便把test2.js這個文件推給服務端,客戶端若是再次請求 https://127.0.0.1:8125/static/js/test.js 時,就會直接獲取到test2.js

這裏我用test2.js的目的是爲了方便的知道,客戶端請求的究竟是服務端推送的test2.js文件,仍是直接經過服務器再次請求獲取到的test.js文件。

其中test.js會在頁面打印:This is normal js.
test2.js會在頁面打印:This is server push js.

按照指望,應該是後者。而後咱們打開chrome,訪問 https://127.0.0.1:8125,展示以下結果:

undefined

!!!!掀桌!!!!

這個展現結果並非意料中的打印出This is server push js,頁面請求的js文件仍是正常網絡請求的,並不是是我主動推送的test2.js。我翻山越嶺搜遍祖國內外,終於在Node的一條issue下看到相似的問題:http2 pushStream not providing files for :path entries (CHROME 65, works in FF)

Works in FireFox ??????? Chrome的bug ??????

你照着文檔寫代碼,結果卻不像文檔所展現,各類排查沒有用,最終發現是一些非主觀的緣由,程序員最大的痛苦莫過於此....而後我夾雜着痛苦心塞和峯迴路轉的心情,打開了本身的Firefox,訪問頁面,展示以下結果:

undefined

這回終於對了!能夠看到,頁面中打印的是test2.js文件的輸出結果。

最開始依葫蘆畫瓢沒用,其實也是由於Chrome的bug。無論怎麼樣,咱們仍是往前邁進了巨大的一步。

ps: 本人chrome版本66.0.3359.117,依舊有此bug

雞肋

雖然咱們前進了一大步,但是面臨了一個很尷尬的問題:咱們的靜態資源更可能是託管在cdn上的。那咱們實際場景就會遇到以下狀況:

  1. 全部網站的資源,包括html/css/js/image等,都是在一臺業務服務器上的。抱歉同窗,你的業務服務器的帶寬原本就低,怕是吃不消這麼多靜態資源的併發請求,你原本就慢的無可救藥了。
  2. 網絡路由走後端,即html走後端,其餘靜態資源託管cdn。抱歉同窗,靜態資源都在cdn上的,你的業務服務器怎麼去推?
  3. 徹底的先後端分離,html與其餘靜態資源都是在cdn上。這種狀況下,仍是有點用處的,但效果並不會很出色。由於HTTP/2自己就支持多路複用,已經減小了TCP三次握手帶來的網絡消耗。server push僅僅只是下降了瀏覽器解析html的時間,對於現代瀏覽器來講,這太微乎其微了。(ps: 就在我寫文章之時,剛好看到某雲服務商支持了server push。)

這麼一說,這就是個雞肋啊!到頭來竹籃打水一場空?

天生我材必有用

作人仍是不能輕易的放棄治療。再仔細想一想,仍是有一些應用場景的---初始化的API請求。

如今不少單頁應用,每每有不少的初始化請求,獲取用戶信息、獲取頁面數據等等。而這些都是須要html加載完,而後js加載完,而後再去執行的。並且不少時候,這些數據不加載完,頁面都只能空白顯示。但是單頁應用的js資源每每又很大。一個vendor包好幾兆也很常見。等瀏覽器加載並解析完這麼大的包,可能已經不少時間消耗了。這時候再去請求一些初始化API,若是這些API又比較費時的話,頁面就要多空白很長時間。

但若是能在請求html時,咱們就把初始化的api數據推送給客戶端,當js解析完再去請求時,就能立刻獲取到數據,這就能節省寶貴的白屏時間。說幹就幹,咱們再次動手實踐!

module.exports = (stream, headers) => {
  const path = headers[':path']
  if (path.indexOf('api') >= 0) {
    // 請求api
    stream.respond({ 'content-type': 'application/json', ':status': 200 })
    stream.end(JSON.stringify({ apiType: 'normal' }))
  } else if (path.indexOf('static') >= 0) {
    // 請求靜態資源代碼-略
  } else {
    // 請求html
    stream.pushStream({ ':path': '/api/getData' }, (err, pushStream, headers) => {
      if (err) throw err
      pushStream.respond({ ':status': 200 , 'content-type': 'application/json'});
      pushStream.end(JSON.stringify({ apiType: 'server push' }))
    });
    stream.respondWithFile('./src/index.html')
  }
}

一樣的,我讓正常請求api與服務端推送的api數據作一些差別,以便於更直觀的判斷是否獲取了服務端推送的數據。而後在前端的js文件中寫以下請求,並打印出請求結果:

window.fetch('/api/getData').then(result => result.json()).then(rs => {
  console.log('fetch:', rs)
})

使人遺憾的是,咱們的到的是以下的結果:
undefined

請求的結果表示這並非server push的數據。吃一塹長一智,這會不會又是瀏覽器的什麼bug?亦或者是否是fetch不支持獲取server push的數據?我立刻用XMLHttpRequest又寫了一版:

window.fetch('/api/getData').then(result => result.json()).then(rs => {
  console.log('fetch:', rs)
})

const request = new XMLHttpRequest();
request.open('GET', '/api/getData', true)
request.onload = function(result) {
  console.log('ajax:', JSON.parse(this.responseText))
};
request.send();

結果以下:
undefined

!!!!掀桌!!!!

居然還真的是fetch不支持http2 server push!

仍是雞肋

其實除了fetch不支持外,還有一個比較致命的問題,就是這個server push,在當下的node服務器上,不能對服務端推送資源的url進行模糊匹配。也就是說,若是一個請求有url動態參數的話,實際上是匹配不到的。像我例子中的stream.pushStream({ ':path': '/api/getData' }, pushHandle),若是前端請求的接口是 /api/getData?param=1,那就得不到server push的數據了。

另外,它僅支持GET請求與HEAD請求,POST、PUT這些也是不支持的。

針對fetch這個問題,我又了搜了下祖國內外,也沒得出個因此然來。這也變相的說明,目前社區裏針對server push這個特性使用的還不多,遇到問題時,很難快速的去定位與解決問題。

因此,彷佛在推送api上,它的應用場景又侷限了,僅適用於推送固定URL的初始化GET請求。

苦海無邊懸崖勒馬

綜上所述,我得出的結論就是:目前在Node上,使用server push,極大的狀況與機率是不合適的,是付出大於收益的。主要因爲以下緣由:

  1. 截止Node v10.0.0,HTTP/2依舊是一個實驗性的模塊;
  2. 瀏覽器支持極差;如上述的Chrome的bug,fetch對server push的不支持;
  3. 推送靜態資源的實際場景很是少,並且速度提高在理論上也不會很明顯;
  4. 推送API僅支持固定的URL,不能攜帶任何動態參數。

注:上述內容僅侷限在Node服務,其餘服務器本人未有研究,不必定有上述問題

雖然server push我目前以爲很差用,可是HTTP/2仍是個好東西的,除了我文章開頭講的那些好處外,HTTP/2還有不少新奇的有用的特性,諸如流優先級、流控制等一些特性,本文並未講到。你們能夠去了解了解,對咱們將來開發高性能的web應該確定有不少幫助!

本文所涉及源碼:https://github.com/wuomzfx/ht...
原文連接:https://yuque.com/wuomzfx/art...

相關文章
相關標籤/搜索