HTTP/2.0 協議基本上已經開始大規模使用,本文主要介紹 HTTP/2.0 協議的一些特性,以及前端應該對應做哪些優化的調整。

HTTP/2.0 之前

最早期的 HTTP/1.0 時代,一個資源 == 一個 TCP 鏈接 == 一個 HTTP 鏈接,每個 HTTP 請求結束都會斷開 TCP 連接,新的 HTTP 請求會另外新建一個 TCP 連接,並且只有當一個完整的請求結束(TCP closed)才會開始下一個請求(阻塞)。

這種使用 TCP 的方式最嚴重的問題是阻塞。後來為了解決阻塞問題,請求允許並發,即同一個域名允許建立多個 TCP 鏈接,這樣算是解決了部分阻塞問題,但是還是有多次建立 TCP 連接的延遲(Hand shaking)以及 TCP 特有的慢啟動(Slow start)問題。

補充:TCP 慢啟動(Slow start)

TCP 連接會隨著時間自我調整,起初會限制連接的最大速度,如果數據傳輸成功,會隨著時間的推移提高傳輸速度

TCP 協議本身太複雜了,細節以後再看吧。簡單來說,TCP 協議主要解決的是兩個問題

  • 穩定傳輸:ACK 確認機制
  • 包亂序:Sequence Number

同時,TCP 還有兩套控制機制

  • Congestion Control (堵塞控制):避免高速發送端癱瘓網路
  • Flow Control(流量控制):避免高速發送端癱瘓低速接收端

這些機制使得 TCP 協議不僅僅知道自己信道的情況,還能知道整個網路的情況

慢啟動是堵塞控制的一個基本演算法,簡單說就是,先設置小一點的窗口,發送少一點的數據,然後接受 ACK,然後慢慢增加窗口大小,通過丟包率,Round Trip Time 等等來判斷網路環境,設定一個合適的窗口大小。

長鏈接

為了解決 TCP 連接利用率低的問題,所以又提出了長鏈接(1.0 開啟,1.1 默認開啟)。即一個請求完成後,不會立刻斷開連接,而是在一定的時間內保持連接,以便快速處理即將到來的 HTTP 請求,復用同一個 TCP 通道,直到客戶端心跳檢測失敗或伺服器連接超時。

  • server 通過 set HTTP Header Connection: keep-alive 來建立
  • client 通過 set HTTP Header Connection: close 來關閉

長鏈接解決了多次創建 TCP 連接的延遲,但是還是有線頭阻塞(Head of line blocking)的問題,即一個 TCP 連接一次只能發出一個請求,所以客戶端必須等待收到響應後才能發出另外一個請求,這樣耗時的請求如果在前面會 block 後面耗時的請求。

小插曲:管道 Pipelining

HTTP 管道曾被提出來用以解決線頭阻塞問題,即把多個 HTTP 請求通過一個 TCP 連接傳送,在發送過程中不需要等待伺服器對前一個請求的響應,但是客戶端還是要按照發送請求的順序接收響應。這種方式並沒有根本解決線頭阻塞問題,因為響應按順序接收還是會有阻塞,所以這個功能都被瀏覽器默認關閉(或者壓根沒有)

HTTP/2.0 通過分幀將數據通過幀的方式傳送,徹底解決了這個問題,並且做了一些別的優化

HTTP/2.0

協議抽象描述(下面這段話很牛逼,需要仔細研讀)

在客戶端與伺服器之間僅建立一個 TCP 連接,而且該連接在交互持續期間一直處於打開狀態。在此連接上,消息是通過邏輯進行傳遞的。一條消息包含一個完整的幀序列。在經過整理後,這些幀表示一個響應或請求。

這裡有一些概念很重要,下面的內容都圍繞這些概念展開

  • Connection (連接):僅與一個對等節點建立一個
  • Stream(流):邏輯意義上的流,物理實體為連接,一個物理連接擁有多個邏輯流
  • 消息 Message(請求/響應):是一組幀,通過邏輯流傳輸,重建這些幀會得到一個完整的請求或者響應
  • 幀(Frame):通信的基本單位

幀的結構

  • LENGTH :幀的大小,最多可為 2 24 bit (16MB)
  • TYPE :標識幀的用途
  • HEADERS:只有 header 信息
  • DATA:只有 messages payload
  • PRIORITY:流的優先順序信息
  • RST_STREAM:報錯,reject PUSH_PROMISE,關閉連接
  • SETTINGS:連接設置
  • PUSH_PROMISE: 通知客戶端有伺服器推送的意圖(intent)
  • PING:心跳和 round-trip time
  • GOAWAY:對當前連接停止提供流
  • WINDOW_UPDATE:flow control of streams
  • CONTINUATION:繼續一系列的 HEADER fragment
  • R:reserved 保留位
  • FLAG:Boolean 值( 0/1)
  • DATA frame 有兩個 flag:END_STREAM / PADDED
  • HEADER frame 有四個 flag:END_STREAM / PADDED / END_HEADERS / PRIORITY
  • PUSH_PROMISE 有兩個 flag:END_HEADERS / PADDED
  • STREAM IDENTIFIER:用於 track the frame membership of a logical stream

其中 FLAG :

  • END_STREAM:end of the data stream
  • PADDED:存在填充數據
  • END_HEADERS:end of the headers
  • PRIORITY:優先順序被設定

二進位分幀(Binary framing)

分幀層處理的栗子

1. 文本請求映射到幀

  • END_STREAM 為 true (+),表示為請求的最後一幀(沒有 DATA 幀)
  • END_HEADERS 為 true,表示為流中最後一個包含 HEADER 信息的幀

2. 文本響應映射到幀

  • END_STREAM in HEADERS 為 false (-) 表示不是流的最後一幀
  • END_HEADER in HEADERS 為 true (+) 表示最後一個 HEADER 幀
  • END_STREAM in DATA 為 true (+) 表示當前流最後一幀

1. 二進位協議(Binary Protocol)

HTTP/2 保留了原始 HTTP 協議的語義,但更改了在系統之間傳輸數據的方式

HTTP/2.0 文本格式跟二進位格式的主要差別在於解析

  • 比如 TCP 就是二進位協議,每一位表示什麼都是固定的,解析起來只用判斷 0/1 就好,效率更高
  • HTTP/1.0 就是文本協議,因為都是字元串,解析起來要麼是正則,要麼是狀態機,效率更低

HTTP/2.0 允許保留原來的文本格式,但會經過一個"二進位分幀"的過程,將文本徹底轉化為二進位傳輸。

2. 多路復用(Multplexing)

舊的 HTTP 協議有個問題叫線頭阻塞(Head of line blocking),之前的解決方案是同時開啟多個 TCP 鏈接(Chrome 有6個),但是這樣多個 TCP 連接就很耗費網路資源了。實際上"單線程"就夠了,方式就是將消息分成更小的單位(幀),這樣就實現了完全雙向?的請求和響應消息復用。

比如下圖有三個邏輯流,一個請求(深藍),兩個響應(淺藍,綠),每一塊代表一個幀

將幀分解成 HEADER 和 DATA 幀

優點:

  • Request 和 Response 都在一個 socket
  • 所有的響應和請求都無法相互阻塞
  • 減少了建立連接帶來的延遲
  • 不再需要像 HTTP/1.1 那樣將多個請求合成一個

每個伺服器(域)只是用一個鏈接,而不是每個文件一個鏈接

3. 伺服器推送(Server push)

允許伺服器預測客戶端的需要,在請求處理完成之前就可以先發一個 PUSH_PROMISE frame,然後在 push 資源。為防止發送不必要的資源,伺服器會給每一個要推送的資源發送一個 PUSH_PROMISE frame,如果資源已經有緩存,瀏覽器可以 respond 一個 RST_STREAM frame,來拒絕 PUSH

(伺服器推送這個選項怎麼說呢,理論上有用,但是實際上還需要 tune,有待觀察吧)

4. 流控制(Flow control)

防止 receiver overwhelmed by the sender,允許 receiver 停止/減少發送的數據。

比如視屏流媒體服務,用戶點擊暫停,client will informs the server to stop sending video data。

連接一旦 open,server 和 client 便會交換 SETTINGS frame,從而構建 flow-control window 的大小。默認為 65KB,但是可以通過 WINDOW_UPDATE frame 來改變。

5. 頭部壓縮(Header compression)

頭部壓縮的原理是 --- 緩存,與其叫頭部壓縮還不如叫頭部緩存。使用 HPACK 協議,要求客戶端和伺服器各自維護一個 HEADER 欄位的列表,在多次發送的時候,只發送差異的部分,其餘的從緩存表裡面取。

// 用 JavaScript 描述的話大概是下面的意思
const cachedHeaders = {
method: GET,
host: example.com
}
const newHeaders = {
method: POST
}
const receivedHeaders = {
...cachedHeaders
...newHeaders
}

舉個栗子,第一次請求後,第二次請求只發送與之前請求頭不同的部分

6. 優先順序(Priority)

Messgae 通過流傳輸,每個流都會被指定一個優先順序,優先順序決定處理的順序分配的資源,優先順序的大小會在 HEADER frame 或者是 PRIORITY frame,為 0 - 256 的數字。

優先順序還可以為樹形結構,來標記依賴關係

A 先發送,B / C 同時發送,分別拿到 40% / 60% 的資源,D / E 則拿到 C 的各一半的資源。

優先順序僅僅是一個參考(only a suggestion),讓客戶端告訴服務如何處理請求是不對的,伺服器應該根據自己的能力(capabilities)來決定如何處理。

使用 HTTP/2.0 時的優化

  1. 減少分片(Sharding)的使用

sharding 指的是將服務分散到多個主機上,因為 2.0 以前是通過多個 TCP 連接來達到並發的目的,而瀏覽器對每個域名可以建立的 TCP 連接是有限制的,所以以前的優化手段是將多個資源分散到多個伺服器。

但是 2.0 協議使用多個 TCP 連接反而會造成性能的下降,因為建立連接很耗費網路資源。同理原來的一些優化方式也都沒什麼用了比如

  • Spiriting(雪碧圖)一種將多個圖合成一個圖從而減少請求的方式
  • Inline(內聯)將圖片用 data url 代替,從而減少請求
  • Concatenation(拼接)將多個 JS 文件打包成一個

這些原來的優化方式反而會損害性能,因為各種合成和拼接都會導致緩存沒那麼容易。

Ref

  • mnot.net/talks/h2fe/#
  • ye11ow.gitbooks.io/http
  • zhihu.com/question/3407
  • w3ctech.com/topic/1563#
  • ruanyifeng.com/blog/201
  • juejin.im/post/5b890903
  • ibm.com/developerworks/
  • developer.ibm.com/artic

推薦閱讀:

相关文章