iOS HTTP/2 Server Push 探索

本文首發地址為-iOS HTTP/2 Server Push 探索 | 李劍飛的博客


HTTP/2

HTTP/2 Server Push 是什么

當用戶的瀏覽器和服務器在建立鏈接后,服務器主動將一些資源推送給瀏覽器并緩存起來,這樣當瀏覽器接下來請求這些資源時就直接從緩存中讀取,不會在從服務器上拉了,提升了速率。舉一個例子就是:

假如一個頁面有3個資源文件index.html,index.css,index.js,當瀏覽器請求index.html的時候,服務器不僅返回index.html的內容,同時將index.css和index.js的內容push給瀏覽器,當瀏覽器下次請求這2兩個文件時就可以直接從緩存中讀取了。

如下圖所示:


Apple-http2ServerPush

HTTP/2 Server Push 原理是什么

要想了解server push原理,首先要理解一些概念。我們知道HTTP/2傳輸的格式并不像HTTP1使用文本來傳輸,而是啟用了二進制幀(Frames)格式來傳輸,和server push相關的幀主要分成這幾種類型:

  1. HEADERS frame(請求返回頭幀):這種幀主要攜帶的http請求頭信息,和HTTP1的header類似。
  2. DATA frames(數據幀) :這種幀存放真正的數據content,用來傳輸。
  3. PUSH_PROMISE frame(推送幀):這種幀是由server端發送給client的幀,用來表示server push的幀,這種幀是實現server push的主要幀類型。
  4. RST_STREAM(取消推送幀):這種幀表示請求關閉幀,簡單講就是當client不想接受某些資源或者接受timeout時會向發送方發送此幀,和PUSH_PROMISE frame一起使用時表示拒絕或者關閉server push。

(PS:HTTP/2相關的幀其實包括10種幀,正是因為底層數據格式的改變,才為HTTP/2帶來許多的特性,幀的引入不僅有利于壓縮數據,也有利于數據的安全性和可靠傳輸性。)

了解了相關的幀類型,下面就是具體server push的實現過程了:

  1. 由多路復用我們可以知道HTTP/2中對于同一個域名的請求會使用一條tcp鏈接而用不同的stream ID來區分各自的請求。
  2. 當client使用stream 1請求index.html時,server正常處理index.html的請求,并可以得知index.html頁面還將要會請求index.css和index.js。
  3. server使用stream 1發送PUSH_PROMISE frame給client告訴client我這邊可以使用stream 2來推送index.js和stream 3來推送index.css資源。
  4. server使用stream 1正常的發送HEADERS frame和DATA frames將index.html的內容返回給client。
  5. client接收到PUSH_PROMISE frame得知stream 2和stream 3來接收推送資源。
  6. server拿到index.css和index.js便會發送HEADERS frame和DATA frames將資源發送給client。
  7. client拿到push的資源后會緩存起來當請求這個資源時會從直接從從緩存中讀取。

Server Push 怎么用

使用 nghttp2 調試 HTTP/2 流量

查看 HTTP/2 流量的幾種方式

  • 在 Chrome 地址欄輸入 chrome://net-internals/#http2,使用 Chrome 自帶的 HTTP/2 調試工具;
    使用方便,但受限于 Chrome 瀏覽器,對于 Chrome 不支持的 h2c(HTTP/2 Cleartext,沒有部署 TLS 的 HTTP/2)協議無能為力。同時,這個工具顯示的信息經過了解析和篩選,不夠全面。
  • 使用 Wireshark 調試 HTTP/2 流量;
    Wireshark 位于服務端和瀏覽器之間,充當的是中間人角色,用它查看 HTTP/2 over HTTPS 流量時,必須擁有網站私鑰或者借助瀏覽器共享對稱密鑰,才能解密 TLS 流量,配置起來比較麻煩。

nghttp2,是一個用 C 實現的 HTTP/2 庫,支持 h2c。它可以做為其它軟件的一部分,為其提供 HTTP/2 相關功能(例如 curl 的 HTTP/2 功能就是用的 nghttp2)。除此之外,它還提供了四個有用的 HTTP/2 工具:

  • nghttp:HTTP/2 客戶端;
  • nghttpd:HTTP/2 服務端;
  • nghttpx:HTTP/2 代理,提供 HTTP/1、HTTP/2 等協議之間的轉換;
  • h2load:HTTP/2 性能測試工具;

nghttp2 安裝

先來用 brew 看一下有沒有 nghttp 相關的庫:

~ brew search nghttp
nghttp2

看來是有 nghttp2 的,再用 brew 看下需要安裝哪些環境:

~ brew info nghttp2
nghttp2: stable 1.21.0 (bottled), HEAD
HTTP/2 C Library
https://nghttp2.org/
Not installed
From: https://github.com/Homebrew/homebrew-core/blob/master/Formula/nghttp2.rb
==> Dependencies
Build: sphinx-doc ?, pkg-config ?, cunit ?
Required: c-ares ?, libev ?, openssl ?, libevent ?, jansson ?, boost ?, spdylay ?
Recommended: jemalloc ?
==> Requirements
Optional: python3 ?
==> Options
--with-examples
    Compile and install example programs
--with-python3
    Build python3 bindings
--without-docs
    Don't build man pages
--without-jemalloc
    Build without jemalloc support
--HEAD
    Install HEAD version

看來需要的依賴還挺多。

使用 brew 安裝 nghttp2 :

brew install nghttp2

一切妥當后,nghttp2 提供的幾個工具就可以直接用了。

nghttp

nghttp 做為一個功能完整的 HTTP/2 客戶端,非常適合用來查看和調試 HTTP/2 流量。它支持的參數很多,通過官方文檔或者 nghttp -h 都能查看。最常用幾個參數如下:

  • -v, --verbose,輸出完整的 debug 信息;
  • -n, --null-out,丟棄下載的數據;
  • -a, --get-assets,下載 html 中的 css、js、image 等外鏈資源;
  • -H, --header = < HEADER >,添加請求頭部字段,如 -H':method: PUT';
  • -u, --upgrade,使用 HTTP 的 Upgrade 機制來協商 HTTP/2 協議,用于 h2c,詳見下面的例子;

以下是使用 nghttp 訪問 https://h2o.examp1e.net 的結果。從調試信息中可以清晰看到 h2c 協商以及 Server Push 的整個過程:

nghttp -nv 'https://h2o.examp1e.net'
[  0.201] Connected
The negotiated protocol: h2
[  1.180] send SETTINGS frame <length=12, flags=0x00, stream_id=0>
          (niv=2)
          [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100]
          [SETTINGS_INITIAL_WINDOW_SIZE(0x04):65535]
[  1.180] send PRIORITY frame <length=5, flags=0x00, stream_id=3>
          (dep_stream_id=0, weight=201, exclusive=0)
[  1.180] send PRIORITY frame <length=5, flags=0x00, stream_id=5>
          (dep_stream_id=0, weight=101, exclusive=0)
[  1.180] send PRIORITY frame <length=5, flags=0x00, stream_id=7>
          (dep_stream_id=0, weight=1, exclusive=0)
[  1.180] send PRIORITY frame <length=5, flags=0x00, stream_id=9>
          (dep_stream_id=7, weight=1, exclusive=0)
[  1.180] send PRIORITY frame <length=5, flags=0x00, stream_id=11>
          (dep_stream_id=3, weight=1, exclusive=0)
[  1.180] send HEADERS frame <length=39, 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: h2o.examp1e.net
          accept: */*
          accept-encoding: gzip, deflate
          user-agent: nghttp2/1.21.1
[  1.373] recv SETTINGS frame <length=12, flags=0x00, stream_id=0>
          (niv=2)
          [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100]
          [SETTINGS_INITIAL_WINDOW_SIZE(0x04):16777216]
[  1.373] recv SETTINGS frame <length=0, flags=0x01, stream_id=0>
          ; ACK
          (niv=0)
[  1.373] recv (stream_id=13) :method: GET
[  1.373] recv (stream_id=13) :scheme: https
[  1.373] recv (stream_id=13) :authority: h2o.examp1e.net
[  1.373] recv (stream_id=13) :path: /search/jquery-1.9.1.min.js
[  1.373] recv (stream_id=13) accept: */*
[  1.373] recv (stream_id=13) accept-encoding: gzip, deflate
[  1.373] recv (stream_id=13) user-agent: nghttp2/1.21.1
[  1.373] recv PUSH_PROMISE frame <length=59, flags=0x04, stream_id=13>
          ; END_HEADERS
          (padlen=0, promised_stream_id=2)
[  1.373] recv (stream_id=2) :status: 200
[  1.373] recv (stream_id=2) server: h2o/2.2.0-beta2
[  1.373] recv (stream_id=2) date: Mon, 10 Apr 2017 06:30:29 GMT
[  1.373] recv (stream_id=2) content-type: application/javascript
[  1.373] recv (stream_id=2) last-modified: Thu, 14 May 2015 04:10:14 GMT
[  1.373] recv (stream_id=2) etag: "55542026-169d5"
[  1.373] recv (stream_id=2) accept-ranges: bytes
[  1.373] recv (stream_id=2) x-http2-push: pushed
[  1.373] recv (stream_id=2) content-length: 92629
[  1.373] recv HEADERS frame <length=126, flags=0x04, stream_id=2>
          ; END_HEADERS
          (padlen=0)
          ; First push response header
[  1.373] recv (stream_id=13) :method: GET
[  1.373] recv (stream_id=13) :scheme: https
[  1.373] recv (stream_id=13) :authority: h2o.examp1e.net
[  1.373] recv (stream_id=13) :path: /search/oktavia-jquery-ui.js
[  1.373] recv (stream_id=13) accept: */*
[  1.373] recv (stream_id=13) accept-encoding: gzip, deflate
[  1.373] recv (stream_id=13) user-agent: nghttp2/1.21.1
[  1.373] recv PUSH_PROMISE frame <length=33, flags=0x04, stream_id=13>
          ; END_HEADERS
          (padlen=0, promised_stream_id=4)
[  1.373] recv (stream_id=4) :status: 200
[  1.373] recv (stream_id=4) server: h2o/2.2.0-beta2
[  1.373] recv (stream_id=4) date: Mon, 10 Apr 2017 06:30:29 GMT
[  1.373] recv (stream_id=4) content-type: application/javascript
[  1.373] recv (stream_id=4) last-modified: Thu, 14 May 2015 04:10:14 GMT
[  1.373] recv (stream_id=4) etag: "55542026-1388"
[  1.373] recv (stream_id=4) accept-ranges: bytes
[  1.374] recv (stream_id=4) x-http2-push: pushed
[  1.374] recv (stream_id=4) content-length: 5000
[  1.374] recv HEADERS frame <length=28, flags=0x04, stream_id=4>
          ; END_HEADERS
          (padlen=0)
          ; First push response header
[  1.374] recv (stream_id=13) :method: GET
[  1.374] recv (stream_id=13) :scheme: https
[  1.374] recv (stream_id=13) :authority: h2o.examp1e.net
[  1.374] recv (stream_id=13) :path: /search/oktavia-english-search.js
[  1.374] recv (stream_id=13) accept: */*
[  1.374] recv (stream_id=13) accept-encoding: gzip, deflate
[  1.374] recv (stream_id=13) user-agent: nghttp2/1.21.1
[  1.374] recv PUSH_PROMISE frame <length=35, flags=0x04, stream_id=13>
          ; END_HEADERS
          (padlen=0, promised_stream_id=6)
[  1.374] recv (stream_id=6) :status: 200
[  1.374] recv (stream_id=6) server: h2o/2.2.0-beta2
[  1.374] recv (stream_id=6) date: Mon, 10 Apr 2017 06:30:29 GMT
[  1.374] recv (stream_id=6) content-type: application/javascript
[  1.374] recv (stream_id=6) last-modified: Thu, 14 May 2015 04:10:14 GMT
[  1.374] recv (stream_id=6) etag: "55542026-34dd6"
[  1.374] recv (stream_id=6) accept-ranges: bytes
[  1.374] recv (stream_id=6) x-http2-push: pushed
[  1.374] recv (stream_id=6) content-length: 216534
[  1.374] recv HEADERS frame <length=31, flags=0x04, stream_id=6>
          ; END_HEADERS
          (padlen=0)
          ; First push response header
[  1.374] recv (stream_id=13) :method: GET
[  1.374] recv (stream_id=13) :scheme: https
[  1.374] recv (stream_id=13) :authority: h2o.examp1e.net
[  1.374] recv (stream_id=13) :path: /assets/style.css
[  1.374] recv (stream_id=13) accept: */*
[  1.374] recv (stream_id=13) accept-encoding: gzip, deflate
[  1.374] recv (stream_id=13) user-agent: nghttp2/1.21.1
[  1.374] recv PUSH_PROMISE frame <length=24, flags=0x04, stream_id=13>
          ; END_HEADERS
          (padlen=0, promised_stream_id=8)
[  1.374] recv (stream_id=8) :status: 200
[  1.374] recv (stream_id=8) server: h2o/2.2.0-beta2
[  1.374] recv (stream_id=8) date: Mon, 10 Apr 2017 06:30:29 GMT
[  1.374] recv (stream_id=8) content-type: text/css
[  1.374] recv (stream_id=8) last-modified: Tue, 20 Sep 2016 05:27:06 GMT
[  1.374] recv (stream_id=8) etag: "57e0c8aa-1586"
[  1.374] recv (stream_id=8) accept-ranges: bytes
[  1.374] recv (stream_id=8) x-http2-push: pushed
[  1.374] recv (stream_id=8) content-length: 5510
[  1.374] recv HEADERS frame <length=58, flags=0x04, stream_id=8>
          ; END_HEADERS
          (padlen=0)
          ; First push response header
[  1.374] recv (stream_id=13) :method: GET
[  1.374] recv (stream_id=13) :scheme: https
[  1.374] recv (stream_id=13) :authority: h2o.examp1e.net
[  1.374] recv (stream_id=13) :path: /assets/searchstyle.css
[  1.374] recv (stream_id=13) accept: */*
[  1.374] recv (stream_id=13) accept-encoding: gzip, deflate
[  1.374] recv (stream_id=13) user-agent: nghttp2/1.21.1
[  1.374] recv PUSH_PROMISE frame <length=28, flags=0x04, stream_id=13>
          ; END_HEADERS
          (padlen=0, promised_stream_id=10)
[  1.374] recv (stream_id=10) :status: 200
[  1.374] recv (stream_id=10) server: h2o/2.2.0-beta2
[  1.374] recv (stream_id=10) date: Mon, 10 Apr 2017 06:30:29 GMT
[  1.374] recv (stream_id=10) content-type: text/css
[  1.374] recv (stream_id=10) last-modified: Tue, 20 Sep 2016 05:27:06 GMT
[  1.374] recv (stream_id=10) etag: "57e0c8aa-8dd"
[  1.374] recv (stream_id=10) accept-ranges: bytes
[  1.374] recv (stream_id=10) x-http2-push: pushed
[  1.374] recv (stream_id=10) content-length: 2269
[  1.374] recv HEADERS frame <length=27, flags=0x04, stream_id=10>
          ; END_HEADERS
          (padlen=0)
          ; First push response header
[  1.374] recv (stream_id=13) :status: 200
[  1.374] recv (stream_id=13) server: h2o/2.2.0-beta2
[  1.374] recv (stream_id=13) date: Mon, 10 Apr 2017 06:30:29 GMT
[  1.374] recv (stream_id=13) link: </search/jquery-1.9.1.min.js>; rel=preload
[  1.374] recv (stream_id=13) link: </search/oktavia-jquery-ui.js>; rel=preload
[  1.374] recv (stream_id=13) link: </search/oktavia-english-search.js>; rel=preload
[  1.374] recv (stream_id=13) link: </assets/style.css>; rel=preload
[  1.374] recv (stream_id=13) link: </assets/searchstyle.css>; rel=preload
[  1.374] recv (stream_id=13) cache-control: no-cache
[  1.374] recv (stream_id=13) content-type: text/html
[  1.374] recv (stream_id=13) last-modified: Wed, 05 Apr 2017 06:55:14 GMT
[  1.374] recv (stream_id=13) etag: "58e494d2-1665"
[  1.374] recv (stream_id=13) accept-ranges: bytes
[  1.374] recv (stream_id=13) set-cookie: h2o_casper=AmgAAAAAAAAAAAAYxfEYAAABSA; Path=/; Expires=Tue, 01 Jan 2030 00:00:00 GMT; Secure
[  1.374] recv (stream_id=13) content-length: 5733
[  1.374] recv HEADERS frame <length=304, flags=0x04, stream_id=13>
          ; END_HEADERS
          (padlen=0)
          ; First response header
[  1.375] send SETTINGS frame <length=0, flags=0x01, stream_id=0>
          ; ACK
          (niv=0)
[  1.566] recv DATA frame <length=16137, flags=0x00, stream_id=2>
[  1.567] recv DATA frame <length=5000, flags=0x01, stream_id=4>
          ; END_STREAM
[  1.567] recv DATA frame <length=4915, flags=0x00, stream_id=6>
[  1.766] recv DATA frame <length=2829, flags=0x00, stream_id=8>
[  1.766] recv DATA frame <length=2269, flags=0x01, stream_id=10>
          ; END_STREAM
[  1.766] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=0>
          (window_size_increment=33120)
[  1.767] recv DATA frame <length=9065, flags=0x00, stream_id=2>
[  1.970] recv DATA frame <length=2829, flags=0x00, stream_id=6>
[  1.970] recv DATA frame <length=2681, flags=0x01, stream_id=8>
          ; END_STREAM
[  1.971] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=2>
          (window_size_increment=33855)
[  1.971] recv DATA frame <length=10072, flags=0x00, stream_id=2>
[  2.172] recv DATA frame <length=2829, flags=0x00, stream_id=6>
[  2.172] recv DATA frame <length=4248, flags=0x00, stream_id=2>
[  2.173] recv DATA frame <length=4248, flags=0x00, stream_id=6>
[  2.173] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=0>
          (window_size_increment=34002)
[  2.173] recv DATA frame <length=4248, flags=0x00, stream_id=2>
[  2.577] recv DATA frame <length=4248, flags=0x00, stream_id=6>
[  2.578] recv DATA frame <length=2829, flags=0x00, stream_id=2>
[  2.579] recv DATA frame <length=12762, flags=0x00, stream_id=6>
[  2.777] recv DATA frame <length=2829, flags=0x00, stream_id=2>
[  2.777] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=6>
          (window_size_increment=33241)
[  2.778] recv DATA frame <length=2829, flags=0x00, stream_id=6>
[  3.177] recv DATA frame <length=8505, flags=0x00, stream_id=2>
[  3.177] recv DATA frame <length=5667, flags=0x00, stream_id=6>
[  3.177] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=0>
          (window_size_increment=33993)
[  3.177] recv DATA frame <length=2829, flags=0x00, stream_id=2>
[  3.177] recv DATA frame <length=2829, flags=0x00, stream_id=6>
[  3.378] recv DATA frame <length=2829, flags=0x00, stream_id=2>
[  3.579] recv DATA frame <length=11343, flags=0x00, stream_id=6>
[  3.580] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=0>
          (window_size_increment=34002)
[  3.580] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=2>
          (window_size_increment=33984)
[  3.583] recv DATA frame <length=7086, flags=0x00, stream_id=2>
[  3.779] recv DATA frame <length=2829, flags=0x00, stream_id=6>
[  4.186] recv DATA frame <length=7086, flags=0x00, stream_id=2>
[  4.186] recv DATA frame <length=2829, flags=0x00, stream_id=6>
[  4.186] recv DATA frame <length=2829, flags=0x00, stream_id=2>
[  4.395] recv DATA frame <length=2829, flags=0x00, stream_id=6>
[  4.396] recv DATA frame <length=2829, flags=0x00, stream_id=2>
[  4.602] recv DATA frame <length=5667, flags=0x00, stream_id=6>
[  4.602] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=6>
          (window_size_increment=33993)
[  4.602] recv DATA frame <length=2829, flags=0x00, stream_id=2>
[  4.602] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=0>
          (window_size_increment=33975)
[  4.808] recv DATA frame <length=4248, flags=0x00, stream_id=6>
[  4.809] recv DATA frame <length=6379, flags=0x01, stream_id=2>
          ; END_STREAM
[  5.010] recv DATA frame <length=3536, flags=0x00, stream_id=6>
[  5.420] recv DATA frame <length=8505, flags=0x00, stream_id=6>
[  5.420] recv DATA frame <length=5667, flags=0x00, stream_id=6>
[  5.628] recv DATA frame <length=4248, flags=0x00, stream_id=6>
[  5.842] recv DATA frame <length=4248, flags=0x00, stream_id=6>
[  5.842] recv DATA frame <length=2829, flags=0x00, stream_id=6>
[  5.842] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=0>
          (window_size_increment=34002)
[  5.842] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=6>
          (window_size_increment=33281)
[  6.057] recv DATA frame <length=4248, flags=0x00, stream_id=6>
[  6.273] recv DATA frame <length=8505, flags=0x00, stream_id=6>
[  6.490] recv DATA frame <length=9924, flags=0x00, stream_id=6>
[  6.490] recv DATA frame <length=4248, flags=0x00, stream_id=6>
[  6.706] recv DATA frame <length=4248, flags=0x00, stream_id=6>
[  6.706] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=0>
          (window_size_increment=34002)
[  6.706] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=6>
          (window_size_increment=34002)
[  6.924] recv DATA frame <length=8505, flags=0x00, stream_id=6>
[  7.141] recv DATA frame <length=8505, flags=0x00, stream_id=6>
[  7.361] recv DATA frame <length=8505, flags=0x00, stream_id=6>
[  7.361] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=0>
          (window_size_increment=34020)
[  7.574] recv DATA frame <length=9924, flags=0x00, stream_id=6>
[  7.574] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=6>
          (window_size_increment=34029)
[  7.787] recv DATA frame <length=9924, flags=0x00, stream_id=6>
[  7.787] recv DATA frame <length=2829, flags=0x00, stream_id=6>
[  7.998] recv DATA frame <length=7086, flags=0x00, stream_id=6>
[  8.210] recv DATA frame <length=9924, flags=0x00, stream_id=6>
[  8.210] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=0>
          (window_size_increment=34011)
[  8.210] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=6>
          (window_size_increment=34011)
[  8.425] recv DATA frame <length=11343, flags=0x00, stream_id=6>
[  8.426] recv DATA frame <length=2829, flags=0x00, stream_id=6>
[  8.426] recv DATA frame <length=4053, flags=0x01, stream_id=6>
          ; END_STREAM
[  8.631] recv DATA frame <length=4443, flags=0x00, stream_id=13>
[  8.633] recv DATA frame <length=1290, flags=0x01, stream_id=13>
          ; END_STREAM
[  8.633] send GOAWAY frame <length=8, flags=0x00, stream_id=0>
          (last_stream_id=10, error_code=NO_ERROR(0x00), opaque_data(0)=[])

當然,我們也可以使用 grep 搜索出來 server push 的相關 stream:

nghttp -nv 'https://h2o.examp1e.net' | grep 'PUSH_PROMISE'
[  1.582] recv PUSH_PROMISE frame <length=59, flags=0x04, stream_id=13>
[  1.582] recv PUSH_PROMISE frame <length=33, flags=0x04, stream_id=13>
[  1.582] recv PUSH_PROMISE frame <length=35, flags=0x04, stream_id=13>
[  1.582] recv PUSH_PROMISE frame <length=24, flags=0x04, stream_id=13>
[  1.582] recv PUSH_PROMISE frame <length=28, flags=0x04, stream_id=13>

使用 NodeJS 搭建 HTTP/2 服務器

在大前端的時代背景下,客戶端開發不會點 JavaScript 都快混不下去了,筆者前段時間在我司前端輪崗了兩周,再加上之前也寫過 ReactNative,但還是感覺前端變化之快領人咋舌,革命尚未結束,同志仍需努力啊。

咱們直接上代碼:

var http2 = require('http2');// http2
var url=require('url'); // https://www.npmjs.com/package/url
var fs=require('fs'); // https://www.npmjs.com/package/fs
var mine=require('mine');
var path=require('path'); // 路徑
 
var server = http2.createServer({
  key: fs.readFileSync('./localhost.key'),
  cert: fs.readFileSync('./localhost.crt')
}, function(request, response) {

    // var pathname = url.parse(request.url).pathname;
    var realPath = './push.json' ;//path.join(pathname,"push.json");    //這里設置自己的文件路徑,這是該次response返回的內容;
 
    var pushArray = [];
    var ext = path.extname(realPath);
    ext = ext ? ext.slice(1) : 'unknown';
    var contentType = mine[ext] || "text/plain";
 
    if (fs.existsSync(realPath)) {
        
        console.log('success')
        response.writeHead(200, {
            'Content-Type': contentType
        });
        
        response.write(fs.readFileSync(realPath,'binary'));

        // 注意 push 路徑必須是絕對路徑,這是該次 server push 返回的內容
        var pushItem = response.push('/Users/f.li/Desktop/http2-nodeServer/newpush.json', {
                response: {
                  'content-type': contentType
                }    
        });
        pushItem.end(fs.readFileSync('/Users/f.li/Desktop/http2-nodeServer/newpush.json','binary'),()=>{
          console.log('newpush end')
        });

        response.end();
 
    } else {
      response.writeHead(404, {
          'Content-Type': 'text/plain'
      });

      response.write("This request URL " + realPath + " was not found on this server.");
      response.end();
    }
 
});
 
server.listen(3000, function() {
  console.log('listen on 3000');
});

這里需要注意幾點:

  • 創建http2的nodejs服務必須時基于https的,因為現在主流的瀏覽器都要支持SSL/TLS的http2,證書和私鑰可以自己通過OPENSSL生成。
  • node http2的相關api和正常的node httpserver相同,可以直接使用。

使用 nghttp 測試一下我們的代碼有沒有進行 server push:

~ nghttp -nv 'https://localhost:3000/'
[  0.007] Connected
The negotiated protocol: h2
[  0.029] recv SETTINGS frame <length=0, flags=0x00, stream_id=0>
          (niv=0)
[  0.029] 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.029] send SETTINGS frame <length=0, flags=0x01, stream_id=0>
          ; ACK
          (niv=0)
[  0.029] send PRIORITY frame <length=5, flags=0x00, stream_id=3>
          (dep_stream_id=0, weight=201, exclusive=0)
[  0.029] send PRIORITY frame <length=5, flags=0x00, stream_id=5>
          (dep_stream_id=0, weight=101, exclusive=0)
[  0.029] send PRIORITY frame <length=5, flags=0x00, stream_id=7>
          (dep_stream_id=0, weight=1, exclusive=0)
[  0.029] send PRIORITY frame <length=5, flags=0x00, stream_id=9>
          (dep_stream_id=7, weight=1, exclusive=0)
[  0.029] send PRIORITY frame <length=5, flags=0x00, stream_id=11>
          (dep_stream_id=3, weight=1, exclusive=0)
[  0.029] 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: localhost:3000
          accept: */*
          accept-encoding: gzip, deflate
          user-agent: nghttp2/1.21.1
[  0.043] recv SETTINGS frame <length=0, flags=0x01, stream_id=0>
          ; ACK
          (niv=0)
[  0.049] recv (stream_id=13) :status: 200
[  0.049] recv (stream_id=13) content-type: text/plain
[  0.049] recv (stream_id=13) date: Tue, 11 Apr 2017 08:34:46 GMT
[  0.049] recv HEADERS frame <length=34, flags=0x04, stream_id=13>
          ; END_HEADERS
          (padlen=0)
          ; First response header
[  0.049] recv DATA frame <length=35, flags=0x00, stream_id=13>
[  0.049] recv (stream_id=13) :method: GET
[  0.049] recv (stream_id=13) :scheme: https
[  0.050] recv (stream_id=13) :authority: localhost:3000
[  0.050] recv (stream_id=13) :path: /Users/f.li/Desktop/http2-nodeServer/newpush.json
[  0.050] recv PUSH_PROMISE frame <length=56, flags=0x04, stream_id=13>
          ; END_HEADERS
          (padlen=0, promised_stream_id=2)
[  0.050] recv DATA frame <length=0, flags=0x01, stream_id=13>
          ; END_STREAM
[  0.050] recv (stream_id=2) :status: 200
[  0.050] recv (stream_id=2) date: Tue, 11 Apr 2017 08:34:46 GMT
[  0.050] recv HEADERS frame <length=2, flags=0x04, stream_id=2>
          ; END_HEADERS
          (padlen=0)
          ; First push response header
[  0.050] recv DATA frame <length=21, flags=0x00, stream_id=2>
[  0.050] recv DATA frame <length=0, flags=0x01, stream_id=2>
          ; END_STREAM
[  0.050] send GOAWAY frame <length=8, flags=0x00, stream_id=0>
          (last_stream_id=2, error_code=NO_ERROR(0x00), opaque_data(0)=[])

看到了 PUSH_PROMISE 的幀,說明進行了 server push。

同樣也可以使用chrome查看 server push,如下圖所示:


chrome 查看 http2 server push

服務端介紹基本完畢。下面我們來介紹一些 iOS 客戶端對 Server Push 的使用。

iOS 使用 HTTP/2 Server Push

Apple 在這方面做的很好,基本實現了客戶端無感調用http/2 server push。但是筆者查看了些許資料,現在只有iOS 10 支持 http/2。

直接上代碼吧:

#import "ViewController.h"

@interface ViewController ()<NSURLSessionDelegate>

@property(nonatomic,strong)NSURLSession *session;

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
}
#pragma mark - Touch
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [self urlSession];
}
#pragma mark - 發送請求
- (void)urlSession
{
    NSURL *url = [NSURL URLWithString:@"https://localhost:3000"];
    
    //發送HTTPS請求是需要對網絡會話設置代理的
    _session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];
    
    NSURLSessionDataTask *dataTask = [_session dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        NSLog(@"%@",[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
        // 收到該次請求后,立即請求下次的內容
        [self urlSessionPush];
        
    }];
    
    [dataTask resume];
}

- (void)urlSessionPush
{
    NSURL *url = [NSURL URLWithString:@"https://localhost:3000/Users/f.li/Desktop/http2-nodeServer/newpush.json"];
    NSURLSessionDataTask *dataTask = [_session dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        NSLog(@"%@",[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
    }];
    [dataTask resume];
}

#pragma mark - URLSession Delegate
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler
{
    // 這里還要設置下 plist 中設置 ATS
    if (![challenge.protectionSpace.authenticationMethod isEqualToString:@"NSURLAuthenticationMethodServerTrust"])
    {
        return;
    }
    NSURLCredential *credential = [[NSURLCredential alloc] initWithTrust:challenge.protectionSpace.serverTrust];
    completionHandler(NSURLSessionAuthChallengeUseCredential,credential);
}


- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics
{
    NSArray *fetchTypes = @[ @"Unknown", @"Network Load", @"Server Push", @"Local Cache"];
    
    for(NSURLSessionTaskTransactionMetrics *transactionMetrics in [metrics transactionMetrics])
    {
        
        NSLog(@"protocol[%@] reuse[%d] fetch:%@ - %@", [transactionMetrics networkProtocolName], [transactionMetrics isReusedConnection], fetchTypes[[transactionMetrics resourceFetchType]], [[transactionMetrics request] URL]);
        
        if([transactionMetrics resourceFetchType] == NSURLSessionTaskMetricsResourceFetchTypeServerPush)
        {
            NSLog(@"Asset was server pushed");
        }
    }
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end

分別看下服務端和客戶端的Log:
客戶端:

Http2ServerPush[2525:274943] protocol[h2] reuse[0] fetch:Network Load - https://localhost:3000/
Http2ServerPush[2525:274943] {"message":" http2.0 server is ok"}
Http2ServerPush[2525:274943] protocol[h2] reuse[1] fetch:Server Push - https://localhost:3000/Users/f.li/Desktop/http2-nodeServer/newpush.json
Http2ServerPush[2525:274943] Asset was server pushed
Http2ServerPush[2525:274943] {"message":"newPush"}

服務端:

http2-nodeServer npm start

> http2-nodeServer@1.0.0 start /Users/f.li/Desktop/http2-nodeServer
> node index.js

listen on 3000
success
newpush end

看來確實是客戶端發出了兩次請求,但是服務端只響應了一次(該次響應+ server push)

本文相關Demo

參考文獻

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,517評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,087評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,521評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,493評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,207評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,603評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,624評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,813評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,364評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,110評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,305評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,874評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,532評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,953評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,209評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,033評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,268評論 2 375

推薦閱讀更多精彩內容