深刻理解 Web 協議 (三):HTTP 2

本篇將詳細介紹 http2 協議的方方面面,知識點以下:css

  • HTTP 2 鏈接的創建html

  • HTTP 2 中幀和流的關係前端

  • HTTP 2 中流量節省的奧祕:HPACK 算法git

  • HTTP 2 協議中 Server Push 的能力github

  • HTTP 2 爲何要實現流量控制?web

  • HTTP 2 協議遇到的問題

1、HTTP 2 鏈接的創建

和許多人的固有印象不一樣的是 HTTP 2協議自己並無規定必須創建在TLS/SSL之上,其實用普通的TCP鏈接也能夠完成HTTP 2鏈接的創建。只不過如今爲了安全市面上全部的瀏覽器都僅默認支持基於TLS/SSL的 HTTP 2協議。簡單來講咱們能夠把構建在TCP鏈接之上的 HTTP 2 協議稱之爲H2C,而構建在TLS/SSL協議之上的就能夠理解爲是H2了。算法

輸入命令:編程

tcpdump -i eth0 port 80 and host nghttp2.org -w h2c.pcap &

而後用curl訪問基於TCP鏈接,也就是port 80端口的 HTTP 2站點(這裏是沒辦法用瀏覽器訪問的,由於瀏覽器不容許)promise

curl http://nghttp2.org --http2 -v

其實看日誌也能夠大體瞭解一下這個鏈接創建的過程:瀏覽器

深刻理解 Web 協議 (三):HTTP 2

咱們將TCPDump出來的pcap文件拷貝到本地,而後用Wireshark打開之後還原一下整個HTTP 2鏈接創建的報文:

首先是 HTTP 1.1 升級到 HTTP 2 協議

深刻理解 Web 協議 (三):HTTP 2

而後客戶端還須要發送一個「魔法幀」:

深刻理解 Web 協議 (三):HTTP 2

最後還須要發送一個設置幀:

深刻理解 Web 協議 (三):HTTP 2

以後,咱們來看一下,基於TLS的 HTTP 2鏈接是如何創建的,考慮到加密等因素,咱們須要提早作一些準備工做。能夠在Chrome中下載這個插件。

深刻理解 Web 協議 (三):HTTP 2

而後打開任意一個網頁只要看到這個閃電的圖標爲藍色就表明這個站點支持HTTP 2;不然不支持。以下圖:

深刻理解 Web 協議 (三):HTTP 2

將Chrome瀏覽器的TLS/SSL之類的信息 輸出到一個日誌文件中,須要額外配置系統變量,如圖所示:

深刻理解 Web 協議 (三):HTTP 2

而後將咱們的Wireshark中SSL相關的設置也進行配置。

深刻理解 Web 協議 (三):HTTP 2

這樣瀏覽器在進行TLS協議交互的時候,相關的加密解密信息都會寫入到這個log文件中,咱們的Wireshark就會用這個log文件中的信息來解密出咱們的TLS報文。

有了上述的基礎,咱們就能夠着手分析基於TLS鏈接的HTTP 2協議了。好比咱們訪問tmall的站點 https://www.tmall.com/ 而後打開咱們的Wireshark。

深刻理解 Web 協議 (三):HTTP 2

看一下標註的地方能夠看出來,是TLS鏈接創建之後 而後繼續發送魔法幀和設置幀,才表明HTTP 2的鏈接真正創建完畢。咱們看一下TLS報文的client hello 這個信息:

深刻理解 Web 協議 (三):HTTP 2

其中這個alpn協議的信息 就表明客戶端能夠接受哪兩種協議。server hello 這個消息 就明確的告知 咱們要使用H2協議。

深刻理解 Web 協議 (三):HTTP 2

這也是HTTP 2相比spdy協議最重要的一個優勢:spdy協議強依賴TLS/SSL,服務器沒有任何選擇。而HTTP 2協議則會在客戶端發起請求的時候攜帶alpn這個擴展,也就是說客戶端發請求的時候會告訴服務端我支持哪些協議。從而可讓服務端來選擇,我是否須要走TLS/SSL。

2、HTTP 2 中幀和流的關係

深刻理解 Web 協議 (三):HTTP 2

簡單來講,HTTP 2就是在應用層上模擬了一下傳輸層TCP中「流」的概念,從而解決了HTTP 1.x協議中的隊頭擁塞的問題,在1.x協議中,HTTP 協議是一個個消息組成的,同一條TCP鏈接上,前面一個消息的響應沒有回來,後續的消息是不能夠發送的。在HTTP 2中,取消了這個限制,將所謂的「消息」定義成「流」,流跟流之間的順序能夠是錯亂的,可是流裏面的幀的順序是不能夠錯亂的。如圖:

深刻理解 Web 協議 (三):HTTP 2

也就是說在同一條TCP鏈接上,能夠同時存在多個stream流,這些流 由一個個frame幀組成,流跟流之間沒有順序關係,可是每個流內部的幀是有前後順序的。注意看這張圖中的 135 等數字其實就是stream id,WebSocket中雖然也有幀的概念,可是由於WebSocket中沒有stream id,因此Websocket是沒有多路複用的功能的。HTTP 2 由於有了stream id因此就有了多路複用的能力。能夠在一條TCP鏈接上存在n個流,就意味着服務端能夠同時併發處理n個請求而後同時將這些請求都響應到同一條TCP鏈接上。固然這種在同一條TCP鏈接上傳送n個stream的能力也是有限制的,在 HTTP 2 鏈接創建的時候,setting幀 中會包含這個設置信息。例以下圖 在訪問天貓的站點的時候,瀏覽器攜帶的setting幀的消息裏面就標識了 瀏覽器這個HTTP 2的客戶端能夠支持併發最大的流爲1000。

深刻理解 Web 協議 (三):HTTP 2

當天貓服務器返回這個setting幀的響應的時候,就告知了瀏覽器,我能支持的最大併發stream爲128。

深刻理解 Web 協議 (三):HTTP 2

同時 咱們也要知道,HTTP 2協議中 流id爲單數就表明是客戶端發起的流,偶數表明服務端主動發起的流(能夠理解爲服務端主動推送)。

3、 HTTP 2 中流量節省的奧祕:HPACK 算法

相比與HTTP 1.x協議,HTTP 2協議還在流量消耗上作了極大改進。主要分爲三塊:靜態字典,動態字典,和哈夫曼編碼. 能夠安裝以下工具探測一下 對流量節省的做用:

apt-get install nghttp2-client

而後能夠探測一下一些已經開啓 HTTP 2的站點,基本上節約的流量都是百分之25起,若是頻繁訪問的話 會更多:

深刻理解 Web 協議 (三):HTTP 2

對於流量消耗來講,其實HTTP 2相比HTTP 1.x協議最大的改進就是在HTTP 2中咱們能夠對HTTP 的頭部進行壓縮了,而在以往HTTP 1.x協議中,gzip等是沒法對header進行壓縮的,尤爲對於絕大多數的請求來講,其實header的佔比是最大的。

咱們首先來了解一下靜態字典,如圖所示:

深刻理解 Web 協議 (三):HTTP 2

這個其實不難理解,無非就是將咱們那些經常使用的HTTP 頭部,用固定的數字來表示,那固然能夠起到節約流量的做用.這裏要注意的是 有些value 狀況比較複雜的header,他們的value 是沒有作靜態字典的。好比cache-control這個緩存控制字段,這後面的值由於太多了就沒法用靜態字典來解決,而只能靠霍夫曼編碼。下圖能夠表示 HPACK這種壓縮算法 起到的節約流量的做用:

深刻理解 Web 協議 (三):HTTP 2

例如,咱們看下62這個 頭部,user-agent 代指瀏覽器,通常咱們請求的時候這個頭部信息都是不會變的,因此最終通過hpack算法優化之後 後續再傳輸的時候 就只須要傳輸62這個數字就能夠表明其含義了。

又例以下圖:

深刻理解 Web 協議 (三):HTTP 2

也是同樣的,多個請求連續發送的時候,多數狀況下變化的只有path,其他頭部信息是不變的,那麼基於此場景,最終傳輸的時候也就只有path這一個頭部信息了。

最後咱們來看看hpack算法中的核心:哈夫曼編碼。哈弗曼編碼核心思想就是出現頻率較高的用較短的編碼,出現頻率較低的用較長的編碼(HTTP 2協議的前身spdy協議採用的是動態的哈夫曼編碼,而HTTP 2協議則選擇了靜態的哈夫曼編碼)。

深刻理解 Web 協議 (三):HTTP 2

來看幾個例子:

深刻理解 Web 協議 (三):HTTP 2

例如這個header幀,注意看這個method:get的頭部信息。由於method:get 在靜態索引表中的索引值爲2.對於這種key和value都在索引表中的值,咱們用一個字節也就是8個bit來標識,其中第一個bit固定爲1,剩下7位就用來表示索引表中的值,這裏method:get 索引表的值爲2,因此這個值就是1000 0010,換算成16進制就是0x82.

深刻理解 Web 協議 (三):HTTP 2

再看一組,key在索引表中,value 不在索引表中的header例子。

深刻理解 Web 協議 (三):HTTP 2

對於key在索引表中,value 不在索引表中的狀況,固定是01開頭的字節,後面6個bit(111010 換算成十進制就是58)就是靜態索引的值, user-agent在索引中index的值是58 再加上01開頭的2個bit 換算成二進制就是01111010,16進制就7a了。而後接着看第二個字節,0xd4,0xd4換算成二進制就是 1 101 0100,其中第一個bit 表明後面採用的是哈夫曼編碼,後面的7個bit 這個key-value的value 須要幾個字節來表示,這裏是101 0100 換算成10進制就是84,也就是說這個user-agent後面的value須要84個字節來表示,咱們數一下圖中的字節數 16*5+第一排d4後面的4個字節,恰好等於84個字節。

最後再看一個key和value 都不在索引表中的例子。

深刻理解 Web 協議 (三):HTTP 2

4、HTTP 2 協議中 Server Push 的能力

前文咱們提到過,H2相比H1.x協議提高最大的就是H2能夠在單條TCP鏈接的基礎上 同時傳輸n個stream。從而避免H1.x協議中隊頭擁塞的問題。實際上在大部分前端的頁面中,咱們還可使用H2協議的Server Push能力 進一步提升頁面的加載速度。例如一般咱們用瀏覽器訪問一個Html頁面時,只有當html頁面返回到瀏覽器,瀏覽器內核解析到這個Html頁面中有CSS或者JS之類的資源時,瀏覽器纔會發送對應的CSS或者JS請求,當CSS和JS回來之後 瀏覽器纔會進一步渲染,這樣的流程一般會致使瀏覽器處於一段時間內的白屏從而下降用戶體驗。有了H2協議之後,當瀏覽器訪問一個Html頁面到服務器時,服務器就能夠主動推送相應的CSS和JS的內容到瀏覽器,這樣就能夠省略瀏覽器以後從新發送CSS和JS請求的步驟。

有些人對Server Push存在必定程度上的誤解,認爲這種技術可以讓服務器向瀏覽器發送「通知」,甚至將其與WebSocket進行比較。事實並不是如此,Server Push只是省去了瀏覽器發送請求的過程。只有當「若是不推送這個資源,瀏覽器就會請求這個資源」的時候,瀏覽器纔會使用推送過來的內容。不然若是瀏覽器自己就不會請求某個資源,那麼推送這個資源只會白白消耗帶寬。固然若是與服務器通訊的是客戶端而不是瀏覽器,那麼HTTP 2協議天然就能夠完成 push推送的功能了。因此都使用HTTP 2協議的狀況下,與服務器通訊的是客戶端仍是瀏覽器 在功能上仍是有必定區別的。

深刻理解 Web 協議 (三):HTTP 2

下面爲了演示這個過程,咱們寫一段代碼。考慮到瀏覽器訪問HTTP 2站點必需要創建在TLS鏈接之上,咱們首先要生成對應的證書和祕鑰。

深刻理解 Web 協議 (三):HTTP 2

而後開啓HTTP 2,在接收到Html請求的時候主動push Html中引用的CSS文件。

package main

import (
    "fmt"
    "net/http"

    "github.com/labstack/echo"
)

func main() {

    e := echo.New()
    e.Static("/", "html")
    //主要用來驗證是否成功開啓http2環境
    e.GET("/request", func(c echo.Context) error {
        req := c.Request()
        format := `
          <code>
            Protocol: %s<br>
            Host: %s<br>
            Remote Address: %s<br>
            Method: %s<br>
            Path: %s<br>
          </code>
        `
        return c.HTML(http.StatusOK, fmt.Sprintf(format, req.Proto, req.Host, req.RemoteAddr, req.Method, req.URL.Path))
    })

    //在收到html請求的時候 同時主動push html中引用的css文件,不須要等待瀏覽器發起請求
    e.GET("/h2.html", func(c echo.Context) (err error) {
        pusher, ok := c.Response().Writer.(http.Pusher)
        if ok {
            if err = pusher.Push("/app.css", nil); err != nil {
                println("error push")
                return
            }

        }

        return c.File("html/h2.html")
    })
    // 
    e.StartTLS(":1323", "cert.pem", "key.pem")
}

而後Chrome訪問這個網頁的時候,看下NetWork面板:

深刻理解 Web 協議 (三):HTTP 2

能夠看出來這個CSS文件 就是咱們主動push過來的。再看下Wireshark。

深刻理解 Web 協議 (三):HTTP 2

能夠看出來 stream id爲13的 是客戶端發起的請求,由於id是單數的,在這個stream中,還存在着push_promise幀,這個幀就是由服務器發送給瀏覽器的,看一下他的具體內容。

深刻理解 Web 協議 (三):HTTP 2

能夠看出來這個幀就是用來告訴瀏覽器,我主動push給你的是哪一個資源,這個資源的stream-id 是6.圖中咱們也看到了有一個stream-id 爲6的  data在傳輸了,這個就是服務器主動push出來的CSS文件。到這裏,一次完整的Server Push就交互完畢了。

但在實際線上應用Server Push的時候 挑戰遠遠比咱們這個demo中來的複雜。首先就是大部分cdn供應商(除非自建cdn)對Server Push的支持比較有限。咱們不可能讓每一次資源的請求都直接打到咱們的源服務器上,大部分靜態資源都是前置在CDN中。其次,對於靜態資源來講,咱們還要考慮緩存的影響,若是是瀏覽器本身發出去的靜態資源請求,瀏覽器是能夠根據緩存狀態來決定這個資源我是否真的須要去請求,而Server Push 是服務器主動發起的,服務器多數狀況下是不知道這個資源的緩存是否過時的。固然能夠在瀏覽器接收到push Promise幀之後,查詢自身的緩存狀態而後發起RST_STREAM幀,告知服務器這個資源我有緩存,不須要繼續發送了,可是你沒辦法保證這個RST_STREAM在到達服務器的時候,服務器主動push出去的data幀還沒發出去。因此仍是會存在必定的帶寬浪費的現象。整體來講,Server Push 仍是一個提升前端用戶體驗至關有效的手段,使用了Server Push之後 瀏覽器的性能指標 idle指標 通常能夠提升3-5倍(畢竟瀏覽器不用等待解析Html之後再去請求CSS和JS了)。

5、HTTP 2 爲何要實現流量控制?

不少人不理解,爲何TCP傳輸層已經實現了流量控制,咱們的應用層 HTTP 2 還要實現流量控制。下面咱們看一張圖。

深刻理解 Web 協議 (三):HTTP 2

在HTTP 2協議中,由於咱們支持多路複用,也就是說咱們能夠同時發送多個stream 在同一條TCP鏈接中,上圖中,每一種顏色就表明一個stream,能夠看到 咱們總共有4種stream,每個stream又有n個frame,這個就很危險了,假設在應用層中咱們使用了多路複用,就會出現n個frame同時不停的發送到目標服務器中,此時流量達到頂峯就會觸發TCP的擁塞控制,從而將後續的frame所有阻塞住,形成服務器響應過慢了。HTTP 1.x 中由於不支持多路複用天然就不存在這個問題。且咱們以前屢次提到過,一個請求從客戶端到達服務器端要通過不少的代理服務器,這些代理服務器內存大小以及網絡狀況均可能不同,因此在應用層上作一次流量控制儘可能避開觸發TCP的流控是十分有必要的。在HTTP 2協議中的流量控制策略,遵循如下幾個原則:

  1. 客戶端和服務端都有流量控制能力。

  2. 發送端和接收端能夠獨立設置流控能力。

  3. 只有data幀才須要流控,其餘header幀或者push promise幀等都不須要。

  4. 流控能力只針對TCP鏈接的兩端,中間即便有代理服務器,也不會透傳到源服務器上。

訪問知乎的站點看一下抓包。

深刻理解 Web 協議 (三):HTTP 2

這些標識window_update幀的 就是所謂的流控幀了。咱們隨意點開一個看一下,就能夠看到這個流量控制幀告訴咱們的幀大小。

深刻理解 Web 協議 (三):HTTP 2

聰明如你必定能想到,既然HTTP 2都能作到流控了,那必定也能夠來作優先級。比方說在HTTP 1.x協議中,咱們訪問一個Html頁面,裏面會有JS和CSS還有圖片等資源,咱們同時發送這些請求,可是這些請求並無優先級的概念,誰先出去誰先回來都是未知的(由於你也不知道這些CSS和JS請求是否是在同一條TCP鏈接上,既然是分散在不一樣的TCP中,那麼哪一個快哪一個慢是不肯定的),可是從用戶體驗的角度來講,確定CSS的優先級最高,而後是JS,最後纔是圖片,這樣就能夠大大縮小瀏覽器白屏的時間。在HTTP 2中 實現了這樣的能力。好比咱們訪問sina的站點,而後抓包就能夠看到:

能夠看下這個CSS 幀的的優先級:

深刻理解 Web 協議 (三):HTTP 2

JS的優先級

深刻理解 Web 協議 (三):HTTP 2

最後是gif圖片的優先級 ,能夠看出來這個優先級是最低的。

深刻理解 Web 協議 (三):HTTP 2

有了weight這個關鍵字來標識優先級,服務器就知道哪些請求須要優先被響應優先被髮送response,哪些請求能夠後一點被髮送。這樣瀏覽器在總體上提供給用戶的體驗就會變的更好。

6、HTTP 2 協議遇到的問題

基於TCP或者TCP+TLS的 HTTP 2協議 仍是遇到了不少問題,好比:握手時間過長問題,若是是基於TCP的HTTP 2協議,那麼至少要三次握手,若是是TCP+TLS的HTTP 2協議,除了TCP的握手還要經歷TLS的屢次握手(TLS1.3已經能夠作到只有1次握手)。每一次握手都須要發送一個報文而後接收到這個報文的ack才能夠進行下一次握手,在弱網環境下能夠想象的到這個鏈接創建的效率是極低的。此外,TCP協議天生的隊頭擁塞 問題也一直在困擾着HTTP 21.x協議和HTTP 2協議。咱們看一下谷歌spdy的宣傳圖,能夠更加精準的理解這個擁塞的本質:

深刻理解 Web 協議 (三):HTTP 2

圖一很好理解,咱們多路複用支持下同時發了3個stream,而後通過TCP/IP協議 發送到服務器端,而後TCP協議把這些數據包再傳給咱們的應用層,注意這裏有個條件是,發送包的順序要和接收包的順序一致。上圖中能夠看到那些方塊的圖的順序是一致的,可是若是碰到下圖中的狀況,好比說這些數據包剛好第一個紅色的數據包傳丟了,那麼後續的數據包即便已經到了服務器的機器裏,也沒法馬上將數據傳遞給咱們的應用層協議,由於TCP協議規定好了接收的順序要和發送的順序保持一致,既然紅色的數據包丟失了,那麼後續的數據包就只能阻塞在服務器裏,一直等到紅色的數據包通過從新發送之後成功到達服務器了,再將這些數據包傳遞給應用層協議。

TCP協議除了有上述的一些缺陷之外,還有一個問題就是TCP協議的實現者是在操做系統層面,咱們任何語言,包括 Java,C,C++,Go等等 對外暴露的所謂Socket編程接口 最終實現者其實都是操做系統本身。要讓操做系統本身升級TCP協議的實現是很是很是困難的,何況整個互聯網中那麼多設備想要總體實現TCP協議的升級是一件不現實的事情(IPV6協議升級的過慢也有這方面的緣由)。基於上述問題,谷歌就基於udp協議封裝了一層quic協議(其實不少基於udp協議的應用層協議,都是在應用層上部分實現了TCP協議的若干功能),來替代HTTP 21.x-HTTP 2中的TCP協議。

咱們打開Chrome中的quic協議開關:

深刻理解 Web 協議 (三):HTTP 2

而後訪問一下youtube(國內的b站其實也支持)。

深刻理解 Web 協議 (三):HTTP 2

能夠看出來已經支持quic協議了。爲何這個選項在Chrome瀏覽器中默認是關閉的,其實也很好理解,這個quic協議其實是谷歌本身搞出來的,尚未被正式歸入到HTTP 3協議中,一切都還在草案中。因此這個選項默認是關閉的。看下quic協議相比於原來的TCP協議主要作了哪些改進?其實就是將原來隊列傳輸報文改爲了無需隊列傳輸,那天然也就不存在隊頭擁塞的問題了。

深刻理解 Web 協議 (三):HTTP 2

此外在HTTP 3中還提供了 變動端口號或者ip地址也能夠複用以前鏈接的能力,我的理解這個協議支持的特性可能更可能是爲了物聯網考慮的。物聯網中不少設備的ip均可能是一直變化的。能複用以前的鏈接將會大大提升網絡傳輸的效率。這樣就能夠避免目前存在的斷網之後從新鏈接到網絡須要至少通過1-3個rtt才能夠繼續傳輸數據的弊端。

深刻理解 Web 協議 (三):HTTP 2

最後要提一下,在極端弱網環境中,HTTP 2 的表現有可能不如HTTP 1.x,由於HTTP 2下面只有一條TCP鏈接,弱網下,若是丟包率極高,那麼會不斷的觸發TCP層面的超時重傳,形成TCP報文的積壓,遲遲沒法將報文傳遞給上面的應用層,可是HTTP 1.x中,由於可使用多條TCP鏈接,因此在必定程度上,報文積壓的狀況不會像HTTP 2那麼嚴重,這也是我認爲的HTTP 2協議惟一不如HTTP 1.x的地方,固然這個鍋是TCP的,並非HTTP 2自己的。

更多閱讀:

做者:vivo 互聯網-WuYue

相關文章
相關標籤/搜索