使用 HTTP2 做開發伺服器 (上)

Lleo發表於2018-03-27

寫作背景

筆者所在專案使用的前端技術比較老舊,在開發的過程中需要先啟動一個後端專案 (tomcat + mysql + redis) 來做為靜態伺服器

然後使用的是一個公司內部的類AMD模組載入工具,每次重新整理頁面都要載入1000+ 的檔案,頁面的響應時間接近20s, 導致開發的過程非常痛苦

所以決定使用 HTTP/2 來開發一個開發伺服器來加快頁面的載入速度. 目前來說效果不錯,相對於 HTTP1.1 來說載入速度提升了 50%

對於開發環境與我們類似的專案,可以嘗試一下。

理論基礎

1. HTTP/2 的TCP連線複用

雖然我們開發的時候使用的是本地伺服器,建立連線的速度和下載速度都很快,但是瀏覽器針對同一域名的併發請求是有上限的。

當所需要的檔案數量很多時,我們每次只能請求一定數量的檔案,當前面的檔案的請求完成後才能去請求下一個檔案,這就造成了堵塞。

使用 HTTP2 做開發伺服器 (上)

從圖中我們可以看到明顯的連結限制和堵塞

而 HTTP/2 可以在同一連線上進行多個併發交換,可以避免出現因為瀏覽器的併發限制而造成的堵塞

HTTP/2 通過支援標頭欄位壓縮和在同一連線上進行多個併發交換,讓應用更有效地利用網路資源,減少感知的延遲時間。具體來說,它可以對同一連線上的請求和響應訊息進行交錯傳送併為 HTTP 標頭欄位使用有效編碼。 HTTP/2 還允許為請求設定優先順序,讓更重要的請求更快速地完成,從而進一步提升效能。出臺的協議對網路更加友好,因為與 HTTP/1.x 相比,可以使用更少的 TCP 連線。 這意味著與其他流的競爭減小,並且連線的持續時間變長,這些特性反過來提高了可用網路容量的利用率。 最後,HTTP/2 還可以通過使用二進位制訊息分幀對訊息進行更高效的處理。(超文字傳輸協議版本 2,草案 17) - https://developers.google.com/web/fundamentals/performance/http2/?hl=zh-cn

2. Server Push 伺服器推送

HTTP/2 中最令人期待的特性就是 Sever Push (伺服器推送)。

通過 Server Push,伺服器可以對瀏覽器的單個請求返回多個響應,而不需要等待瀏覽器發出請求再給去響應。

簡單舉個?

  1. 瀏覽器向伺服器傳送 a.com 請求
  2. 伺服器確定這個請求返回一個 index.html 檔案,同時發現這個檔案需要 style.css 和 script.js 檔案
  3. 伺服器向瀏覽器放回 index.html 的響應,同時告訴瀏覽器我這裡有 style.css 和 script.js 檔案你可能需要
  4. 瀏覽器收到 index.html 後,解析後發現需要 style.css 和 script.js,正好伺服器端說可以推送這兩個資源,瀏覽器就不需要再次傳送請求去獲取,而是直接就收伺服器的推送

結合上面的連線複用,HTTP/2 可以極大的加快資原始檔的載入速度

使用 HTTP2 做開發伺服器 (上)

可以看到瀏覽器使用一個連結載入完了所有的資原始檔

Nodejs HTTP/2 模組簡單使用

這裡先簡單介紹下 Node 中 HTTP/2 的使用,下篇文章將詳細闡述如何編寫一個可以應用的 HTTP/2 開發伺服器

1. 建立 HTTPS 證照

由於 HTTP/2 需要使用 HTTPS,這裡我們需要先生成一個證照。

openssl req -x509 -newkey rsa:2048 -nodes -sha256 -subj '/CN=localhost' \
  -keyout localhost-privkey.pem -out localhost-cert.pem
複製程式碼

2. 專案結構

.
├── certificate
│   ├── localhost-cert.pem
│   └── localhost-privkey.pem
├── node_modules
├── package-lock.json
├── package.json
├── src
│   └── app.js
└── www
    ├── index.html
    ├── script.js
    └── styles.css
複製程式碼
<!-- www/index.html -->

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>HTTP2 demo</title>
    <link rel="stylesheet" href="styles.css">
</head>

<body>
    <div id="content"></div>
    <script src="script.js"></script>
</body>

</html>
複製程式碼
// www/script.js

const content = document.querySelector("#content");

content.innerHTML = `<h1>Hello HTTP/2</h1>`;
複製程式碼
/* www/styles.css */

h1 {
  color: cornflowerblue;
}
複製程式碼

建立如上的專案結構

3. 建立伺服器

const http2 = require("http2");
const fs = require("fs");
const path = require("path");

const server = http2.createSecureServer({
  key: fs.readFileSync(
    path.resolve(__dirname, "../certificate/localhost-privkey.pem")
  ),
  cert: fs.readFileSync(
    path.resolve(__dirname, "../certificate/localhost-cert.pem")
  )
});
server.on("error", err => console.error(err));

server.on("stream", (stream, headers) => {
  // stream is a Duplex
  stream.respond({
    "content-type": "text/html",
    ":status": 200
  });
  stream.end("<h1>Hello World</h1>");
});

server.listen(8443);
複製程式碼

開啟控制檯,進入 Network ,開啟 Protocol 顯示

使用 HTTP2 做開發伺服器 (上)

訪問 https://localhost:8443/ ,即可看到協議變為 h2

使用 HTTP2 做開發伺服器 (上)

4. 啟用伺服器端推送

const http2 = require("http2");
const fs = require("fs");
const path = require("path");

const pemPath = path.resolve(__dirname, "../certificate/localhost-privkey.pem");
const certPaht = path.resolve(__dirname, "../certificate/localhost-cert.pem");

// 獲取 HTTP2 header 常量
const { HTTP2_HEADER_PATH, HTTP2_HEADER_STATUS } = http2.constants;

// 獲取靜態目錄下的所有檔案資訊
function createFileInfoMap() {
  let fileInfoMap = new Map();
  const fileList = fs.readdirSync(staticPath);
  const contentTypeMap = {
    js: "application/javascript",
    css: "text/css",
    html: "text/html"
  };

  fileList.forEach(file => {
    const fd = fs.openSync(path.resolve(staticPath, file), "r");
    const contentType = contentTypeMap[file.split(".")[1]];

    const stat = fs.fstatSync(fd);

    const headers = {
      "content-length": stat.size,
      "last-modified": stat.mtime.toUTCString(),
      "content-type": contentType
    };

    fileInfoMap.set(`/${file}`, {
      fd,
      headers
    });
  });

  return fileInfoMap;
}

// 定義靜態目錄
const staticPath = path.resolve(__dirname, "../www");
const fileInfoMap = createFileInfoMap();

// 將傳入的檔案推送到瀏覽器
function push(stream, path) {
  const file = fileInfoMap.get(path);

  if (!file) {
    return;
  }

  stream.pushStream({ [HTTP2_HEADER_PATH]: path }, (err, pushStream) => {
    pushStream.respondWithFD(file.fd, file.headers);
  });
}

const server = http2.createSecureServer({
  key: fs.readFileSync(pemPath),
  cert: fs.readFileSync(certPaht)
});
server.on("error", err => console.error(err));

server.on("stream", (stream, headers) => {
  // 獲取請求路徑
  let requestPath = headers[HTTP2_HEADER_PATH];

  // 請求到 '/' 的請求返回 index.html
  if (requestPath === "/") {
    requestPath = "/index.html";
  }

  // 根據請求路徑獲取對應的檔案資訊
  const fileInfo = fileInfoMap.get(requestPath);
  if (!fileInfo) {
    stream.respond({
      [HTTP2_HEADER_STATUS]: 404
    });
    stream.end("Not found");
  }

  // 訪問首頁時同時推送其他檔案資源
  if (requestPath === "/index.html") {
    for (let key in fileInfoMap.keys()) {
      push(stream, key);
    }
  }

  // 推送首頁資料
  stream.respondWithFD(fileInfo.fd, {
    ...fileInfo.headers
  });
});

server.listen(8443);
複製程式碼

訪問 https://localhost:8443 就可以看到 styles.css 和 script.js 是通過 HTTP/2 推送過來的

使用 HTTP2 做開發伺服器 (上)

相關連結

  1. HTTP/2 簡介 | Web | Google Developers
  2. HTTP/2 Server Push 詳解(上) | AlloyTeam
  3. HTTP/2 Server Push 詳解(下) | AlloyTeam
  4. demo 下載地址
  5. 原文地址

相關文章