[译]Node.js 能进行 HTTP/2 推送啦!

[译] Node.js 能进行 HTTP/2 推送啦!

Node.js 能进行 HTTP/2 推送啦!

本文由来自 @nearForm 的首席架构师、Node.js 技术指导委员会成员 Matteo Collina 以及谷歌软件工程师 Jinwoo Lee 共同撰写。

自从 2017 年 7 月 Node.js 中引入 HTTP/2 以来,该实践经历了好几轮的改进。现在我们基本已经准备好去掉“实验性”标志。当然最好使用 Node.js 版本 9 来尝试 HTTP/2 支持,因为这个版本有着最新的修复和改进的内容。

最简单的入门方法是使用新版 http2 核心模块部分提供的的兼容层

const http2 = require('http2');
const options = {
 key: getKeySomehow(),
 cert: getCertSomehow()
};

// 必须使用 https
// 不然浏览器无法连接
const server = http2.createSecureServer(options, (req, res) => {
 res.end('Hello World!');
});
server.listen(3000);

兼容层提供了和 require('http') 相同的高级 API(具有请求和响应对象相同的请求侦听器),这样就可以平滑的迁移到 HTTP/2。

兼容层的也为 web 框架作者提供了一个简单的升级途径,到目前为止,RestifyFastify 都基于 Node.js HTTP/2 兼容层实现了对 HTTP/2 的支持。

Fastify 是一个新的 web 框架,它专注于性能而不牺牲开发者的生产力,也不抛弃最近升级到 1.0.0 版本的丰富的插件生态系统。

在 fastify 中使用 HTTP/2 非常简单:

const Fastify = require('fastify');

// 必须使用 https
// 不然浏览器无法连接
const fastify = Fastify({
 http2: true,         // 译者注:原文作者这里少了逗号
 https: {
   key: getKeySomehow(),
   cert: getCertSomehow()
 }
});

fastify.get('/fastify', async (request, reply) => {
 return 'Hello World!';
});

server.listen(3000);

尽管能在 HTTP/1.1 和 HTTP/2 上运行相同的应用代码对于协议的选择非常重要,但单独的兼容层并没有提供 HTTP/2 支持的一些更强大的功能。http2 核心模块可以通过”流“侦听器来实现对新的核心 API(Http2Stream)来使用这些额外的功能:

const http2 = require('http2');
const options = {
 key: getKeySomehow(),
 cert: getCertSomehow()
};

// 必须使用 https
// 不然浏览器无法连接
const server = http2.createSecureServer(options);
server.on('stream', (stream, headers) => {
 // 流是双工的
 // headers 是一个包含请求头的对象

 // 响应将把 headers 发到客户端
 // meta headers 用冒号(:)开头
 stream.respond({ ':status': 200 });

 // 这是 stream.respondWithFile()
 // 和 stream.pushStream()

 stream.end('Hello World!');
});

server.listen(3000);

在 Fastify 中, 可以通过 request.raw.stream API 访问 Http2Stream 如下所示:

fastify.get('/fastify', async (request, reply) => {
 request.raw.stream.pushStream({
  ':path': '/a/resource'
 }, function (err, stream) {
  if (err) {
    request.log.warn(err);
    return
  }
  stream.respond({ ':status': 200 });
  stream.end('content');
 });

 return 'Hello World!';
});

HTTP/2 推送 —— 机遇与挑战

HTTP/2 在 HTTP/1 的基础上对性能进行了相当大的提升,服务端推送是其一大成果。

典型的(或者说是简化的)HTTP 请求和响应的流程应该像是这样(下面屏幕截图是和 Hack News 的连接):

  1. 浏览器请求 HTML 文档。
  2. 服务器处理请求并生成以及发回 HTML 文档。
  3. 浏览器收到响应并对 HTML 文档进行解析。
  4. 浏览器会为 HTML 文档渲染过程中需要的更多资源,比如样式表、图像、 JavaScript 文件等发送更多请求(来获取这些资源)。
  5. 服务器响应对每个资源的请求。
  6. 浏览器使用 HTML 文档和相关的资源来渲染出页面。

这意味着渲染一个 HTML 文档通常会需要多次请求和响应,因为浏览器需要额外与其关联的资源来完成对文档的正确渲染。如果这些相关的资源能在不需要浏览器请求的情况下随原始 HTML 文档一起发送给浏览器,那就太棒了。这也正是 HTTP/2 服务端推送的目的。

在 HTTP/2 中,服务器可以主动将它认为浏览器稍候会请求的额外资源和原来的请求响应一起推送。如果稍后浏览器真的需要这些额外资源,它只是会使用已经推送的资源,而不去发送额外的请求。 例如,假设服务器正在发送这个 /index.html 文件

<!DOCTYPE html>
<html>
<head>
  <title>Awesome Unicorn!</title>
  <link rel="stylesheet" type="text/css" href="/static/awesome.css">
</head>
<body>
  This is an awesome Unicorn! <img src="/static/unicorn.png">
</body>
</html>

服务器将通过发回这个文件来响应请求。但它知道 /index.html 需要 /static/awesome.css 和 /static/unicorn.png 才能正确渲染。因此,服务器将这些文件和 /index.html 一起推送

for (const asset of ['/static/awesome.css', '/static/unicorn.png']) {
  // stream 是 ServerHttp2Stream。
  stream.pushStream({':path': asset}, (err, pushStream) => {
    if (err) throw err;
    pushStream.respondWithFile(asset);
  });
}

在客户端,一但浏览器解析 /index.html,它会指出需要 /static/awesome.css 和 /static/unicorn.png,但是浏览器得知他们已经被推动并存储到了缓存中!所有他并不需要发送两个额外的请求,而是使用已经推送的资源。

这听起来蛮不错。但是有一些挑战(难点)。首先,服务器要想知道为原始请求推送哪些附加资源并不是那么容易。虽然我们可以把这个决定权放到应用程序层,但是让开发人员做出决定也同样不简单。一种方法是手动解析 HTML,找出其所需要的资源列表。但是随着应用程序的迭代和 HTML 文件的更新,维护该列表的工作将非常繁琐而且容易出错。

另一个挑战来自浏览器内部缓存先前检索到的资源。使用上面的例子,如果浏览器昨天加载了 /index.html,它也会加载 /static/unicorn.png,并且该文件通常会缓存在浏览器中。当浏览器加载 /index.html,然后尝试加载 /static/unicorn.png 时,它知道后者已经被缓存,并且只会使用它而不是去再次请求。这种情况下,如果服务器推送 /static/unicorn.png 就会浪费带宽。所以服务器应该有一些方法来判断资源是否已经缓存到了浏览器中。

还会有其他类型的挑战,以及针对 HTTP/2 推送文档的经验法则等这些。

HTTP/2 自动推送

为了方便 Node.js 开发者支持服务端推送功能,Google 发布了一个 npm 包来实现自动化:h2-auto-push。其设计目的是处理上面和 针对 HTTP/2 推送文档的经验法则 中提到的诸多挑战。

它会监视来自浏览器的请求的模式,并且确定与最初请求资源相关联的附加资源。之后如果请求原始资源,相关的资源会自动推送到浏览器。它还将估计浏览器是否可能已经缓存了某个资源,如果确定了就会跳过推送。

h2-auto-push 被设计为供各种 web 框架使用的中间件。作为一个静态文件服务中间件,使用这个 npm 包开发一个自动推送中间件非常容易。比如说请参阅 fastify-auto-push。这是一个支持 HTTP/2 自动推送并使用 h2-auto-push 包的 fastify 插件。

在应用程序中使用这个中间件也非常容易

const fastify = require('fastify');
const fastifyAutoPush = require('fastify-auto-push');
const fs = require('fs');
const path = require('path');
const {promisify} = require('util');

const fsReadFile = promisify(fs.readFile);

const STATIC_DIR = path.join(__dirname, 'static');
const CERTS_DIR = path.join(__dirname, 'certs');
const PORT = 8080;

async function createServerOptions() {
  const readCertFile = (filename) => {
    return fsReadFile(path.join(CERTS_DIR, filename));
  };
  const [key, cert] = await Promise.all(
      [readCertFile('server.key'), readCertFile('server.crt')]);
  return {key, cert};
}

async function main() {
  const {key, cert} = await createServerOptions();
  // 浏览器只支持 https 使用 HTTP/2。
  const app = fastify({https: {key, cert}, http2: true});

  // 新建并注册自动推送插件
  // 它应该注册在中间件链的一开始。
  app.register(fastifyAutoPush.staticServe, {root: STATIC_DIR});

  await app.listen(PORT);
  console.log(`Listening on port ${PORT}`);
}

main().catch((err) => {
  console.error(err);
});

很简单,是吧?

我们的测试表明,h2-auto-push 比 HTTP/2 的性能提高了 12%,比 HTTP/1 提高了大概 135%。我们希望本文能让您更好地理解 HTTP2 以及其可以为您应用带来的好处,包括 HTTP2 推送。

特别感谢 nearForm 的 James Snell 和 David Mark Clements 以及 Google 的 Ali SheikhKelvin Jin 能帮忙编辑这篇博文。非常感谢 Google 的 Matt Loring 在自动推送方面的最初的努力。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容