[譯] Node.js 能進行 HTTP/2 推送啦!

0x7e2發表於2019-03-02

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 在自動推送方面的最初的努力。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章