亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb

首頁 > 編程 > JavaScript > 正文

詳解基于Node.js的HTTP/2 Server實踐

2019-11-19 13:44:34
字體:
來源:轉載
供稿:網友

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

配置

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

本項目的測試域名是 you.keyin.me ,首先我們去域名提供商那把測試服務器的地址綁定到這個域名上。然后使用Let's Encrypt生成一個免費的SSL證書:

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

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

改造Koa

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

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 httpsapp.redirect(80, () => {logger.ok('http redirect server start at', `http://you.keyin.me`);});

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

靜態文件中間件

靜態文件中間件主要用來返回url所指向的本地靜態資源。在http/2服務器中我們可以在訪問html資源的時候通過服務器推送(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 {    // 當希望收到html時,推送額外資源。    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();  } };};

需要注意的是,推送的發生永遠要先于當前頁面的返回。否則服務器推送與客戶端請求可能就會出現競爭的情況,降低傳輸效率。

依賴記錄

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

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;  }};

當設置好特定的當前頁 currentKey 后,調用 addDep 將方法能夠為當前頁面添加依賴,調用 getDep 方法能夠取出當前頁面的所有依賴。 addDep 方法需要寫在路由中間件中,監控所有需要推送的靜態文件請求得出依賴路徑并記錄下來:

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); }});

服務器推送

Node.js最新的API文檔中已經簡單描述了服務器推送的寫法,實現很簡單:

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 代表的是當前HTTP請求的響應流, file 是一個對象,包含文件路徑 filePath 與文件資源鏈接 url 。先使用 stream.pushStream 方法推送一個 PUSH_PROMISE 幀,然后在回調函數中調用 responseWidthFD 方法推送具體的文件內容。

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

Why?

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

因此,為了盡可能的加快服務器數據傳輸的速度,我們只有在上方 push 函數中手動對文件進行壓縮。改造后的代碼如下,以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 函數判斷當前資源是否需要進行壓縮,然后調用 pushStream.response(header) 先返回當前資源的 header 幀,再基于流的方式來高效返回文件內容:

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

Bug

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

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

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

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

即監聽推送流的關閉事件,手動撤銷文件讀取流。

最后

本項目代碼開源在Github上,如果覺得對你有幫助希望能給我點個Star。

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持武林網。

發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb
久久的精品视频| 色偷偷噜噜噜亚洲男人| 欧美精品在线看| 精品女同一区二区三区在线播放| 亚洲激情小视频| 亚洲色图国产精品| 国产精品激情自拍| 久热在线中文字幕色999舞| 中文字幕亚洲情99在线| 欧美整片在线观看| 亚洲精品mp4| 久久99亚洲精品| 日韩日本欧美亚洲| 成人免费网视频| 久久久久久久久91| 韩国日本不卡在线| 亚洲精品短视频| 亚洲第一免费播放区| 亚州国产精品久久久| 国产精品日韩在线一区| 91极品视频在线| 欧美日本中文字幕| 中文字幕精品一区二区精品| 性欧美视频videos6一9| 日韩av一区在线| 国模精品系列视频| 国产精品久久久av久久久| 国产精品极品美女粉嫩高清在线| 日韩av免费观影| 色妞一区二区三区| 成人黄色在线观看| 国产精品免费视频久久久| 久久综合伊人77777| 国产在线精品播放| 97成人精品区在线播放| 91在线免费网站| 2018中文字幕一区二区三区| 亚洲欧洲av一区二区| 日韩av资源在线播放| 菠萝蜜影院一区二区免费| 欧美性受xxxx白人性爽| 亚洲女人初尝黑人巨大| 欧美激情网友自拍| 精品视频久久久久久久| 国产精品人人做人人爽| 国精产品一区一区三区有限在线| 热久久免费国产视频| 国产日韩在线看片| 欧美激情精品久久久久久免费印度| 亚洲欧美日韩在线高清直播| 国产精品福利无圣光在线一区| 亚洲爱爱爱爱爱| 欧美成在线视频| 国产剧情久久久久久| 夜夜嗨av一区二区三区四区| 欧美一区二区三区精品电影| 久久精品中文字幕电影| 91啪国产在线| 国产精品香蕉av| 亚洲免费av电影| 91av国产在线| 91精品国产91久久| 亚洲欧美成人一区二区在线电影| 久久影视电视剧免费网站| 亚洲自拍欧美色图| 欧美性videos高清精品| 欧美丰满少妇xxxxx做受| 国模视频一区二区三区| 亚洲日韩欧美视频一区| 精品女厕一区二区三区| 午夜精品久久久久久久白皮肤| 国产精品日韩在线播放| 亚洲精品在线视频| 91精品久久久久久久久久久久久| 国产在线精品播放| 色综合老司机第九色激情| 欧美成人午夜视频| 色与欲影视天天看综合网| 成人性教育视频在线观看| 97视频在线观看成人| 亚洲专区中文字幕| 韩剧1988在线观看免费完整版| 日韩欧美国产视频| 成人免费在线视频网址| 亚洲性线免费观看视频成熟| 国产精品一二三在线| 色妞色视频一区二区三区四区| 久久久久久国产三级电影| 日韩国产欧美精品在线| 国产中文日韩欧美| 国产成人精品免高潮在线观看| 欧美激情在线观看视频| 欧美精品videos| 日韩天堂在线视频| 国产亚洲美女久久| 国产精品都在这里| 国产精品午夜一区二区欲梦| 欧美性色视频在线| 一区二区三区视频免费在线观看| 国产精品免费一区二区三区都可以| 久久理论片午夜琪琪电影网| 国产欧美精品一区二区三区介绍| 日韩精品免费观看| 亚洲午夜激情免费视频| 亚洲男女自偷自拍图片另类| 91免费看片在线| 国产香蕉97碰碰久久人人| 欧美日韩一区二区在线| 久久久久久中文字幕| 亚洲乱码国产乱码精品精天堂| 久久久精品在线观看| 国产精品国产福利国产秒拍| 亚洲国产一区二区三区四区| 亚洲美女激情视频| 国产精品伦子伦免费视频| 一区二区在线视频播放| 亚洲xxxx18| 国产91露脸中文字幕在线| 欧美成人午夜影院| 亚洲欧美国产精品| 欧美性猛交xxxx富婆弯腰| 欧美一级视频在线观看| 精品国产福利在线| 美乳少妇欧美精品| 欧美日韩在线免费| 日韩欧美在线看| 欧美日韩国产精品| 久久精品国产亚洲| 97精品国产aⅴ7777| 91色视频在线观看| 7777免费精品视频| 国产成人精品在线观看| 国产精品久久久久免费a∨| 日本aⅴ大伊香蕉精品视频| 国产综合香蕉五月婷在线| 性色av一区二区三区在线观看| 日韩在线一区二区三区免费视频| 亚洲成人精品视频| 亚洲精品一区二区三区婷婷月| 精品福利一区二区| 黑人巨大精品欧美一区二区三区| 国产精品高清网站| 久久精品成人欧美大片| 久久中文精品视频| 欧美另类高清videos| 日韩免费在线电影| 午夜精品理论片| 国产精品美乳一区二区免费| 欧美视频在线观看免费| 中文字幕久热精品在线视频| 亚洲国产一区二区三区四区| 狠狠色狠狠色综合日日五| 国产亚洲精品91在线| 中文字幕亚洲情99在线| 久久91亚洲精品中文字幕奶水| 欧美大片在线看| 国产美女主播一区| 国产精品视频午夜| 色综合视频一区中文字幕| 91精品国产91久久久久久吃药| 激情av一区二区| 亚洲天堂av高清| 91九色单男在线观看| 国产精品旅馆在线|