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 連接會隨著時間自我調整,起初會限制連接的最大速度,如果數據傳輸成功,會隨著時間的推移提高傳輸速度
TCP 協議本身太複雜了,細節以後再看吧。簡單來說,TCP 協議主要解決的是兩個問題
同時,TCP 還有兩套控制機制
這些機制使得 TCP 協議不僅僅知道自己信道的情況,還能知道整個網路的情況
慢啟動是堵塞控制的一個基本演算法,簡單說就是,先設置小一點的窗口,發送少一點的數據,然後接受 ACK,然後慢慢增加窗口大小,通過丟包率,Round Trip Time 等等來判斷網路環境,設定一個合適的窗口大小。
為了解決 TCP 連接利用率低的問題,所以又提出了長鏈接(1.0 開啟,1.1 默認開啟)。即一個請求完成後,不會立刻斷開連接,而是在一定的時間內保持連接,以便快速處理即將到來的 HTTP 請求,復用同一個 TCP 通道,直到客戶端心跳檢測失敗或伺服器連接超時。
Connection: keep-alive
Connection: close
長鏈接解決了多次創建 TCP 連接的延遲,但是還是有線頭阻塞(Head of line blocking)的問題,即一個 TCP 連接一次只能發出一個請求,所以客戶端必須等待收到響應後才能發出另外一個請求,這樣耗時的請求如果在前面會 block 後面耗時的請求。
HTTP 管道曾被提出來用以解決線頭阻塞問題,即把多個 HTTP 請求通過一個 TCP 連接傳送,在發送過程中不需要等待伺服器對前一個請求的響應,但是客戶端還是要按照發送請求的順序接收響應。這種方式並沒有根本解決線頭阻塞問題,因為響應按順序接收還是會有阻塞,所以這個功能都被瀏覽器默認關閉(或者壓根沒有)
HTTP/2.0 通過分幀將數據通過幀的方式傳送,徹底解決了這個問題,並且做了一些別的優化
協議抽象描述(下面這段話很牛逼,需要仔細研讀)
在客戶端與伺服器之間僅建立一個 TCP 連接,而且該連接在交互持續期間一直處於打開狀態。在此連接上,消息是通過邏輯流進行傳遞的。一條消息包含一個完整的幀序列。在經過整理後,這些幀表示一個響應或請求。
這裡有一些概念很重要,下面的內容都圍繞這些概念展開
其中 FLAG :
分幀層處理的栗子
HTTP/2 保留了原始 HTTP 協議的語義,但更改了在系統之間傳輸數據的方式
HTTP/2.0 文本格式跟二進位格式的主要差別在於解析
HTTP/2.0 允許保留原來的文本格式,但會經過一個"二進位分幀"的過程,將文本徹底轉化為二進位傳輸。
舊的 HTTP 協議有個問題叫線頭阻塞(Head of line blocking),之前的解決方案是同時開啟多個 TCP 鏈接(Chrome 有6個),但是這樣多個 TCP 連接就很耗費網路資源了。實際上"單線程"就夠了,方式就是將消息分成更小的單位(幀),這樣就實現了完全雙向?的請求和響應消息復用。
比如下圖有三個邏輯流,一個請求(深藍),兩個響應(淺藍,綠),每一塊代表一個幀
將幀分解成 HEADER 和 DATA 幀
優點:
每個伺服器(域)只是用一個鏈接,而不是每個文件一個鏈接
允許伺服器預測客戶端的需要,在請求處理完成之前就可以先發一個 PUSH_PROMISE frame,然後在 push 資源。為防止發送不必要的資源,伺服器會給每一個要推送的資源發送一個 PUSH_PROMISE frame,如果資源已經有緩存,瀏覽器可以 respond 一個 RST_STREAM frame,來拒絕 PUSH
(伺服器推送這個選項怎麼說呢,理論上有用,但是實際上還需要 tune,有待觀察吧)
防止 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 來改變。
頭部壓縮的原理是 --- 緩存,與其叫頭部壓縮還不如叫頭部緩存。使用 HPACK 協議,要求客戶端和伺服器各自維護一個 HEADER 欄位的列表,在多次發送的時候,只發送差異的部分,其餘的從緩存表裡面取。
// 用 JavaScript 描述的話大概是下面的意思 const cachedHeaders = { method: GET, host: example.com } const newHeaders = { method: POST } const receivedHeaders = { ...cachedHeaders ...newHeaders }
舉個栗子,第一次請求後,第二次請求只發送與之前請求頭不同的部分
Messgae 通過流傳輸,每個流都會被指定一個優先順序,優先順序決定處理的順序和分配的資源,優先順序的大小會在 HEADER frame 或者是 PRIORITY frame,為 0 - 256 的數字。
優先順序還可以為樹形結構,來標記依賴關係
A 先發送,B / C 同時發送,分別拿到 40% / 60% 的資源,D / E 則拿到 C 的各一半的資源。
優先順序僅僅是一個參考(only a suggestion),讓客戶端告訴服務如何處理請求是不對的,伺服器應該根據自己的能力(capabilities)來決定如何處理。
sharding 指的是將服務分散到多個主機上,因為 2.0 以前是通過多個 TCP 連接來達到並發的目的,而瀏覽器對每個域名可以建立的 TCP 連接是有限制的,所以以前的優化手段是將多個資源分散到多個伺服器。
但是 2.0 協議使用多個 TCP 連接反而會造成性能的下降,因為建立連接很耗費網路資源。同理原來的一些優化方式也都沒什麼用了比如
這些原來的優化方式反而會損害性能,因為各種合成和拼接都會導致緩存沒那麼容易。
推薦閱讀: