HTTP/2 服務器推送(Server push)實踐

自從 2015 年 HTTP/2 標準正式發佈以來,各大主要 Web 服務器以及各大主要瀏覽器已經陸續完成了對終版 HTTP/2 協議的支持。對於各位站長來講,這項技術也已經不是什麼新鮮玩意。然而 HTTP/2 的一項子功能「服務器推送」——儘管當初被吹得很火——卻沒有跟上 HTTP/2 推廣的步伐。css

HTTP/2 服務器推送

HTTP/2 服務器推送是一種提高首屏加載速度的技術,它容許 Web 服務器在收到瀏覽器的請求以前提早發送一些資源給客戶端。好比說某個頁面 index.html 使用了 a.cssb.js 兩個子資源,Web 服務器在返回 index.html 的內容後表示「你可能還須要這兩個文件」將 a.cssb.js 的內容一併發送給了客戶端瀏覽器,因而瀏覽器就不須要另外去單獨請求這兩個文件。html

看起來一切都是這麼美好,然而現實狀況卻沒有這麼簡單。首要問題就是 Web 服務器怎樣知道客戶端須要什麼,若是推送了沒必要要的資源——好比某個資源已經被瀏覽器緩存——不只不能提高加載速度還會形成網絡帶寬的浪費。nginx

固然還有另一個更重要緣由:我大 Nginx 根本不支持 HTTP/2 服務器推送,你想體驗一把都不行。。。git

這裏得誇獎一下咱們的老大哥 Apache Httpd,在很早以前就支持了,但 Apache 並未能(徹底)解決過度推送的問題。在這裏筆者給你們安利的是另一款 Web 服務器:H2Ogithub

H2O:爲 HTTP/2 而生的 Web 服務器

H2O 是一款新的 Web 服務器,它在 HTTP/2 正式標準化的那年發佈穩定版 1.0 版本,口號就是「可徹底利用 HTTP/2 的特性」,並且號稱比 Nginx 還快。這裏筆者並不打算比較二者的性能,但對 HTTP/2 的支持的確是 H2O 走在了前面。apache

注:Nginx 有個第三方模塊用於實現 HTTP/2 服務器推送,實測還不能正常使用。數組

HTTP/2 服務器推送實踐

H2O 安裝的話很簡單。筆者是 macOS 用戶能夠 brew install h2o 直接安裝,Linux 用戶可使用官方的 rpm 鏡像包安裝。promise

要啓用 HTTP/2 首先要啓用 SSL,要啓用 SSL 就首先得有個證書。筆者這裏直接建立了一個指向本機(127.0.0.1)的域名而後用 Let's engypt 生成了有效證書;若是讀者沒有此條件也能夠用 openssl 生成一個自簽名證書而後將其加入系統鑰匙串(網上資料不少,也有專門生成自簽名證書的網站)。瀏覽器

H2O 的配置文件是含有少許擴展的 YAML 文件,簡單配置以下:緩存

hosts:
  "test.eoitek.net:80": # 你的域名
    listen:
      port: 80 # 監聽 80 端口
    paths:
      /:
        redirect: https://test.eoitek.net/ # 重定向至 HTTPS
    access-log: /dev/stdout # 測試時簡單的把 log 輸出至控制檯
  "test.eoitek.net:443": # 域名
    listen:
      port: 443 # 監聽 443 端口
      ssl:
        certificate-file: /Users/Carter/.acme.sh/test.eoitek.net_ecc/fullchain.cer # 公鑰(徹底證書鏈,用於自動 OCSP stapling)
        key-file: /Users/Carter/.acme.sh/test.eoitek.net_ecc/test.eoitek.net.key # 私鑰
        minimum-version: TLSv1.2 # 最小支持 TLS 版本(想支持 IE 8 須要設置爲 TLSv1.0)
        dh-file: dhparam.pem # DH祕鑰(openssl dhparam -out dhparam.pem 2048)
        cipher-preference: server # 讓服務器決定使用的加密套件
        cipher-suite: "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256" # 支持的加密套件
    paths:
      /:
        mruby.handler: | # 注入 mruby 腳本
          lambda do |env|
            [399, {"link" => "</test.css>; rel=preload; as=style"}, []]
          end
        file.dir: . # 映射的服務器路徑
    access-log: /dev/stdout # 將 log 輸出至控制檯
http2-casper: ON # 啓用 [cache-aware server-push](https://h2o.examp1e.net/configure/http2_directives.html#http2-casper)
compress: ON # 啓用即時壓縮(同時會啓用 brotli 壓縮支持)

其中的 mruby 腳本是啓用 HTTP/2 服務器推送的重點。注入的腳本是一個 lambda 表達式,env 是函數參數,包括了客戶端請求信息。返回值是一個數組,格式爲 [HTTP 狀態碼, { 返回頭 }, [返回體]]。特別的,當 HTTP 狀態碼爲 399 時表示將本次請求交給其餘處理程序處理(即把這段 mruby 腳本當作中間件使用)。因此這段腳本的意思即「將全部的請求添加返回頭 link: </test.css>; rel=preload; as=style」。

H2O 會識別 mruby 腳本輸出的 link 返回頭,當基本條件成立時就會將對應的文件(示例中爲「/test.css」)推送給客戶端。

啓動 H2O(由於監聽了 80 和 443 端口,因此須要 sudo 權限),能夠看到 H2O 幫咱們自動搞定了 OCSP stapling

$ sudo h2o -c h2o.conf
[INFO] raised RLIMIT_NOFILE to 10240
h2o server (pid:15880) is ready to serve requests
fetch-ocsp-response (using OpenSSL 1.1.0g  2 Nov 2017)
fetch-ocsp-response (using OpenSSL 1.1.0g  2 Nov 2017)
sending OCSP request to http://ocsp.int-x3.letsencrypt.org
sending OCSP request to http://ocsp.int-x3.letsencrypt.org
/Users/Carter/.acme.sh/test.eoitek.net_ecc/fullchain.cer: good
    This Update: Nov 25 14:00:00 2017 GMT
    Next Update: Dec  2 14:00:00 2017 GMT
verifying the response signature
/Users/Carter/.acme.sh/test.eoitek.net_ecc/fullchain.cer: good
    This Update: Nov 25 14:00:00 2017 GMT
    Next Update: Dec  2 14:00:00 2017 GMT
verifying the response signature
verify OK (used: -VAfile /tmp/pszPkNSxUe/issuer.crt)
[OCSP Stapling] successfully updated the response for certificate file:/Users/Carter/.acme.sh/test.eoitek.net_ecc/fullchain.cer
verify OK (used: -VAfile /tmp/S5a00hOcQ6/issuer.crt)
[OCSP Stapling] successfully updated the response for certificate file:/Users/Carter/.acme.sh/test.eoitek.net_ecc/fullchain.cer

在 Chrome 瀏覽器中測試,Network 中帶 Push / XXX 字樣的「僞」請求即爲由服務器推送到客戶端的文件。

clipboard.png

若是看得不很清楚,可使用 nghttp 調試 HTTP/2 流量:

$ nghttp -nv https://test.eoitek.net # -n 表示丟棄返回體,-v 表示輸出冗餘調試信息
[  0.008] Connected
The negotiated protocol: h2
[  0.010] send SETTINGS frame <length=12, flags=0x00, stream_id=0>
          (niv=2)
          [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100]
          [SETTINGS_INITIAL_WINDOW_SIZE(0x04):65535]
[  0.010] send PRIORITY frame <length=5, flags=0x00, stream_id=3>
          (dep_stream_id=0, weight=201, exclusive=0)
[  0.010] send PRIORITY frame <length=5, flags=0x00, stream_id=5>
          (dep_stream_id=0, weight=101, exclusive=0)
[  0.010] send PRIORITY frame <length=5, flags=0x00, stream_id=7>
          (dep_stream_id=0, weight=1, exclusive=0)
[  0.010] send PRIORITY frame <length=5, flags=0x00, stream_id=9>
          (dep_stream_id=7, weight=1, exclusive=0)
[  0.010] send PRIORITY frame <length=5, flags=0x00, stream_id=11>
          (dep_stream_id=3, weight=1, exclusive=0)
[  0.010] send HEADERS frame <length=38, flags=0x25, stream_id=13>
          ; END_STREAM | END_HEADERS | PRIORITY
          (padlen=0, dep_stream_id=11, weight=16, exclusive=0)
          ; Open new stream
          :method: GET
          :path: /
          :scheme: https
          :authority: test.eoitek.net
          accept: */*
          accept-encoding: gzip, deflate
          user-agent: nghttp2/1.28.0
[  0.010] recv SETTINGS frame <length=6, flags=0x00, stream_id=0>
          (niv=1)
          [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100]
[  0.010] recv WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=0>
          (window_size_increment=16711681)
[  0.010] recv SETTINGS frame <length=0, flags=0x01, stream_id=0>
          ; ACK
          (niv=0)
[  0.010] recv (stream_id=13) :method: GET
[  0.010] recv (stream_id=13) :scheme: https
[  0.010] recv (stream_id=13) :authority: test.eoitek.net
[  0.010] recv (stream_id=13) :path: /test.css
[  0.010] recv (stream_id=13) accept: */*
[  0.010] recv (stream_id=13) accept-encoding: gzip, deflate
[  0.010] recv (stream_id=13) user-agent: nghttp2/1.28.0
[  0.010] recv PUSH_PROMISE frame <length=44, flags=0x04, stream_id=13>
          ; END_HEADERS
          (padlen=0, promised_stream_id=2)
[  0.010] recv (stream_id=2) :status: 200
[  0.010] recv (stream_id=2) server: h2o/2.3.0-DEV
[  0.010] recv (stream_id=2) link: </test.css>; rel=preload; as=style
[  0.010] recv (stream_id=2) date: Sun, 26 Nov 2017 13:47:14 GMT
[  0.010] recv (stream_id=2) date: Sun, 26 Nov 2017 13:47:14 GMT
[  0.010] recv (stream_id=2) content-type: text/css
[  0.010] recv (stream_id=2) last-modified: Sat, 25 Nov 2017 15:02:22 GMT
[  0.010] recv (stream_id=2) etag: "5a1985fe-8a88"
[  0.010] recv (stream_id=2) accept-ranges: none
[  0.010] recv (stream_id=2) x-content-type-options: nosniff
[  0.010] recv (stream_id=2) content-encoding: gzip
[  0.010] recv (stream_id=2) vary: accept-encoding
[  0.010] recv (stream_id=2) x-http2-push: pushed
[  0.010] recv HEADERS frame <length=177, flags=0x04, stream_id=2>
          ; END_HEADERS
          (padlen=0)
          ; First push response header
[  0.010] recv (stream_id=13) :status: 200
[  0.010] recv (stream_id=13) server: h2o/2.3.0-DEV
[  0.010] recv (stream_id=13) link: </test.css>; rel=preload; as=style
[  0.010] recv (stream_id=13) date: Sun, 26 Nov 2017 13:47:14 GMT
[  0.010] recv (stream_id=13) date: Sun, 26 Nov 2017 13:47:14 GMT
[  0.010] recv (stream_id=13) content-type: text/html
[  0.010] recv (stream_id=13) last-modified: Sat, 29 Jul 2017 13:49:11 GMT
[  0.010] recv (stream_id=13) etag: "597c9257-28a"
[  0.010] recv (stream_id=13) accept-ranges: none
[  0.010] recv (stream_id=13) x-content-type-options: nosniff
[  0.010] recv (stream_id=13) content-encoding: gzip
[  0.010] recv (stream_id=13) vary: accept-encoding
[  0.010] recv (stream_id=13) set-cookie: h2o_casper=AAAAAAADoA; Path=/; Expires=Tue, 01 Jan 2030 00:00:00 GMT; Secure
[  0.010] recv HEADERS frame <length=113, flags=0x04, stream_id=13>
          ; END_HEADERS
          (padlen=0)
          ; First response header
[  0.010] recv DATA frame <length=3837, flags=0x01, stream_id=2>
          ; END_STREAM
[  0.010] recv DATA frame <length=355, flags=0x01, stream_id=13>
          ; END_STREAM
[  0.011] send GOAWAY frame <length=8, flags=0x00, stream_id=0>
          (last_stream_id=2, error_code=NO_ERROR(0x00), opaque_data(0)=[])

上面輸出中能夠很清楚的看到:HTTP/2 是經過發送數據幀來達成雙端通訊的。服務端給客戶端推送了一個 PUSH_PROMISE 幀,表示服務器推送的文件,它同樣帶有返回頭,只是沒有請求頭。

特別的,服務器給客戶端發送了一個名爲 h2o_casper 的 Cookie,這個 Cookie 就是用來標識客戶端緩存的。H2O 經過這個 Cookie 識別已經給客戶端推送過哪些文件(客戶端緩存了哪些文件),從而最大限度避免浪費帶寬。讀者可能會發現再次刷新瀏覽器就算禁用緩存服務器也不會向客戶端推送文件,就是這個 Cookie 在起做用。

相關文章
相關標籤/搜索