使用 HTTP2 作開發服務器 (上)

寫做背景

筆者所在項目使用的前端技術比較老舊,在開發的過程當中須要先啓動一個後端項目 (tomcat + mysql + redis) 來作爲靜態服務器javascript

而後使用的是一個公司內部的類AMD模塊加載工具,每次刷新頁面都要加載1000+ 的文件,頁面的響應時間接近20s, 致使開發的過程很是痛苦css

因此決定使用 HTTP/2 來開發一個開發服務器來加快頁面的加載速度. 目前來講效果不錯,相對於 HTTP1.1 來講加載速度提高了 50%html

對於開發環境與咱們相似的項目,能夠嘗試一下。前端

理論基礎

1. HTTP/2 的TCP鏈接複用

雖然咱們開發的時候使用的是本地服務器,創建鏈接的速度和下載速度都很快,可是瀏覽器針對同一域名的併發請求是有上限的。java

當所須要的文件數量不少時,咱們每次只能請求必定數量的文件,當前面的文件的請求完成後才能去請求下一個文件,這就形成了堵塞。node

從圖中咱們能夠看到明顯的連接限制和堵塞mysql

而 HTTP/2 能夠在同一鏈接上進行多個併發交換,能夠避免出現由於瀏覽器的併發限制而形成的堵塞web

HTTP/2 經過支持標頭字段壓縮和在同一鏈接上進行多個併發交換,讓應用更有效地利用網絡資源,減小感知的延遲時間。具體來講,它能夠對同一鏈接上的請求和響應消息進行交錯發送併爲 HTTP 標頭字段使用有效編碼。 HTTP/2 還容許爲請求設置優先級,讓更重要的請求更快速地完成,從而進一步提高性能。出臺的協議對網絡更加友好,由於與 HTTP/1.x 相比,可使用更少的 TCP 鏈接。 這意味着與其餘流的競爭減少,而且鏈接的持續時間變長,這些特性反過來提升了可用網絡容量的利用率。 最後,HTTP/2 還能夠經過使用二進制消息分幀對消息進行更高效的處理。(超文本傳輸協議版本 2,草案 17) - https://developers.google.com...

2. Server Push 服務器推送

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

經過 Server Push,服務器能夠對瀏覽器的單個請求返回多個響應,而不須要等待瀏覽器發出請求再給去響應。sql

簡單舉個?

  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 能夠極大的加快資源文件的加載速度

能夠看到瀏覽器使用一個連接加載完了全部的資源文件

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 顯示

訪問 https://localhost:8443/ ,便可看到協議變爲 h2

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 推送過來的

相關連接

  1. HTTP/2 簡介 | Web | Google Developers
  2. HTTP/2 Server Push 詳解(上) | AlloyTeam
  3. HTTP/2 Server Push 詳解(下) | AlloyTeam
  4. demo 下載地址
相關文章
相關標籤/搜索