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

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

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

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

最简单的入门方法是使用新版 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。node

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

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

在 fastify 中使用 HTTP/2 很是简单:git

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)来使用这些额外的功能:github

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 以下所示:web

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 的基础上对性能进行了至关大的提高,服务端推送是其一大成果。npm

典型的(或者说是简化的)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 在自动推送方面的最初的努力。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索