HTTP2 的一些理解與練習

weixin_34019929發表於2018-04-24

HTTP2 解決了部分浪費問題

本文只做科普,真正的學習請參考標準 RFC 7520 HTTP/2

在我剛開始工作時,經常使用 SOAP 呼叫 WebService,理論上 SOAP 可以使用任何協議作為載體:HTTP、FTP、SMTP、甚至你自己寫的某種協議,但是大家都是使用 HTTP,這便是我第一個問題:why?答案當然可以寫出很多,但是有一個詞啟發了我,那就是穿牆穿牆保證你可以很方便的通過防火牆去呼叫遠端的服務,那麼為什麼防火牆支援 80 或者 443 埠呢(in default most times)?因為 HTTP 協議很流行。那麼為什麼 HTTP 協議很流行呢?因為 web,沒有 web 你現在也沒辦法在簡書上看文章了。所以 HTTP 是最重要的應用層協議。但是 HTTP 是非常不美好的一個實現,最讓人詬病的是資源的浪費。

  • 我們知道這是一個先進先出的協議 FIFO,也就是說在一個 connection 中,第一個 request 發出後你必須要等待 response,直到收到 response 再進行下一次的請求,所以現代瀏覽器會同時啟動多個 connection 進行資源下載(RFC-2616-8.1.4 Practical Considerations) 來提高效能。但是,在底層 socket 中,我們知道這是一個 duplex 的連線,那為什麼我們還要等前一個請求完成呢?
  • 當你在淘寶進行一次搜尋時,如果 dump 出一個完整的 HTTP 請求,可以看到除了內容以外還有大量的 header 進行傳輸,特別是一些用於記錄行為的 cookie。我們知道 HTTP 一個無狀態的協議(這是一個優點大於缺點的選擇),所以我們不得不帶上這些 header 以及其中的 cookie,而 body 則是可以選擇壓縮來節約頻寬的。
  • HTTP 是底層是建立在 TCP 之上的,我們知道如果傳輸資料的成本小與建立連線的成本(特別是 TLS)那是及不划算的,也無法利用到現在網路終端的高頻寬。之前我們的解決方案多是降低延遲,而不是增加頻寬使用效率,因為 HTTP 無法做到在一個連線上增加傳輸,無法多路複用(Multiplexing)。
  • 當你開啟某大型網站時,HTML 下載完成後(現代瀏覽器不是這樣,但是效率也不會太高),你才能夠獲取內嵌的 js、css、圖片等等這些資源。但是幾乎每個網站的每個頁面都有共享的資源(例如 header、通用樣式等),或者我們希望在連線建立時就能獲取這些通用的資源。
  • 對於 Mobile HTTP 是天生不友好的,WebService 開發相對於 RPC 是簡單的,但是對於效能有一定要求的 APP,使用 WebService 則有很大的風險,連線過多、效率很低。所以也有部分同學在自己的 APP 中使用自訂的協議(各種 PRC),而這些協議難以開發,難以除錯。
  • Server 不能發起主動通告,我們有時候希望伺服器在做完某些事情後通知我們,但是 HTTP 是不支援的,所以我們有了很多折中的方案:while poll、websocket 等,但是我們知道,這是效率很低的做法。

HTTP2 怎麼做到的?

HTTP2 最大的優勢就是提升了 web 效能,並且相容了 HTTP1.1 的語義,實現了向後相容。Akamai 的朋友曾經告訴我,Akamai 在 CDN 中已經預設開啟了 HTTP2,大約效能提升在 10%~20%(這個數字是非常粗略的並且沒有足夠的上下文,很多情況需要具體分析,所以才會有最後的例子),而這對於很多開發人員來說,只是一個開關的事情。

所以 HTTP2 需要重新發明與 TCP 的整合方式,而不是簡單粗暴的將 ASCII 格式的訊息丟下去,同時還必須要相容 HTTP1.1,所以應對這種問題,經典的解決方案就是:加一層。

Any problem in computer science can be solved by another layer of indirection.

Binary Framing —— 二進位制封幀

HTTP2 引入了 frame 作為資料層的封裝表示,歸根結底HTTP2 的目標是提高資源利用——儘量的在一個 connection 中做更多的事情——也就意味著原始的 raw ASCII 傳遞是需要被替代的,而這個替代就是 frame 與 stream。Binary Frame 是 HTTP2 資料交換的單位或是最小實體,我們使用二進位制的格式去構造這些 frame。操作 frame 要簡單于一個巨大的二進位制流,這意味著我們可以做很多高階的操作:併發、流量控制、錯誤處理與優先排序,同時還有更好的擴充套件性。

Multiplexing —— 多路複用

HTTP2 最重要的改進就是多路複用,為了實現多路複用,引入了 stream 這個抽象的概念。如果 frame 是車廂的話,stream 就是火車,排程這些 stream 來達到多路複用(老實說,要按照標準實現 stream 來達到多路複用真是一個很有挑戰的事情,特別是我看完了 stream state 後……或許應該用 Go 寫一個作為練習)。按照 RFC,stream 應該有一下的特性:

  • Stream 是獨立的、雙向的用於 frame 傳輸的序列
  • 一個 HTTP2 connection 包含大量的、併發的 stream
  • 通訊雙方可以從 stream 中獲取 frame
  • Stream 在被建立後可以單獨的使用,也可以被共享
  • Stream 可以被通訊雙方關閉
  • Stream 中的 frame 的順序是非常重要的,應答 frame 的順序與收到時相同
  • Stream 使用 Integer 值標識,並由端點指定

所以我們可以訪問一個網站,就使用一個 connection,也不會擔心 FIFO 造成效能上的損失(head-of-line blocking problem),如果一個 response 比較慢,其他的 frames 會帶來另一些快速的 response,而系統不會被 block 住。

Header Compression —— 頭部壓縮

很多人喜歡用 TCP Slow Start 來說明 Header Compression 的作用,我感覺貌似搞錯了點。雖然是有一部分的相關性,但是重點是為了節約傳輸的 bits。即時輕度的頭部壓縮,只要能在一次 round trip 完成任務就是非常高效的了。很多網站都有很多圖片、樣式表、JS等等資源,如果每個資源有 800 bytes 的 headers(cookies、refer 等等),我們可以計算下只為這些 header 傳輸,我們可以節約多少資源。

Server Push —— 服務端推送

當瀏覽器請求一個頁面時,伺服器會在響應中傳送 HTML 返回給瀏覽器,然後需要等待瀏覽器解析 HTML 並在開始傳送JavaScript,影像和 CSS 之前發出對所有嵌入資源的請求。Server Push 可能允許伺服器通過將它認為客戶端需要的響應 push 到其快取中來避免這種延遲,簡直太智慧、太美好了!所以在 2018 年我們終於有點未來世界的影子了?

然而並不是,仔細想想 Server Push 的描述,有幾個詞是很難做到的:“需要的”、“快取”。有快取,就需要有過期策略,就需要有相應的重新整理策略,沒有哪個前端工程師喜歡快取,當然大多數人必須承認快取是能提高部分使用者體驗的。

但是我一直比較關心的是,Server Push 可以替代一些只為了推送而開的 WebSocket 與 loop AJAX 嗎?當然如果是富互動應用,我不覺得 HTTP2 的目標是取代 WebSocket。

練習

粗略的讀完了 HTTP2 的 RFC,也做了一些簡單的練習,主要使用者觀察多路複用的具體表現與 Server Push,思路是使用 docker-compose 在本地開啟一些 NGINX 容器。使用 docker 去驗證、學習是一個很好的載體,某種情況下,docker 是一個秒級的、輕量的、全功能的虛擬機器。而 compose 可以避免你記錄一堆命令或者配置。

Example: Http2 Practice

docker-compose.yml 檔案中我們定義了三個容器用來驗證,web-server-http1 用於對照。

version: '2'

services:
  base:
    image: nginx:1.13.12
    volumes:
      - ./site:/usr/share/nginx/html
      - ./cert:/etc/nginx/ssl

  web-server-http1:
    extends:
      service: base
    volumes:
      - ./config/base-ssl.conf:/etc/nginx/nginx.conf
    ports:
      - "443:443"

  web-server-http2:
    extends:
      service: base
    volumes:
      - ./config/base-ssl-http2.conf:/etc/nginx/nginx.conf
    ports:
      - "444:443"

  web-server-http2-server-push:
    extends:
      service: base
    volumes:
      - ./config/base-ssl-http2-server-push.conf:/etc/nginx/nginx.conf
    ports:
      - "445:443"

以下是多路複用的觀察結果:


3819274-8991db1f142bb3a7.png
效果拔群

關於 NGINX 的 HTTP2 Features 可以參考:

相關文章