本篇將詳細介紹 http2 協(xié)議的方方面面异袄,知識點如下:
HTTP 2 連接的建立
HTTP 2 中幀和流的關系
HTTP 2 中流量節(jié)省的奧秘:HPACK 算法
HTTP 2 協(xié)議中 Server Push 的能力
HTTP 2 為什么要實現(xiàn)流量控制?
HTTP 2 協(xié)議遇到的問題
一、HTTP 2 連接的建立
和許多人的固有印象不同的是 HTTP 2協(xié)議本身并沒有規(guī)定必須建立在TLS/SSL之上倦踢,其實用普通的TCP連接也可以完成HTTP 2連接的建立炕倘。只不過現(xiàn)在為了安全市面上所有的瀏覽器都僅默認支持基于TLS/SSL的 HTTP 2協(xié)議求冷。簡單來說我們可以把構(gòu)建在TCP連接之上的 HTTP 2 協(xié)議稱之為H2C,而構(gòu)建在TLS/SSL協(xié)議之上的就可以理解為是H2了窍霞。
輸入命令:
tcpdump -i eth0 port 80 and host nghttp2.org -w h2c.pcap &
然后用curl訪問基于TCP連接匠题,也就是port 80端口的 HTTP 2站點(這里是沒辦法用瀏覽器訪問的,因為瀏覽器不允許)
curl http://nghttp2.org --http2 -v
其實看日志也可以大致了解一下這個連接建立的過程:
[圖片上傳失敗...(image-d2b038-1614065039358)]
我們將TCPDump出來的pcap文件拷貝到本地但金,然后用Wireshark打開以后還原一下整個HTTP 2連接建立的報文:
首先是 HTTP 1.1 升級到 HTTP 2 協(xié)議
然后客戶端還需要發(fā)送一個“魔法幀”:
最后還需要發(fā)送一個設置幀:
之后韭山,我們來看一下,基于TLS的 HTTP 2連接是如何建立的冷溃,考慮到加密等因素钱磅,我們需要提前做一些準備工作∷普恚可以在Chrome中下載這個插件盖淡。
然后打開任意一個網(wǎng)頁只要看到這個閃電的圖標為藍色就代表這個站點支持HTTP 2;否則不支持凿歼。如下圖:
將Chrome瀏覽器的TLS/SSL之類的信息 輸出到一個日志文件中褪迟,需要額外配置系統(tǒng)變量,如圖所示:
然后將我們的Wireshark中SSL相關的設置也進行配置答憔。
這樣瀏覽器在進行TLS協(xié)議交互的時候味赃,相關的加密解密信息都會寫入到這個log文件中,我們的Wireshark就會用這個log文件中的信息來解密出我們的TLS報文攀唯。
有了上述的基礎洁桌,我們就可以著手分析基于TLS連接的HTTP 2協(xié)議了。比如我們訪問tmall的站點 https://www.tmall.com/ 然后打開我們的Wireshark侯嘀。
看一下標注的地方可以看出來另凌,是TLS連接建立以后 然后繼續(xù)發(fā)送魔法幀和設置幀,才代表HTTP 2的連接真正建立完畢戒幔。我們看一下TLS報文的client hello 這個信息:
其中這個alpn協(xié)議的信息 就代表客戶端可以接受哪兩種協(xié)議吠谢。server hello 這個消息 就明確的告知 我們要使用H2協(xié)議。
這也是HTTP 2相比spdy協(xié)議最重要的一個優(yōu)點:spdy協(xié)議強依賴TLS/SSL诗茎,服務器沒有任何選擇工坊。而HTTP 2協(xié)議則會在客戶端發(fā)起請求的時候攜帶alpn這個擴展,也就是說客戶端發(fā)請求的時候會告訴服務端我支持哪些協(xié)議敢订。從而可以讓服務端來選擇王污,我是否需要走TLS/SSL。
二楚午、HTTP 2 中幀和流的關系
簡單來說昭齐,HTTP 2就是在應用層上模擬了一下傳輸層TCP中“流”的概念,從而解決了HTTP 1.x協(xié)議中的隊頭擁塞的問題矾柜,在1.x協(xié)議中阱驾,HTTP 協(xié)議是一個個消息組成的就谜,同一條TCP連接上,前面一個消息的響應沒有回來里覆,后續(xù)的消息是不可以發(fā)送的丧荐。在HTTP 2中,取消了這個限制喧枷,將所謂的“消息”定義成“流”虹统,流跟流之間的順序可以是錯亂的,但是流里面的幀的順序是不可以錯亂的割去。如圖:
也就是說在同一條TCP連接上窟却,可以同時存在多個stream流,這些流 由一個個frame幀組成呻逆,流跟流之間沒有順序關系,但是每一個流內(nèi)部的幀是有先后順序的菩帝。注意看這張圖中的 135 等數(shù)字其實就是stream id咖城,WebSocket中雖然也有幀的概念,但是因為WebSocket中沒有stream id呼奢,所以Websocket是沒有多路復用的功能的宜雀。HTTP 2 因為有了stream id所以就有了多路復用的能力∥沾。可以在一條TCP連接上存在n個流辐董,就意味著服務端可以同時并發(fā)處理n個請求然后同時將這些請求都響應到同一條TCP連接上。當然這種在同一條TCP連接上傳送n個stream的能力也是有限制的禀综,在 HTTP 2 連接建立的時候简烘,setting幀 中會包含這個設置信息。例如下圖 在訪問天貓的站點的時候定枷,瀏覽器攜帶的setting幀的消息里面就標識了 瀏覽器這個HTTP 2的客戶端可以支持并發(fā)最大的流為1000孤澎。
當天貓服務器返回這個setting幀的響應的時候,就告知了瀏覽器欠窒,我能支持的最大并發(fā)stream為128覆旭。
同時 我們也要知道,HTTP 2協(xié)議中 流id為單數(shù)就代表是客戶端發(fā)起的流岖妄,偶數(shù)代表服務端主動發(fā)起的流(可以理解為服務端主動推送)型将。
三、 HTTP 2 中流量節(jié)省的奧秘:HPACK 算法
相比與HTTP 1.x協(xié)議荐虐,HTTP 2協(xié)議還在流量消耗上做了極大改進七兜。主要分為三塊:靜態(tài)字典,動態(tài)字典缚俏,和哈夫曼編碼. 可以安裝如下工具探測一下 對流量節(jié)省的作用:
apt-get install nghttp2-client
然后可以探測一下一些已經(jīng)開啟 HTTP 2的站點惊搏,基本上節(jié)約的流量都是百分之25起贮乳,如果頻繁訪問的話 會更多:
對于流量消耗來說,其實HTTP 2相比HTTP 1.x協(xié)議最大的改進就是在HTTP 2中我們可以對HTTP 的頭部進行壓縮了恬惯,而在以往HTTP 1.x協(xié)議中向拆,gzip等是無法對header進行壓縮的,尤其對于絕大多數(shù)的請求來說酪耳,其實header的占比是最大的浓恳。
我們首先來了解一下靜態(tài)字典,如圖所示:
這個其實不難理解碗暗,無非就是將我們那些常用的HTTP 頭部颈将,用固定的數(shù)字來表示,那當然可以起到節(jié)約流量的作用.這里要注意的是 有些value 情況比較復雜的header言疗,他們的value 是沒有做靜態(tài)字典的晴圾。比如cache-control這個緩存控制字段,這后面的值因為太多了就無法用靜態(tài)字典來解決噪奄,而只能靠霍夫曼編碼死姚。下圖可以表示 HPACK這種壓縮算法 起到的節(jié)約流量的作用:
例如,我們看下62這個 頭部勤篮,user-agent 代指瀏覽器都毒,一般我們請求的時候這個頭部信息都是不會變的,所以最終經(jīng)過hpack算法優(yōu)化以后 后續(xù)再傳輸?shù)臅r候 就只需要傳輸62這個數(shù)字就可以代表其含義了碰缔。
又例如下圖:
也是一樣的账劲,多個請求連續(xù)發(fā)送的時候,多數(shù)情況下變化的只有path金抡,其余頭部信息是不變的瀑焦,那么基于此場景,最終傳輸?shù)臅r候也就只有path這一個頭部信息了竟终。
最后我們來看看hpack算法中的核心:哈夫曼編碼蝠猬。哈弗曼編碼核心思想就是出現(xiàn)頻率較高的用較短的編碼,出現(xiàn)頻率較低的用較長的編碼(HTTP 2協(xié)議的前身spdy協(xié)議采用的是動態(tài)的哈夫曼編碼统捶,而HTTP 2協(xié)議則選擇了靜態(tài)的哈夫曼編碼)榆芦。
來看幾個例子:
例如這個header幀,注意看這個method:get的頭部信息喘鸟。因為method:get 在靜態(tài)索引表中的索引值為2.對于這種key和value都在索引表中的值匆绣,我們用一個字節(jié)也就是8個bit來標識,其中第一個bit固定為1什黑,剩下7位就用來表示索引表中的值崎淳,這里method:get 索引表的值為2,所以這個值就是1000 0010愕把,換算成16進制就是0x82.
再看一組拣凹,key在索引表中森爽,value 不在索引表中的header例子。
對于key在索引表中嚣镜,value 不在索引表中的情況爬迟,固定是01開頭的字節(jié),后面6個bit(111010 換算成十進制就是58)就是靜態(tài)索引的值菊匿, user-agent在索引中index的值是58 再加上01開頭的2個bit 換算成二進制就是01111010,16進制就7a了付呕。然后接著看第二個字節(jié),0xd4,0xd4換算成二進制就是 1 101 0100跌捆,其中第一個bit 代表后面采用的是哈夫曼編碼徽职,后面的7個bit 這個key-value的value 需要幾個字節(jié)來表示,這里是101 0100 換算成10進制就是84佩厚,也就是說這個user-agent后面的value需要84個字節(jié)來表示姆钉,我們數(shù)一下圖中的字節(jié)數(shù) 16*5+第一排d4后面的4個字節(jié),剛好等于84個字節(jié)抄瓦。
最后再看一個key和value 都不在索引表中的例子育韩。
四、HTTP 2 協(xié)議中 Server Push 的能力
前文我們提到過闺鲸,H2相比H1.x協(xié)議提升最大的就是H2可以在單條TCP連接的基礎上 同時傳輸n個stream。從而避免H1.x協(xié)議中隊頭擁塞的問題埃叭。實際上在大部分前端的頁面中摸恍,我們還可以使用H2協(xié)議的Server Push能力 進一步提高頁面的加載速度。例如通常我們用瀏覽器訪問一個Html頁面時赤屋,只有當html頁面返回到瀏覽器立镶,瀏覽器內(nèi)核解析到這個Html頁面中有CSS或者JS之類的資源時,瀏覽器才會發(fā)送對應的CSS或者JS請求类早,當CSS和JS回來以后 瀏覽器才會進一步渲染媚媒,這樣的流程通常會導致瀏覽器處于一段時間內(nèi)的白屏從而降低用戶體驗。有了H2協(xié)議以后涩僻,當瀏覽器訪問一個Html頁面到服務器時缭召,服務器就可以主動推送相應的CSS和JS的內(nèi)容到瀏覽器,這樣就可以省略瀏覽器之后重新發(fā)送CSS和JS請求的步驟逆日。
有些人對Server Push存在一定程度上的誤解嵌巷,認為這種技術(shù)能夠讓服務器向瀏覽器發(fā)送“通知”,甚至將其與WebSocket進行比較室抽。事實并非如此搪哪,Server Push只是省去了瀏覽器發(fā)送請求的過程。只有當“如果不推送這個資源坪圾,瀏覽器就會請求這個資源”的時候晓折,瀏覽器才會使用推送過來的內(nèi)容惑朦。否則如果瀏覽器本身就不會請求某個資源,那么推送這個資源只會白白消耗帶寬漓概。當然如果與服務器通信的是客戶端而不是瀏覽器漾月,那么HTTP 2協(xié)議自然就可以完成 push推送的功能了。所以都使用HTTP 2協(xié)議的情況下垛耳,與服務器通信的是客戶端還是瀏覽器 在功能上還是有一定區(qū)別的栅屏。
下面為了演示這個過程,我們寫一段代碼堂鲜≌祸ǎ考慮到瀏覽器訪問HTTP 2站點必須要建立在TLS連接之上,我們首先要生成對應的證書和秘鑰缔莲。
然后開啟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環(huán)境
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文件痴奏,不需要等待瀏覽器發(fā)起請求
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訪問這個網(wǎng)頁的時候蛀骇,看下NetWork面板:
可以看出來這個CSS文件 就是我們主動push過來的。再看下Wireshark读拆。
可以看出來 stream id為13的 是客戶端發(fā)起的請求擅憔,因為id是單數(shù)的,在這個stream中檐晕,還存在著push_promise幀暑诸,這個幀就是由服務器發(fā)送給瀏覽器的,看一下他的具體內(nèi)容辟灰。
可以看出來這個幀就是用來告訴瀏覽器个榕,我主動push給你的是哪個資源,這個資源的stream-id 是6.圖中我們也看到了有一個stream-id 為6的 data在傳輸了芥喇,這個就是服務器主動push出來的CSS文件西采。到這里,一次完整的Server Push就交互完畢了继控。
但在實際線上應用Server Push的時候 挑戰(zhàn)遠遠比我們這個demo中來的復雜械馆。首先就是大部分cdn供應商(除非自建cdn)對Server Push的支持比較有限。我們不可能讓每一次資源的請求都直接打到我們的源服務器上湿诊,大部分靜態(tài)資源都是前置在CDN中癞谒。其次萍恕,對于靜態(tài)資源來說臼膏,我們還要考慮緩存的影響危尿,如果是瀏覽器自己發(fā)出去的靜態(tài)資源請求,瀏覽器是可以根據(jù)緩存狀態(tài)來決定這個資源我是否真的需要去請求,而Server Push 是服務器主動發(fā)起的错沽,服務器多數(shù)情況下是不知道這個資源的緩存是否過期的簿晓。當然可以在瀏覽器接收到push Promise幀以后,查詢自身的緩存狀態(tài)然后發(fā)起RST_STREAM幀千埃,告知服務器這個資源我有緩存憔儿,不需要繼續(xù)發(fā)送了,但是你沒辦法保證這個RST_STREAM在到達服務器的時候放可,服務器主動push出去的data幀還沒發(fā)出去谒臼。所以還是會存在一定的帶寬浪費的現(xiàn)象∫铮總體來說蜈缤,Server Push 還是一個提高前端用戶體驗相當有效的手段,使用了Server Push以后 瀏覽器的性能指標 idle指標 一般可以提高3-5倍(畢竟瀏覽器不用等待解析Html以后再去請求CSS和JS了)冯挎。
五底哥、HTTP 2 為什么要實現(xiàn)流量控制?
很多人不理解房官,為什么TCP傳輸層已經(jīng)實現(xiàn)了流量控制趾徽,我們的應用層 HTTP 2 還要實現(xiàn)流量控制。下面我們看一張圖翰守。
在HTTP 2協(xié)議中孵奶,因為我們支持多路復用,也就是說我們可以同時發(fā)送多個stream 在同一條TCP連接中蜡峰,上圖中拒课,每一種顏色就代表一個stream,可以看到 我們總共有4種stream事示,每一個stream又有n個frame,這個就很危險了僻肖,假設在應用層中我們使用了多路復用肖爵,就會出現(xiàn)n個frame同時不停的發(fā)送到目標服務器中,此時流量達到頂峰就會觸發(fā)TCP的擁塞控制臀脏,從而將后續(xù)的frame全部阻塞住劝堪,造成服務器響應過慢了。HTTP 1.x 中因為不支持多路復用自然就不存在這個問題揉稚。且我們之前多次提到過秒啦,一個請求從客戶端到達服務器端要經(jīng)過很多的代理服務器,這些代理服務器內(nèi)存大小以及網(wǎng)絡情況都可能不一樣搀玖,所以在應用層上做一次流量控制盡量避開觸發(fā)TCP的流控是十分有必要的余境。在HTTP 2協(xié)議中的流量控制策略,遵循以下幾個原則:
客戶端和服務端都有流量控制能力。
發(fā)送端和接收端可以獨立設置流控能力芳来。
只有data幀才需要流控含末,其他header幀或者push promise幀等都不需要。
流控能力只針對TCP連接的兩端即舌,中間即使有代理服務器佣盒,也不會透傳到源服務器上。
訪問知乎的站點看一下抓包顽聂。
這些標識window_update幀的 就是所謂的流控幀了肥惭。我們隨意點開一個看一下,就可以看到這個流量控制幀告訴我們的幀大小紊搪。
聰明如你一定能想到蜜葱,既然HTTP 2都能做到流控了,那一定也可以來做優(yōu)先級嗦明。比方說在HTTP 1.x協(xié)議中笼沥,我們訪問一個Html頁面,里面會有JS和CSS還有圖片等資源娶牌,我們同時發(fā)送這些請求奔浅,但是這些請求并沒有優(yōu)先級的概念,誰先出去誰先回來都是未知的(因為你也不知道這些CSS和JS請求是不是在同一條TCP連接上诗良,既然是分散在不同的TCP中汹桦,那么哪個快哪個慢是不確定的),但是從用戶體驗的角度來說鉴裹,肯定CSS的優(yōu)先級最高舞骆,然后是JS,最后才是圖片径荔,這樣就可以大大縮小瀏覽器白屏的時間督禽。在HTTP 2中 實現(xiàn)了這樣的能力。比如我們訪問sina的站點总处,然后抓包就可以看到:
可以看下這個CSS 幀的的優(yōu)先級:
JS的優(yōu)先級
最后是gif圖片的優(yōu)先級 狈惫,可以看出來這個優(yōu)先級是最低的。
有了weight這個關鍵字來標識優(yōu)先級鹦马,服務器就知道哪些請求需要優(yōu)先被響應優(yōu)先被發(fā)送response胧谈,哪些請求可以后一點被發(fā)送。這樣瀏覽器在整體上提供給用戶的體驗就會變的更好荸频。
六菱肖、HTTP 2 協(xié)議遇到的問題
基于TCP或者TCP+TLS的 HTTP 2協(xié)議 還是遇到了很多問題,比如:握手時間過長問題旭从,如果是基于TCP的HTTP 2協(xié)議稳强,那么至少要三次握手场仲,如果是TCP+TLS的HTTP 2協(xié)議,除了TCP的握手還要經(jīng)歷TLS的多次握手(TLS1.3已經(jīng)可以做到只有1次握手)键袱。每一次握手都需要發(fā)送一個報文然后接收到這個報文的ack才可以進行下一次握手燎窘,在弱網(wǎng)環(huán)境下可以想象的到這個連接建立的效率是極低的。此外蹄咖,TCP協(xié)議天生的隊頭擁塞 問題也一直在困擾著HTTP 21.x協(xié)議和HTTP 2協(xié)議褐健。我們看一下谷歌spdy的宣傳圖,可以更加精準的理解這個擁塞的本質(zhì):
圖一很好理解,我們多路復用支持下同時發(fā)了3個stream澜汤,然后經(jīng)過TCP/IP協(xié)議 發(fā)送到服務器端蚜迅,然后TCP協(xié)議把這些數(shù)據(jù)包再傳給我們的應用層,注意這里有個條件是俊抵,發(fā)送包的順序要和接收包的順序一致谁不。上圖中可以看到那些方塊的圖的順序是一致的,但是如果碰到下圖中的情況徽诲,比如說這些數(shù)據(jù)包恰好第一個紅色的數(shù)據(jù)包傳丟了刹帕,那么后續(xù)的數(shù)據(jù)包即使已經(jīng)到了服務器的機器里,也無法立刻將數(shù)據(jù)傳遞給我們的應用層協(xié)議谎替,因為TCP協(xié)議規(guī)定好了接收的順序要和發(fā)送的順序保持一致偷溺,既然紅色的數(shù)據(jù)包丟失了,那么后續(xù)的數(shù)據(jù)包就只能阻塞在服務器里钱贯,一直等到紅色的數(shù)據(jù)包經(jīng)過重新發(fā)送以后成功到達服務器了挫掏,再將這些數(shù)據(jù)包傳遞給應用層協(xié)議。
TCP協(xié)議除了有上述的一些缺陷以外秩命,還有一個問題就是TCP協(xié)議的實現(xiàn)者是在操作系統(tǒng)層面尉共,我們?nèi)魏握Z言,包括 Java弃锐,C袄友,C++,Go等等 對外暴露的所謂Socket編程接口 最終實現(xiàn)者其實都是操作系統(tǒng)自己霹菊。要讓操作系統(tǒng)自己升級TCP協(xié)議的實現(xiàn)是非常非常困難的杠河,況且整個互聯(lián)網(wǎng)中那么多設備想要整體實現(xiàn)TCP協(xié)議的升級是一件不現(xiàn)實的事情(IPV6協(xié)議升級的過慢也有這方面的原因)〗焦迹基于上述問題,谷歌就基于udp協(xié)議封裝了一層quic協(xié)議(其實很多基于udp協(xié)議的應用層協(xié)議唾戚,都是在應用層上部分實現(xiàn)了TCP協(xié)議的若干功能)柳洋,來替代HTTP 21.x-HTTP 2中的TCP協(xié)議。
我們打開Chrome中的quic協(xié)議開關:
然后訪問一下youtube(國內(nèi)的b站其實也支持)叹坦。
可以看出來已經(jīng)支持quic協(xié)議了熊镣。為什么這個選項在Chrome瀏覽器中默認是關閉的,其實也很好理解,這個quic協(xié)議實際上是谷歌自己搞出來的绪囱,還沒有被正式納入到HTTP 3協(xié)議中测蹲,一切都還在草案中。所以這個選項默認是關閉的鬼吵】奂祝看下quic協(xié)議相比于原來的TCP協(xié)議主要做了哪些改進?其實就是將原來隊列傳輸報文改成了無需隊列傳輸齿椅,那自然也就不存在隊頭擁塞的問題了琉挖。
此外在HTTP 3中還提供了 變更端口號或者ip地址也可以復用之前連接的能力,個人理解這個協(xié)議支持的特性可能更多是為了物聯(lián)網(wǎng)考慮的涣脚。物聯(lián)網(wǎng)中很多設備的ip都可能是一直變化的示辈。能復用之前的連接將會大大提高網(wǎng)絡傳輸?shù)男省_@樣就可以避免目前存在的斷網(wǎng)以后重新連接到網(wǎng)絡需要至少經(jīng)過1-3個rtt才可以繼續(xù)傳輸數(shù)據(jù)的弊端遣蚀。
最后要提一下矾麻,在極端弱網(wǎng)環(huán)境中,HTTP 2 的表現(xiàn)有可能不如HTTP 1.x芭梯,因為HTTP 2下面只有一條TCP連接险耀,弱網(wǎng)下,如果丟包率極高粥帚,那么會不斷的觸發(fā)TCP層面的超時重傳胰耗,造成TCP報文的積壓,遲遲無法將報文傳遞給上面的應用層芒涡,但是HTTP 1.x中柴灯,因為可以使用多條TCP連接,所以在一定程度上费尽,報文積壓的情況不會像HTTP 2那么嚴重赠群,這也是我認為的HTTP 2協(xié)議唯一不如HTTP 1.x的地方,當然這個鍋是TCP的旱幼,并不是HTTP 2本身的查描。
更多閱讀:
作者:vivo 互聯(lián)網(wǎng)-WuYue