欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

詳解基于Node.js的HTTP/2 Server實(shí)踐

 更新時(shí)間:2018年05月31日 13:44:59   作者:clasky  
HTTP/2目前已經(jīng)逐漸的在各大網(wǎng)站上開始使用,這篇文章主要介紹了詳解基于Node.js的HTTP/2 Server實(shí)踐,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧

雖然HTTP/2目前已經(jīng)逐漸的在各大網(wǎng)站上開始了使用,但是在目前最新的Node.js上仍然處于實(shí)驗(yàn)性API,還沒有能有效解決生產(chǎn)環(huán)境各種問題的應(yīng)用示例。因此在應(yīng)用HTTP/2的道路上我自己也遇到了許多坑,下面介紹了項(xiàng)目的主要架構(gòu)與開發(fā)中遇到的問題及解決方式,也許會對你有一點(diǎn)點(diǎn)啟示。

配置

雖然W3C的規(guī)范中沒有規(guī)定HTTP/2協(xié)議一定要使用ssl加密,但是支持非加密的HTTP/2協(xié)議的瀏覽器實(shí)在少的可憐,因此我們有必要申請一個(gè)自己的域名和一個(gè)ssl證書。

本項(xiàng)目的測試域名是 you.keyin.me ,首先我們?nèi)ビ蛎峁┥棠前褱y試服務(wù)器的地址綁定到這個(gè)域名上。然后使用Let's Encrypt生成一個(gè)免費(fèi)的SSL證書:

sudo certbot certonly --standalone -d you.keyin.me

輸入必要信息并通過驗(yàn)證之后就可以在 /etc/letsencrypt/live/you.keyin.me/ 下面找到生成的證書了。

改造Koa

Koa是一個(gè)非常簡潔高效的Node.js服務(wù)器框架,我們可以簡單改造一下來讓它支持HTTP/2協(xié)議:

class KoaOnHttps extends Koa {
 constructor() {
  super();
 }
 get options() {
  return {
   key: fs.readFileSync(require.resolve('/etc/letsencrypt/live/you.keyin.me/privkey.pem')),
   cert: fs.readFileSync(require.resolve('/etc/letsencrypt/live/you.keyin.me/fullchain.pem'))
  };
 }
 listen(...args) {
  const server = http2.createSecureServer(this.options, this.callback());
  return server.listen(...args);
 }
 redirect(...args) {
  const server = http.createServer(this.callback());
  return server.listen(...args);
 }
}

const app = new KoaOnHttps();
app.use(sslify());
//...
app.listen(443, () => {
logger.ok('app start at:', `https://you.keyin.cn`);
});

// receive all the http request, redirect them to https
app.redirect(80, () => {
logger.ok('http redirect server start at', `http://you.keyin.me`);
});

上述代碼簡單基于Koa生成了一個(gè)HTTP/2服務(wù)器,并同時(shí)監(jiān)聽80端口,通過sslify中間件的幫助自動將http協(xié)議的連接重定向到https協(xié)議。

靜態(tài)文件中間件

靜態(tài)文件中間件主要用來返回url所指向的本地靜態(tài)資源。在http/2服務(wù)器中我們可以在訪問html資源的時(shí)候通過服務(wù)器推送(Server push)將該頁面所依賴的js\css\font等資源一起推送回去。具體代碼如下:

const send = require('koa-send');
const logger = require('../util/logger');
const { push, acceptsHtml } = require('../util/helper');
const depTree = require('../util/depTree');
module.exports = (root = '') => {
 return async function serve(ctx, next) {
  let done = false;
  if (ctx.method === 'HEAD' || ctx.method === 'GET') {
   try {
    // 當(dāng)希望收到html時(shí),推送額外資源。
    if (/(\.html|\/[\w-]*)$/.test(ctx.path)) {
     depTree.currentKey = ctx.path;
     const encoding = ctx.acceptsEncodings('gzip', 'deflate', 'identity');
     // server push
     for (const file of depTree.getDep()) {
      // server push must before response!
      // https://huangxuan.me/2017/07/12/upgrading-eleme-to-pwa/#fast-skeleton-painting-with-settimeout-hack
      push(ctx.res.stream, file, encoding);
     }
    }
    done = await send(ctx, ctx.path, { root });
   } catch (err) {
    if (err.status !== 404) {
     logger.error(err);
     throw err;
    }
   }
  }
  if (!done) {
   await next();
  }
 };
};

需要注意的是,推送的發(fā)生永遠(yuǎn)要先于當(dāng)前頁面的返回。否則服務(wù)器推送與客戶端請求可能就會出現(xiàn)競爭的情況,降低傳輸效率。

依賴記錄

從靜態(tài)文件中間件代碼中我們可以看到,服務(wù)器推送資源取自depTree這個(gè)對象,它是一個(gè)依賴記錄工具,記錄當(dāng)前頁面 depTree.currentKey 所有依賴的靜態(tài)資源(js,css,img...)路徑。具體的實(shí)現(xiàn)是:

const logger = require('./logger');

const db = new Map();
let currentKey = '/';

module.exports = {
  get currentKey() {
    return currentKey;
  },
  set currentKey(key = '') {
    currentKey = this.stripDot(key);
  },
  stripDot(str) {
    if (!str) return '';
    return str.replace(/index\.html$/, '').replace(/\./g, '-');
  },
  addDep(filePath, url, key = this.currentKey) {
    if (!key) return;
    key = this.stripDot(key);
    if(!db.has(key)){
      db.set(key,new Map());
    }
    const keyDb = db.get(key);

    if (keyDb.size >= 10) {
      logger.warning('Push resource limit exceeded');
      return;
    }
    keyDb.set(filePath, url);
  },
  getDep(key = this.currentKey) {
    key = this.stripDot(key);
    const keyDb = db.get(key);
    if(keyDb == undefined) return [];
    const ret = [];
    for(const [filePath,url] of keyDb.entries()){
      ret.push({filePath,url});
    }
    return ret;
  }
};

當(dāng)設(shè)置好特定的當(dāng)前頁 currentKey 后,調(diào)用 addDep 將方法能夠?yàn)楫?dāng)前頁面添加依賴,調(diào)用 getDep 方法能夠取出當(dāng)前頁面的所有依賴。 addDep 方法需要寫在路由中間件中,監(jiān)控所有需要推送的靜態(tài)文件請求得出依賴路徑并記錄下來:

router.get(/\.(js|css)$/, async (ctx, next) => {
 let filePath = ctx.path;
 if (/\/sw-register\.js/.test(filePath)) return await next();
 filePath = path.resolve('../dist', filePath.substr(1));
 await next();
 if (ctx.status === 200 || ctx.status === 304) {
  depTree.addDep(filePath, ctx.url);
 }
});

服務(wù)器推送

Node.js最新的API文檔中已經(jīng)簡單描述了服務(wù)器推送的寫法,實(shí)現(xiàn)很簡單:

exports.push = function(stream, file) {
 if (!file || !file.filePath || !file.url) return;
 file.fd = file.fd || fs.openSync(file.filePath, 'r');
 file.headers = file.headers || getFileHeaders(file.filePath, file.fd);

 const pushHeaders = {[HTTP2_HEADER_PATH]: file.url};

 stream.pushStream(pushHeaders, (err, pushStream) => {
  if (err) {
   logger.error('server push error');
   throw err;
  }
  pushStream.respondWithFD(file.fd, file.headers);
 });
};

stream 代表的是當(dāng)前HTTP請求的響應(yīng)流, file 是一個(gè)對象,包含文件路徑 filePath 與文件資源鏈接 url 。先使用 stream.pushStream 方法推送一個(gè) PUSH_PROMISE 幀,然后在回調(diào)函數(shù)中調(diào)用 responseWidthFD 方法推送具體的文件內(nèi)容。

以上寫法簡單易懂,也能立即見效。網(wǎng)上很多文章介紹到這里就沒有了。但是如果你真的拿這樣的HTTP/2服務(wù)器與普通的HTTP/1.x服務(wù)器做比較的話,你會發(fā)現(xiàn)現(xiàn)實(shí)并沒有你想象的那么美好,盡管HTTP/2理論上能夠加快傳輸效率,但是HTTP/1.x總共傳輸?shù)臄?shù)據(jù)明顯比HTTP/2要小得多。最終兩者相比較起來其實(shí)還是HTTP/1.x更快。

Why?

答案就在于資源壓縮(gzip/deflate)上,基于Koa的服務(wù)器能夠很輕松的用上 koa-compress 這個(gè)中間件來對文本等靜態(tài)資源進(jìn)行壓縮,然而盡管Koa的洋蔥模型能夠保證所有的HTTP返回的文件數(shù)據(jù)流經(jīng)這個(gè)中間件,卻對于服務(wù)器推送的資源來說鞭長莫及。這樣造成的后果是,客戶端主動請求的資源都經(jīng)過了必要的壓縮處理,然而服務(wù)器主動推送的資源卻都是一些未壓縮過的數(shù)據(jù)。也就是說,你的服務(wù)器推送資源越大,不必要的流量浪費(fèi)也就越大。新的服務(wù)器推送的特性反而變成了負(fù)優(yōu)化。

因此,為了盡可能的加快服務(wù)器數(shù)據(jù)傳輸?shù)乃俣?,我們只有在上?push 函數(shù)中手動對文件進(jìn)行壓縮。改造后的代碼如下,以gzip為例。

exports.push = function(stream, file) {
 if (!file || !file.filePath || !file.url) return;
 file.fd = file.fd || fs.openSync(file.filePath, 'r');
 file.headers = file.headers || getFileHeaders(file.filePath, file.fd);

 const pushHeaders = {[HTTP2_HEADER_PATH]: file.url};

 stream.pushStream(pushHeaders, (err, pushStream) => {
  if (err) {
   logger.error('server push error');
   throw err;
  }
  if (shouldCompress()) {
   const header = Object.assign({}, file.headers);
   header['content-encoding'] = "gzip";
   delete header['content-length'];
   
   pushStream.respond(header);
   const fileStream = fs.createReadStream(null, {fd: file.fd});
   const compressTransformer = zlib.createGzip(compressOptions);
   fileStream.pipe(compressTransformer).pipe(pushStream);
  } else {
   pushStream.respondWithFD(file.fd, file.headers);
  }
 });
};

我們通過 shouldCompress 函數(shù)判斷當(dāng)前資源是否需要進(jìn)行壓縮,然后調(diào)用 pushStream.response(header) 先返回當(dāng)前資源的 header 幀,再基于流的方式來高效返回文件內(nèi)容:

  1. 獲取當(dāng)前文件的讀取流 fileStream
  2. 基于 zlib 創(chuàng)建一個(gè)可以動態(tài)gzip壓縮的變換流 compressTransformer
  3. 將這些流依次通過管道( pipe )傳到最終的服務(wù)器推送流 pushStream 中

Bug

經(jīng)過上述改造,同樣的請求HTTP/2服務(wù)器與HTTP/1.x服務(wù)器的返回總體資源大小基本保持了一致。在Chrome中能夠順暢打開。然而進(jìn)一步使用Safari測試時(shí)卻返回HTTP 401錯(cuò)誤,另外打開服務(wù)端日志也能發(fā)現(xiàn)存在一些紅色的異常報(bào)錯(cuò)。

經(jīng)過一段時(shí)間的琢磨,我最終發(fā)現(xiàn)了問題所在:因?yàn)榉?wù)器推送的推送流是一個(gè)特殊的可中斷流,當(dāng)客戶端發(fā)現(xiàn)當(dāng)前推送的資源目前不需要或者本地已有緩存的版本,就會給服務(wù)器發(fā)送 RST 幀,用來要求服務(wù)器中斷掉當(dāng)前資源的推送。服務(wù)器收到該幀之后就會立即把當(dāng)前的推送流( pushStream )設(shè)置為關(guān)閉狀態(tài),然而普通的可讀流都是不可中斷的,包括上述代碼中通過管道連接到它的文件讀取流( fileStream ),因此服務(wù)器日志里的報(bào)錯(cuò)就來源于此。另一方面對于瀏覽器具體實(shí)現(xiàn)而言,W3C標(biāo)準(zhǔn)里并沒有嚴(yán)格規(guī)定客戶端這種情況應(yīng)該如何處理,因此才出現(xiàn)了繼續(xù)默默接收后續(xù)資源的Chrome派與直接激進(jìn)報(bào)錯(cuò)的Safari派。

解決辦法很簡單,在上述代碼中插入一段手動中斷可讀流的邏輯即可。

//...
fileStream.pipe(compressTransformer).pipe(pushStream);
pushStream.on('close', () => fileStream.destroy());
//...

即監(jiān)聽推送流的關(guān)閉事件,手動撤銷文件讀取流。

最后

本項(xiàng)目代碼開源在Github上,如果覺得對你有幫助希望能給我點(diǎn)個(gè)Star。

以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。

相關(guān)文章

  • 阿里大于短信驗(yàn)證碼node koa2的實(shí)現(xiàn)代碼(最新)

    阿里大于短信驗(yàn)證碼node koa2的實(shí)現(xiàn)代碼(最新)

    本文給大家分享一個(gè)最新版阿里大于短信驗(yàn)證碼node koa2的實(shí)現(xiàn)代碼及注意事項(xiàng),需要的朋友參考下吧
    2017-09-09
  • node.js中koa和express的差異對比

    node.js中koa和express的差異對比

    Express和koa都是服務(wù)端的開發(fā)框架,服務(wù)端開發(fā)的重點(diǎn)是對HTTP Request和HTTP Response兩個(gè)對象的封裝和處理,下面這篇文章主要給大家介紹了關(guān)于node.js中koa和express的差異對比,需要的朋友可以參考下
    2023-05-05
  • node.js的Express服務(wù)器基本使用教程

    node.js的Express服務(wù)器基本使用教程

    express是一個(gè)開源的node.js項(xiàng)目框架,下面這篇文章主要給大家介紹了關(guān)于node.js的Express服務(wù)器基本使用的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-01-01
  • node.js mongoose index索引操作

    node.js mongoose index索引操作

    在 Mongoose 中,索引(Index)是一種用于提高查詢性能的數(shù)據(jù)結(jié)構(gòu),它可以加速對數(shù)據(jù)庫中文檔的檢索操作,本文給大家介紹
    node.js mongoose index索引操作
    ,感興趣的朋友一起看看吧
    2023-12-12
  • Node.js包管理工具(npm、yarn、cnpm)

    Node.js包管理工具(npm、yarn、cnpm)

    本文主要介紹了Node.js包管理工具,包含npm、yarn、cnpm者三種,借助包管理工具,可以快速開發(fā)項(xiàng)目,提升開發(fā)效率,下面就來具體介紹一下如何使用,感興趣的可以了解一下
    2024-08-08
  • Node.js高級編程cluster環(huán)境及源碼調(diào)試詳解

    Node.js高級編程cluster環(huán)境及源碼調(diào)試詳解

    這篇文章主要為大家介紹了Node.js高級編程cluster環(huán)境及源碼調(diào)試詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-12-12
  • 給nodejs升級的兩種方法

    給nodejs升級的兩種方法

    nodejs是一種流行的服務(wù)器端JavaScript運(yùn)行環(huán)境,它經(jīng)常需要更新以獲取最新的功能和性能優(yōu)化,本文將給大家介紹了給nodejs升級的兩種方法,文中通過代碼示例講解非常詳細(xì),需要的朋友可以參考下
    2023-12-12
  • node中socket.io的事件使用詳解

    node中socket.io的事件使用詳解

    這篇文章主要介紹了node中socket.io的事件使用詳解,需要的朋友可以參考下
    2014-12-12
  • 如何能分清npm cnpm npx nvm

    如何能分清npm cnpm npx nvm

    這篇文章主要介紹了如何能分清npm cnpm npx nvm,本文就詳細(xì)的來介紹一下區(qū)別,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2019-01-01
  • node.js中的events.emitter.once方法使用說明

    node.js中的events.emitter.once方法使用說明

    這篇文章主要介紹了node.js中的events.emitter.once方法使用說明,本文介紹了events.emitter.once的方法說明、語法、接收參數(shù)、使用實(shí)例和實(shí)現(xiàn)源碼,需要的朋友可以參考下
    2014-12-12

最新評論