二进制图片在http怎么显示_HTTP/2内核剖析
點擊?藍色“?深入原理”,關注并“設為星標”
技術干貨,第一時間推送
今天我們繼續上一講的話題,深入 HTTP/2 協議的內部,看看它的實現細節。
這次實驗環境的 URI 是“/31-1”,我用 Wireshark 把請求響應的過程抓包存了下來,文件放在 GitHub 的“wireshark”目錄。今天我們就對照著抓包來實地講解 HTTP/2 的頭部壓縮、二進制幀等特性。
連接前言
由于 HTTP/2“事實上”是基于 TLS,所以在正式收發數據之前,會有 TCP 握手和 TLS 握手,這兩個步驟相信你一定已經很熟悉了,所以這里就略過去不再細說。
TLS 握手成功之后,客戶端必須要發送一個“連接前言”(connection preface),用來確認建立 HTTP/2 連接。
這個“連接前言”是標準的 HTTP/1 請求報文,使用純文本的 ASCII 碼格式,請求方法是特別注冊的一個關鍵字“PRI”,全文只有 24 個字節:
PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n在 Wireshark 里,HTTP/2 的“連接前言”被稱為“Magic”,意思就是“不可知的魔法”。
所以,就不要問“為什么會是這樣”了,只要服務器收到這個“有魔力的字符串”,就知道客戶端在 TLS 上想要的是 HTTP/2 協議,而不是其他別的協議,后面就會都使用 HTTP/2 的數據格式。
頭部壓縮
確立了連接之后,HTTP/2 就開始準備請求報文。
因為語義上它與 HTTP/1 兼容,所以報文還是由“Header+Body”構成的,但在請求發送前,必須要用“HPACK”算法來壓縮頭部數據。
“HPACK”算法是專門為壓縮 HTTP 頭部定制的算法,與 gzip、zlib 等壓縮算法不同,它是一個“有狀態”的算法,需要客戶端和服務器各自維護一份“索引表”,也可以說是“字典”(這有點類似 brotli),壓縮和解壓縮就是查表和更新表的操作。
為了方便管理和壓縮,HTTP/2 廢除了原有的起始行概念,把起始行里面的請求方法、URI、狀態碼等統一轉換成了頭字段的形式,并且給這些“不是頭字段的頭字段”起了個特別的名字——“偽頭字段”(pseudo-header fields)。而起始行里的版本號和錯誤原因短語因為沒什么大用,順便也給廢除了。
為了與“真頭字段”區分開來,這些“偽頭字段”會在名字前加一個“:”,比如“:authority” “:method” “:status”,分別表示的是域名、請求方法和狀態碼。
現在 HTTP 報文頭就簡單了,全都是“Key-Value”形式的字段,于是 HTTP/2 就為一些最常用的頭字段定義了一個只讀的“靜態表”(Static Table)。
下面的這個表格列出了“靜態表”的一部分,這樣只要查表就可以知道字段名和對應的值,比如數字“2”代表“GET”,數字“8”代表狀態碼 200。
但如果表里只有 Key 沒有 Value,或者是自定義字段根本找不到該怎么辦呢?
這就要用到“動態表”(Dynamic Table),它添加在靜態表后面,結構相同,但會在編碼解碼的時候隨時更新。
比如說,第一次發送請求時的“user-agent”字段長是一百多個字節,用哈夫曼壓縮編碼發送之后,客戶端和服務器都更新自己的動態表,添加一個新的索引號“65”。那么下一次發送的時候就不用再重復發那么多字節了,只要用一個字節發送編號就好。
你可以想象得出來,隨著在 HTTP/2 連接上發送的報文越來越多,兩邊的“字典”也會越來越豐富,最終每次的頭部字段都會變成一兩個字節的代碼,原來上千字節的頭用幾十個字節就可以表示了,壓縮效果比 gzip 要好得多。
二進制幀
頭部數據壓縮之后,HTTP/2 就要把報文拆成二進制的幀準備發送。
HTTP/2 的幀結構有點類似 TCP 的段或者 TLS 里的記錄,但報頭很小,只有 9 字節,非常地節省(可以對比一下 TCP 頭,它最少是 20 個字節)。
二進制的格式也保證了不會有歧義,而且使用位運算能夠非常簡單高效地解析。
幀開頭是 3 個字節的長度(但不包括頭的 9 個字節),默認上限是 2^14,最大是 2^24,也就是說 HTTP/2 的幀通常不超過 16K,最大是 16M。
長度后面的一個字節是幀類型,大致可以分成數據幀和控制幀兩類,HEADERS 幀和 DATA 幀屬于數據幀,存放的是 HTTP 報文,而 SETTINGS、PING、PRIORITY 等則是用來管理流的控制幀。
HTTP/2 總共定義了 10 種類型的幀,但一個字節可以表示最多 256 種,所以也允許在標準之外定義其他類型實現功能擴展。這就有點像 TLS 里擴展協議的意思了,比如 Google 的 gRPC 就利用了這個特點,定義了幾種自用的新幀類型。
第 5 個字節是非常重要的幀標志信息,可以保存 8 個標志位,攜帶簡單的控制信息。常用的標志位有 END_HEADERS 表示頭數據結束,相當于 HTTP/1 里頭后的空行(“\r\n”),END_STREAM 表示單方向數據發送結束(即 EOS,End of Stream),相當于 HTTP/1 里 Chunked 分塊結束標志(“0\r\n\r\n”)。
報文頭里最后 4 個字節是流標識符,也就是幀所屬的“流”,接收方使用它就可以從亂序的幀里識別出具有相同流 ID 的幀序列,按順序組裝起來就實現了虛擬的“流”。
流標識符雖然有 4 個字節,但最高位被保留不用,所以只有 31 位可以使用,也就是說,流標識符的上限是 2^31,大約是 21 億。
好了,把二進制頭理清楚后,我們來看一下 Wireshark 抓包的幀實例:
在這個幀里,開頭的三個字節是“00010a”,表示數據長度是 266 字節。
幀類型是 1,表示 HEADERS 幀,負載(payload)里面存放的是被 HPACK 算法壓縮的頭部信息。
標志位是 0x25,轉換成二進制有 3 個位被置 1。PRIORITY 表示設置了流的優先級,END_HEADERS 表示這一個幀就是完整的頭數據,END_STREAM 表示單方向數據發送結束,后續再不會有數據幀(即請求報文完畢,不會再有 DATA 幀 /Body 數據)。
最后 4 個字節的流標識符是整數 1,表示這是客戶端發起的第一個流,后面的響應數據幀也會是這個 ID,也就是說在 stream[1]里完成這個請求響應。
流與多路復用
弄清楚了幀結構后我們就來看 HTTP/2 的流與多路復用,它是 HTTP/2 最核心的部分。
在上一講里我簡單介紹了流的概念,不知道你“悟”得怎么樣了?這里我再重復一遍:流是二進制幀的雙向傳輸序列。
要搞明白流,關鍵是要理解幀頭里的流 ID。
在 HTTP/2 連接上,雖然幀是亂序收發的,但只要它們都擁有相同的流 ID,就都屬于一個流,而且在這個流里幀不是無序的,而是有著嚴格的先后順序。
比如在這次的 Wireshark 抓包里,就有“0、1、3”一共三個流,實際上就是分配了三個流 ID 號,把這些幀按編號分組,再排一下隊,就成了流。
在概念上,一個 HTTP/2 的流就等同于一個 HTTP/1 里的“請求 - 應答”。在 HTTP/1 里一個“請求 - 響應”報文來回是一次 HTTP 通信,在 HTTP/2 里一個流也承載了相同的功能。
你還可以對照著 TCP 來理解。TCP 運行在 IP 之上,其實從 MAC 層、IP 層的角度來看,TCP 的“連接”概念也是“虛擬”的。但從功能上看,無論是 HTTP/2 的流,還是 TCP 的連接,都是實際存在的,所以你以后大可不必再糾結于流的“虛擬”性,把它當做是一個真實存在的實體來理解就好。
HTTP/2 的流有哪些特點呢?我給你簡單列了一下:
流是可并發的,一個 HTTP/2 連接上可以同時發出多個流傳輸數據,也就是并發多請求,實現“多路復用”;
客戶端和服務器都可以創建流,雙方互不干擾;
流是雙向的,一個流里面客戶端和服務器都可以發送或接收數據幀,也就是一個“請求 - 應答”來回;
流之間沒有固定關系,彼此獨立,但流內部的幀是有嚴格順序的;
流可以設置優先級,讓服務器優先處理,比如先傳 HTML/CSS,后傳圖片,優化用戶體驗;
流 ID 不能重用,只能順序遞增,客戶端發起的 ID 是奇數,服務器端發起的 ID 是偶數;
在流上發送“RST_STREAM”幀可以隨時終止流,取消接收或發送;
第 0 號流比較特殊,不能關閉,也不能發送數據幀,只能發送控制幀,用于流量控制。
這里我又畫了一張圖,把上次的圖略改了一下,顯示了連接中無序的幀是如何依據流 ID 重組成流的。
從這些特性中,我們還可以推理出一些深層次的知識點。
比如說,HTTP/2 在一個連接上使用多個流收發數據,那么它本身默認就會是長連接,所以永遠不需要“Connection”頭字段(keepalive 或 close)。
你可以再看一下 Wireshark 的抓包,里面發送了兩個請求“/31-1”和“/favicon.ico”,始終用的是“560958443”這個連接,對比一下第 8 講,你就能夠看出差異了。
又比如,下載大文件的時候想取消接收,在 HTTP/1 里只能斷開 TCP 連接重新“三次握手”,成本很高,而在 HTTP/2 里就可以簡單地發送一個“RST_STREAM”中斷流,而長連接會繼續保持。
再比如,因為客戶端和服務器兩端都可以創建流,而流 ID 有奇數偶數和上限的區分,所以大多數的流 ID 都會是奇數,而且客戶端在一個連接里最多只能發出 2^30,也就是 10 億個請求。
所以就要問了:ID 用完了該怎么辦呢?這個時候可以再發一個控制幀“GOAWAY”,真正關閉 TCP 連接。
流狀態轉換
流很重要,也很復雜。為了更好地描述運行機制,HTTP/2 借鑒了 TCP,根據幀的標志位實現流狀態轉換。當然,這些狀態也是虛擬的,只是為了輔助理解。
HTTP/2 的流也有一個狀態轉換圖,雖然比 TCP 要簡單一點,但也不那么好懂,所以今天我只畫了一個簡化的圖,對應到一個標準的 HTTP“請求 - 應答”。
最開始的時候流都是“空閑”(idle)狀態,也就是“不存在”,可以理解成是待分配的“號段資源”。
當客戶端發送 HEADERS 幀后,有了流 ID,流就進入了“打開”狀態,兩端都可以收發數據,然后客戶端發送一個帶“END_STREAM”標志位的幀,流就進入了“半關閉”狀態。
這個“半關閉”狀態很重要,意味著客戶端的請求數據已經發送完了,需要接受響應數據,而服務器端也知道請求數據接收完畢,之后就要內部處理,再發送響應數據。
響應數據發完了之后,也要帶上“END_STREAM”標志位,表示數據發送完畢,這樣流兩端就都進入了“關閉”狀態,流就結束了。
剛才也說過,流 ID 不能重用,所以流的生命周期就是 HTTP/1 里的一次完整的“請求 - 應答”,流關閉就是一次通信結束。
下一次再發請求就要開一個新流(而不是新連接),流 ID 不斷增加,直到到達上限,發送“GOAWAY”幀開一個新的 TCP 連接,流 ID 就又可以重頭計數。
你再看看這張圖,是不是和 HTTP/1 里的標準“請求 - 應答”過程很像,只不過這是發生在虛擬的“流”上,而不是實際的 TCP 連接,又因為流可以并發,所以 HTTP/2 就可以實現無阻塞的多路復用。
小結
HTTP/2 的內容實在是太多了,為了方便學習,我砍掉了一些特性,比如流的優先級、依賴關系、流量控制等。
但只要你掌握了今天的這些內容,以后再看 RFC 文檔都不會有難度了。
HTTP/2 必須先發送一個“連接前言”字符串,然后才能建立正式連接;
HTTP/2 廢除了起始行,統一使用頭字段,在兩端維護字段“Key-Value”的索引表,使用“HPACK”算法壓縮頭部;
HTTP/2 把報文切分為多種類型的二進制幀,報頭里最重要的字段是流標識符,標記幀屬于哪個流;
流是 HTTP/2 虛擬的概念,是幀的雙向傳輸序列,相當于 HTTP/1 里的一次“請求 - 應答”;
在一個 HTTP/2 連接上可以并發多個流,也就是多個“請求 - 響應”報文,這就是“多路復用”。
-深入原理- ?
? ?知其然并知其所以然? ??
總結
以上是生活随笔為你收集整理的二进制图片在http怎么显示_HTTP/2内核剖析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python安装报错ox000007b_
- 下一篇: 先进先出算法_结构与算法(02):队列和