透視HTTP協(xié)議
由于http/2 “事實(shí)上” 是基于TLS,所有在正式收發(fā)數(shù)據(jù)之前仲吏,會(huì)有TCP握和TLS握手诅挑,這兩個(gè)步驟相信你一定已經(jīng)很熟悉了
TLS握手成功之前返帕,客戶端必須要發(fā)送一個(gè) “連接前言” 夫嗓,用來(lái)確認(rèn)建立HTTP/2 連接
這個(gè)“連接前言”是標(biāo)準(zhǔn)的 HTTP/1 請(qǐng)求報(bào)文迟螺,使用純文本的ASCII碼格式;請(qǐng)求方法是特別注冊(cè)一個(gè)關(guān)鍵字 “PRI”舍咖;全文只有24個(gè)字節(jié)
PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n
在 wireshark里矩父,http/2 的連接前言被稱為 Magic
意思是 不可知的魔法
所以,就不要問(wèn)“為什么會(huì)是這樣”了排霉,只要服務(wù)器收到這個(gè)“有魔力的字符串”窍株,就知道客戶端在TLS上想要的是是HTTP/2協(xié)議,而不是其他別的協(xié)議,后面就會(huì)都使用 HTTP/2 的數(shù)據(jù)格式夹姥。
頭部壓縮:
確認(rèn)了連接了,HTTP/2就開(kāi)始準(zhǔn)備請(qǐng)求報(bào)文
因?yàn)檎Z(yǔ)義上它與HTTP/1兼容辙诞,所以報(bào)文還是由 “Header+Body”構(gòu)成的辙售,但在請(qǐng)求發(fā)送前,必須用“HPACK”算法來(lái)壓縮頭部數(shù)據(jù)飞涂。
“HPACK”算法是專門壓縮HTTP頭部制定的算法旦部。與gzip、zlib 等壓縮算法不同较店,它是一個(gè)“有狀態(tài)”的算法士八,需要客戶端和服務(wù)器各自維護(hù)一份“索引表”,也可以說(shuō)是“字典”(這有點(diǎn)類似brotli),壓縮和解壓縮就是查表和更新表的操作梁呈。
為了方便管理和壓縮婚度,HTTP/2 廢除了原有的起始行的概念,把起始行里面的請(qǐng)求方法官卡,URI蝗茁、狀態(tài)碼等統(tǒng)一轉(zhuǎn)換成了頭字段形式,并且給這些“不是頭字段的頭字段”起了個(gè)特別的名字——“偽頭字段”寻咒。而起始行里的版本號(hào)和錯(cuò)誤原因短語(yǔ)因?yàn)闆](méi)什么大用哮翘,順便也給廢除了
現(xiàn)在http 報(bào)文就簡(jiǎn)單了,全都是“key-value”形式的字段毛秘,于是HTTP/2 就為一些最常用的頭字段定義了一個(gè)只讀的“靜態(tài)表”
下面的這個(gè)表格列出了“靜態(tài)表”的一部分饭寺,這樣只要查表就可以知道字段名和對(duì)應(yīng)的值,
比如 數(shù)字 “2” 代表 “GET”, 數(shù)字“8” 代表狀態(tài)碼“200”
但是如果表里只有key 沒(méi)有value,或者是自定義字段根本找不到該怎么辦呢叫挟?
這就要用到 “動(dòng)態(tài)表”艰匙,他添加在靜態(tài)表后面,結(jié)構(gòu)相同霞揉,但會(huì)在編碼解碼的時(shí)候隨時(shí)更新旬薯。
比如說(shuō),第一次發(fā)送請(qǐng)求時(shí)的“user-agent”字段長(zhǎng)是一百多個(gè)字節(jié)适秩,用哈夫曼壓縮編碼發(fā)送之后绊序,客戶端和服務(wù)端都更新自己的動(dòng)態(tài)表,添加一個(gè)新的索引號(hào)“65”秽荞。 那么下一次發(fā)送的時(shí)候就不用再重復(fù)發(fā)那么多字節(jié)了骤公,只要用一個(gè)字節(jié)發(fā)送變化就好。
你可以想象得出來(lái)扬跋,隨著在HTTP/2 連接上發(fā)送的報(bào)文越來(lái)越多,兩邊的“字典”也會(huì)越來(lái)越豐富阶捆,最終每次的頭部字段都會(huì)變成一兩個(gè)字節(jié)的代碼,原來(lái)上千字節(jié)的頭用幾十字節(jié)就可以表示了,壓縮效果比gzip要好多了洒试。
二進(jìn)制幀
頭部數(shù)據(jù)壓縮之后倍奢,HTTP/2 就要把報(bào)文拆成二進(jìn)制的幀準(zhǔn)備發(fā)送
HTTP/2 的幀結(jié)構(gòu)有點(diǎn)類似TCP的端或者TLS里的記錄,但報(bào)頭很小垒棋,只有9字節(jié)卒煞,非常地節(jié)省(可以對(duì)比一下TCP頭叼架,它最少是20個(gè)字節(jié))
二進(jìn)制的格式也保證了不會(huì)有歧義畔裕,而且使用位運(yùn)算能夠非常簡(jiǎn)單高效地解析。
幀開(kāi)頭是3個(gè)字節(jié)的長(zhǎng)度(但不包括頭的9個(gè)字節(jié))乖订,默認(rèn)上限是214扮饶,最大是224,也就是說(shuō)HTTP/2 的幀通常不超過(guò)16K,最大是16M
長(zhǎng)度后面的一個(gè)字節(jié)是 幀類型 乍构,大致可以分為 數(shù)據(jù)幀和控制幀兩類甜无,HEADERS幀和DATA幀屬于數(shù)據(jù)幀,存放的是HTTP報(bào)文蜡吧,而SETTINGS,PING毫蚓、PRIORITY 等則是用來(lái)管理流的控制幀。
HTTP/2 總共定義了10中類型的幀昔善,但一個(gè)字節(jié)可以表示最多256種元潘,所以也允許在標(biāo)準(zhǔn)之外定義其他類型實(shí)現(xiàn)功能擴(kuò)展。這就有點(diǎn)像TLS里擴(kuò)展協(xié)議的意思了君仆,比如Google 的gRPC就利用了這個(gè)特點(diǎn)翩概,定義了自用的新幀類型。
第5個(gè)字節(jié)是非常重要的 幀標(biāo)志信息返咱,可以保存8個(gè)標(biāo)志位钥庇,攜帶簡(jiǎn)單的控制信息。常用的標(biāo)志位有 END_HEADERS 表示頭數(shù)據(jù)結(jié)束咖摹,相當(dāng)于HTTP/1 里頭的空行(“\r\n”),END_STREAM 表示單方向數(shù)據(jù)發(fā)送結(jié)束(即EOS,End of Stream),相當(dāng)于HTTP/1里 Chunked 分塊結(jié)束標(biāo)志(“0\r\n\r\n”)
報(bào)文頭里最后4個(gè)字節(jié)是流標(biāo)識(shí)符评姨,也就是幀所屬的流,接收方使用它就可以從亂序的幀里識(shí)別出具有相同流ID的幀序列萤晴,按書(shū)序組裝起來(lái)就實(shí)現(xiàn)了虛擬的“流”
流標(biāo)識(shí)符雖然有4個(gè)字節(jié)吐句,但最高位被保留不用,所以只有31位可以使用店读,也就是說(shuō)嗦枢,流標(biāo)識(shí)符的上限是2^31,大約是21億屯断。
好了文虏,把二進(jìn)制頭理清楚后侣诺,我們來(lái)看一下 Wireshark 抓包的幀實(shí)例:
在這個(gè)幀里,開(kāi)頭的三個(gè)字節(jié)是"00010a",表示數(shù)據(jù)長(zhǎng)度是266字節(jié)氧秘。
幀類型是1年鸳,表示 HEADERS幀,負(fù)載(payload)里面存放的是被HPACK算法壓縮的頭部信息丸相。
標(biāo)志位是0x25,轉(zhuǎn)換成二進(jìn)制有3個(gè)位被置1
PRIORITY表示設(shè)置了流的優(yōu)先級(jí)阻星,END_HEADERS表示這一個(gè)幀就是完整的頭數(shù)據(jù),END_STREAM表示單方向數(shù)據(jù)發(fā)送結(jié)束已添,后續(xù)再不會(huì)有數(shù)據(jù)幀(即請(qǐng)求報(bào)文完畢,不會(huì)再有DATA幀)
最后4個(gè)字節(jié)的流標(biāo)識(shí)符是整數(shù)1滥酥,表示這個(gè)客戶端發(fā)起的第一個(gè)流更舞,后面的響應(yīng)數(shù)據(jù)幀也會(huì)是這個(gè)ID,也就是說(shuō)在Stream[1]里完成這個(gè)請(qǐng)求響應(yīng)坎吻。
流與多路復(fù)用
流是二進(jìn)制幀的雙向傳輸序列缆蝉。
要搞明白流,關(guān)鍵是要理解幀頭里的流ID.
在HTTP/2 連接上瘦真,雖然幀是亂序收發(fā)的刊头,但只要他們都擁有相同的流ID,就都屬于一個(gè)流,而且在這個(gè)流里幀不是無(wú)需的诸尽,而是有著嚴(yán)格的先后順序
在概念上原杂,一個(gè)HTTP/2的流就等同于一個(gè)HTTP/1里的“請(qǐng)求-應(yīng)答”。在HTTP/1里一個(gè)“請(qǐng)求-響應(yīng)”報(bào)文來(lái)回是一次HTTP通信您机,在HTTP/2里一個(gè)流也承載了相同的功能穿肄。
你還可以對(duì)照著TCP來(lái)理解。TCP運(yùn)行在IP之上际看,其實(shí)從MAC層咸产、IP層的角度來(lái)看。TCP的“連接”概念也是虛擬的仲闽。但從功能上看脑溢,無(wú)論是HTTP/2的流,還是TCP的連接赖欣,都是實(shí)際存在的屑彻,所以你以后大可不必再糾結(jié)于流的“虛擬”性,把它當(dāng)做是一個(gè)真實(shí)存在的實(shí)體來(lái)理解就好畏鼓。
HTTP/2的流有哪些特點(diǎn)呢酱酬?
- 流是可并發(fā)的,一個(gè)HTTP/2連接上可以同時(shí)發(fā)出多個(gè)流傳輸數(shù)據(jù)云矫,也就是并發(fā)多請(qǐng)求膳沽,實(shí)現(xiàn)“多路復(fù)用”;
- 客戶端和服務(wù)器都可以創(chuàng)建流,雙方互補(bǔ)干擾挑社;
- 流是雙向的陨界,一個(gè)流里面客戶端和服務(wù)器都可以發(fā)送或接收數(shù)據(jù)幀
- 流之間沒(méi)有固定關(guān)系,彼此獨(dú)立痛阻,但流內(nèi)部的幀是有嚴(yán)格順序的菌瘪;
- 流可以設(shè)置優(yōu)先級(jí),讓服務(wù)器優(yōu)先處理阱当,比如先傳 HTML/CSS,后傳圖片俏扩,優(yōu)化用戶體驗(yàn)
- 流ID不能重用,只能順序遞增弊添,客戶端發(fā)起的ID是奇數(shù)录淡,服務(wù)器端發(fā)起的id是偶數(shù);
- 在流上發(fā)送“RST_STREAM” 幀可以隨時(shí)終止流,取消接收或發(fā)送油坝;
- 第0號(hào)流比較特殊嫉戚,不能關(guān)閉,也不能發(fā)送數(shù)據(jù)幀澈圈,只能發(fā)送控制幀彬檀,用于流量控制。
下面的圖顯示了 連接中無(wú)序的幀是如何依據(jù)流ID重組成流的瞬女。
HTTP/2 在一個(gè)連接上使用多個(gè)流收發(fā)數(shù)據(jù)窍帝,那么它本身默認(rèn)就會(huì)是長(zhǎng)連接,所以永遠(yuǎn)不需要“Connection”頭字段(Keepalive/close)
又比如诽偷,下載大文件的時(shí)候想取消接收盯桦,在 HTTP/1 里只能斷開(kāi) TCP 連接重新“三次握手”,成本很高渤刃,而在 HTTP/2 里就可以簡(jiǎn)單地發(fā)送一個(gè)“RST_STREAM”中斷流拥峦,而長(zhǎng)連接會(huì)繼續(xù)保持。
再比如卖子,因?yàn)榭蛻舳撕头?wù)器兩端都可以創(chuàng)建流略号,而流 ID 有奇數(shù)偶數(shù)和上限的區(qū)分,所以大多數(shù)的流 ID 都會(huì)是奇數(shù)洋闽,而且客戶端在一個(gè)連接里最多只能發(fā)出 2^30玄柠,也就是 10 億個(gè)請(qǐng)求。
所以就要問(wèn)了:ID 用完了該怎么辦呢诫舅?這個(gè)時(shí)候可以再發(fā)一個(gè)控制幀“GOAWAY”羽利,真正關(guān)閉 TCP 連接。
流狀態(tài)轉(zhuǎn)換
流很重要刊懈,也很復(fù)雜这弧。為了更好地描述運(yùn)行機(jī)制娃闲,HTTP/2 借鑒了 TCP,根據(jù)幀的標(biāo)志位實(shí)現(xiàn)流狀態(tài)轉(zhuǎn)換匾浪。當(dāng)然皇帮,這些狀態(tài)也是虛擬的,只是為了輔助理解蛋辈。
HTTP/2 的流也有一個(gè)狀態(tài)轉(zhuǎn)換圖属拾,雖然比 TCP 要簡(jiǎn)單一點(diǎn),但也不那么好懂冷溶,所以今天我只畫(huà)了一個(gè)簡(jiǎn)化的圖渐白,對(duì)應(yīng)到一個(gè)標(biāo)準(zhǔn)的 HTTP“請(qǐng)求 - 應(yīng)答”。
最開(kāi)始的時(shí)候流都是“空閑”(idle)狀態(tài)逞频,也就是“不存在”礼预,可以理解成是待分配的“號(hào)段資源”。
當(dāng)客戶端發(fā)送HEADERS幀后虏劲,有了流id,流就進(jìn)入了 **“打開(kāi)” ** 狀態(tài)褒颈,兩端都可以收發(fā)數(shù)據(jù)柒巫,然后客戶端發(fā)送個(gè)“END_STREAM”標(biāo)志位的幀,流就進(jìn)入了“半關(guān)閉”狀態(tài)
這個(gè)“半關(guān)閉”狀態(tài)很重要谷丸,意味著客戶端的請(qǐng)求數(shù)據(jù)以及發(fā)送完了堡掏,需要接受響應(yīng)數(shù)據(jù),而服務(wù)端也知道請(qǐng)求數(shù)據(jù)接收完畢刨疼,之后就要內(nèi)部處理泉唁,再發(fā)送響應(yīng)數(shù)據(jù)。
響應(yīng)數(shù)據(jù)發(fā)完了之后揩慕,也要帶上“END_STREAM”標(biāo)志位亭畜,表示數(shù)據(jù)發(fā)送完畢,這樣流兩端就都進(jìn)入了“關(guān)閉”狀態(tài)迎卤,流就結(jié)束了拴鸵。
剛才也說(shuō)過(guò),流ID不能重用蜗搔,所以流的生命周期就是HTTP/1里的一次完整的“請(qǐng)求-應(yīng)答”劲藐,流關(guān)閉就是一次通信結(jié)束。
下一次在發(fā)送請(qǐng)求就要開(kāi)一個(gè)新流(而不是新連接)樟凄,流id不斷增加聘芜,直到到達(dá)上限,發(fā)送“GOAWAY”幀開(kāi)一個(gè)新的TCP連接缝龄,流ID就又可以重頭計(jì)數(shù)汰现。
你再看看這張圖挂谍,是不是和 HTTP/1 里的標(biāo)準(zhǔn)“請(qǐng)求 - 應(yīng)答”過(guò)程很像,只不過(guò)這是發(fā)生在虛擬的“流”上服鹅,而不是實(shí)際的 TCP 連接凳兵,又因?yàn)榱骺梢圆l(fā),所以 HTTP/2 就可以實(shí)現(xiàn)無(wú)阻塞的多路復(fù)用企软。
小結(jié)
HTTP/2 的內(nèi)容實(shí)在是太多了庐扫,為了方便學(xué)習(xí),我砍掉了一些特性仗哨,比如流的優(yōu)先級(jí)形庭、依賴關(guān)系、流量控制等厌漂。
但只要你掌握了今天的這些內(nèi)容萨醒,以后再看 RFC 文檔都不會(huì)有難度了。
- HTTP/2 必須先發(fā)送一個(gè)“連接前言”字符串苇倡,然后才能建立正式連接富纸;
- HTTP/2 廢除了起始行,統(tǒng)一使用頭字段旨椒,在兩端維護(hù)"key-value"的索引表晓褪,使用“HPACK”算法壓縮頭部;
- HTTP/2 把報(bào)文切分為多種類型的二進(jìn)制幀综慎,報(bào)頭里最重要的字段是流標(biāo)識(shí)符涣仿,標(biāo)記幀屬于哪個(gè)流;
- 流是 http/2虛擬的概念示惊,是幀的雙向傳輸序列好港,相當(dāng)與HTTP/1里的一次“請(qǐng)求-應(yīng)答”
- 在一個(gè)HTTP/2連接上 可以并發(fā)多個(gè)流,也就是多個(gè)“請(qǐng)求-響應(yīng)” 報(bào)文米罚,這就是多路復(fù)用