【網絡通信 -- 直播】IM 學習系列 -- 網絡通信協議簡介(HTTP 協議 七)

【網絡通信 -- 直播】IM 學習系列 -- 網絡通信協議簡介(HTTP 協議 七)

【1】HTTP/2 特性概覽

HTTP/2 把 HTTP 分解成了“語義”和“語法”兩個部分,“語義”層不做改動,與 HTTP/1 完全一致;同時 HTTP/2 沒有在 URI 裏引入新的協議名,仍然用“http”表示明文協議,用“https”表示加密協議;

【1.1】頭部壓縮
HTTP/1 中可以用頭字段“Content-Encoding”指定 Body 的編碼方式,比如用 gzip 壓縮來節約帶寬,但報文的另一個組成部分——Header 卻沒有針對其的優化手段,報文 Header 一般會攜帶“User Agent”“Cookie”“Accept”“Server”等許多固定的頭字段,多達幾百字節甚至上千字節,但 Body 卻經常只有幾十字節;此外,請求響應報文裏有很多字段值都是重複的,造成“長尾效應”,導致大量帶寬消耗在了這些冗餘度極高的數據上;
HTTP/2 把“頭部壓縮”作爲性能改進的一個重點,優化的方式便是“壓縮”,HTTP/2  開發了專門的“HPACK”算法,在客戶端和服務器兩端建立“字典”,用索引號表示重複的字符串,還釆用哈夫曼編碼來壓縮整數和字符串,可以達到 50%~90% 的高壓縮率;

【1.2】二進制格式
HTTP/2 全面採用二進制格式,大大方便了計算機的解析;二進制裏只有“0”和“1”,可以嚴格規定字段大小、順序、標誌位等格式,解析起來沒有歧義,實現簡單,而且體積小、速度快,做到“內部提效”;HTTP/2 把 TCP 協議的部分特性挪到了應用層,把原來的“Header+Body”的消息“打散”爲數個小片的二進制“幀”(Frame),用“HEADERS”幀存放頭數據、“DATA”幀存放實體數據;

【1.3】虛擬的“流”
針對消息的“碎片”到達目的地後的組裝問題,HTTP/2 爲此定義了一個“流”(Stream)的概念,它是二進制幀的雙向傳輸序列,同一個消息往返的幀會分配一個唯一的流 ID,可以把它想象成是一個虛擬的“數據流”,在裏面流動的是一串有先後順序的數據幀,這些數據幀按照次序組裝起來就是 HTTP/1 裏的請求報文和響應報文;因爲“流”是虛擬的,HTTP/2 就可以在一個 TCP 連接上用“流”同時發送多個“碎片化”的消息,這就是常說的“多路複用”( Multiplexing)多個往返通信都複用一個連接來處理;
在“流”的層面上看,消息是一些有序的“幀”序列,而在“連接”的層面上看,消息卻是亂序收發的“幀”,多個請求 / 響應之間沒有了順序關係,不需要排隊等待,也就不會再出現“隊頭阻塞”問題,降低了延遲,大幅度提高了連接的利用率;
HTTP/2 中服務器不再是完全被動地響應請求,也可以新建“流”主動向客戶端發送消息;比如,在瀏覽器剛請求 HTML 的時候就提前把可能會用到的 JS、CSS 文件發給客戶端,減少等待的延遲,稱之爲“服務器推送”(Server Push,也叫 Cache Push);

【1.4】強化安全
出於兼容的考慮,HTTP/2 延續了 HTTP/1 的“明文”特點,可以像以前一樣使用明文傳輸數據,不強制使用加密通信,不過格式還是二進制,只是不需要解密,但實際上 HTTP/2 是加密的,即互聯網上通常所能見到的 HTTP/2 都是使用“https”協議名,跑在 TLS 上面;
爲了區分“加密”和“明文”這兩個不同的版本,HTTP/2 協議定義了兩個字符串標識符:“h2”表示加密的 HTTP/2,“h2c”表示明文的 HTTP/2,多出的那個字母“c”的意思是“clear text”;加密版本的 HTTP/2 在安全方面做了強化,要求下層的通信協議必須是 TLS1.2 以上,還要支持前向安全和 SNI,並且把幾百個弱密碼套件列入了“黑名單”;

【1.5】協議棧

【2】HTTP/2內核剖析

【2.1】連接前言
HTTP/2 是基於 TLS 因此在正式收發數據之前,會有 TCP 握手和 TLS 握手,TLS 握手成功之後,客戶端必須要發送一個“連接前言”(connection preface),用來確認建立 HTTP/2 連接,該“連接前言”是標準的 HTTP/1 請求報文,使用純文本的 ASCII 碼格式,請求方法是特別註冊的一個關鍵字“PRI”,從而服務器便可知道客戶端在 TLS 上想要的是 HTTP/2 協議,此後將會使用 HTTP/2 的數據格式;

PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n

【2.2】頭部壓縮
確立了連接之後,HTTP/2 就開始準備請求報文,因爲語義上它與 HTTP/1 兼容,所以報文還是由“Header+Body”構成的,但在請求發送前,必須要用“HPACK”算法來壓縮頭部數據;“HPACK”算法是專門爲壓縮 HTTP 頭部定製的算法,它是一個“有狀態”的算法,需要客戶端和服務器各自維護一份“索引表”,壓縮和解壓縮就是查表和更新表的操作;爲了方便管理和壓縮,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”,那麼下一次發送的時候就不用再重複發那麼多字節了,只要用一個字節發送編號即可;

【2.3】二進制幀
幀長度(3 個字節),默認上限是 2^14,最大是 2^24,即 HTTP/2 的幀通常不超過 16K,最大是 16M;
幀類型(1 個字節),可以分成數據幀和控制幀兩類,HEADERS 幀和 DATA 幀屬於數據幀,存放的是 HTTP 報文,而 SETTINGS、PING、PRIORITY 等則是用來管理流的控制幀;HTTP/2 總共定義了 10 種類型的幀,但一個字節可以表示最多 256 種,所以也允許在標準之外定義其他類型實現功能擴展;
幀標誌,可以保存 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 億;

【2.4】流與多路複用
流是二進制幀的雙向傳輸序列
HTTP/2 的流的特點
1. 流是可併發的,一個 HTTP/2 連接上可以同時發出多個流傳輸數據,也就是併發多請求,實現“多路複用”;
2. 客戶端和服務器都可以創建流,雙方互不干擾;
3. 流是雙向的,一個流裏面客戶端和服務器都可以發送或接收數據幀,也就是一個“請求 - 應答”來回;
4. 流之間沒有固定關係,彼此獨立,但流內部的幀是有嚴格順序的;
5. 流可以設置優先級,讓服務器優先處理,比如先傳 HTML/CSS,後傳圖片,優化用戶體驗;
6. 流 ID 不能重用,只能順序遞增,客戶端發起的 ID 是奇數,服務器端發起的 ID 是偶數;
7. 在流上發送“RST_STREAM”幀可以隨時終止流,取消接收或發送;
8. 第 0 號流比較特殊,不能關閉,也不能發送數據幀,只能發送控制幀,用於流量控制;

HTTP/2 在一個連接上使用多個流收發數據,則其本身默認就會是長連接,永遠不需要“Connection”頭字段(keepalive 或 close);
下載大文件的時候想取消接收,在 HTTP/1 裏只能斷開 TCP 連接重新“三次握手”,成本很高,而在 HTTP/2 裏就可以簡單地發送一個“RST_STREAM”中斷流,而長連接會繼續保持;
客戶端和服務器兩端都可以創建流,而流 ID 有奇數偶數和上限的區分,所以大多數的流 ID 都會是奇數,而且客戶端在一個連接裏最多隻能發出 2^30,即 10 億個請求;

【2.5】流狀態轉換

1. 最開始的時候流都是“空閒”(idle)狀態,可以理解成是待分配的“號段資源”;
2. 當客戶端發送 HEADERS 幀後,有了流 ID,流就進入了“打開”狀態,兩端都可以收發數據;
3. 客戶端發送一個帶“END_STREAM”標誌位的幀,流就進入了“半關閉”狀態;這個“半關閉”狀態意味着客戶端的請求數據已經發送完了,需要接受響應數據,而服務器端也知道請求數據接收完畢,之後就要內部處理,再發送響應數據;響應數據發完了之後,也要帶上“END_STREAM”標誌位,表示數據發送完畢,這樣流兩端就都進入了“關閉”狀態,流就結束了;
流 ID 不能重用,所以流的生命週期就是 HTTP/1 裏的一次完整的“請求 - 應答”,流關閉就是一次通信結束;下一次再發請求就要開一個新流(而不是新連接),流 ID 不斷增加,直到到達上限,發送“GOAWAY”幀開一個新的 TCP 連接,流 ID 就又可以重頭計數;

【3】HTTP/3展望

【3.1】HTTP/3 協議棧

【3.2】QUIC 協議

QUIC 通常指 iQUIC,與早期的 gQUIC 不同,是一個傳輸層的協議,和 TCP 是平級;

【3.3】QUIC 的特點
QUIC 基於 UDP,而 UDP 是“無連接”的,根本就不需要“握手”和“揮手”,所以天生就要比 TCP 快;QUIC 基於 UDP 實現了可靠傳輸,保證數據一定能夠抵達目的地,同時引入了類似 HTTP/2 的“流”和“多路複用”,單個“流”是有序的,可能會因爲丟包而阻塞,但其他“流”不會受到影響;爲了防止網絡上的中間設備(Middle Box)識別協議的細節,QUIC 全面採用加密通信,可以很好地抵禦竄改和“協議僵化”(ossification);QUIC 就直接應用了 TLS1.3,獲得了 0-RTT、1-RTT 連接的優勢,但 QUIC 並不是建立在 TLS 之上,而是內部“包含”了 TLS,它使用自己的幀“接管”了 TLS 裏的“記錄”,握手消息、警報消息都不使用 TLS 記錄,直接封裝成 QUIC 的幀發送,省掉了一次開銷;

【3.4】QUIC 內部細節
QUIC 的基本數據傳輸單位是包(packet)和幀(frame),一個包由多個幀組成,包面向的是“連接”,幀面向的是“流”;
QUIC 使用不透明的“連接 ID”來標記通信的兩個端點,客戶端和服務器可以自行選擇一組 ID 來標記自己,這樣就解除了 TCP 裏連接對“IP 地址 + 端口”的強綁定,支持“連接遷移”(Connection Migration);

QUIC 的幀裏有多種類型,PING、ACK 等幀用於管理連接,STREAM 幀專門用來實現流;QUIC 裏的流與 HTTP/2 的流是幀的序列,但 HTTP/2 裏的流都是雙向的,而 QUIC 則分爲雙向流和單向流;
QUIC 幀普遍採用變長編碼,最少只要 1 個字節,最多有 8 個字節;流 ID 的最大可用位數是 62,數量上比 HTTP/2 的 2^31 大大增加;流 ID 還保留了最低兩位用作標誌,第 1 位標記流的發起者,0 表示客戶端,1 表示服務器;第 2 位標記流的方向,0 表示雙向流,1 表示單向流;

【3.5】HTTP/3 協議
幀頭只有兩個字段:類型和長度,而且同樣都採用變長編碼,最小隻需要兩個字節

HTTP/3 裏的幀仍然分成數據幀和控制幀兩類,HEADERS 幀和 DATA 幀傳輸數據,但其他一些幀因爲在下層的 QUIC 裏有了替代,所以在 HTTP/3 裏就都消失了,比如 RST_STREAM、WINDOW_UPDATE、PING 等;
頭部壓縮算法在 HTTP/3 裏升級成了“QPACK”,使用方式上也做了改變,雖然也分成靜態表和動態表,但在流上發送 HEADERS 幀時不能更新字段,只能引用,索引表的更新需要在專門的單向流上發送指令來管理,解決了 HPACK 的“隊頭阻塞”問題;QPACK 的字典也做了優化,靜態表由之前的 61 個增加到了 98 個,而且序號從 0 開始;

【3.6】HTTP/3 服務發現
使用 HTTP/2 裏的“擴展幀”,瀏覽器需要先用 HTTP/2 協議連接服務器,然後服務器可以在啓動 HTTP/2 連接後發送一個“Alt-Svc”幀,包含一個“h3=host:port”的字符串,告訴瀏覽器在另一個端點上提供等價的 HTTP/3 服務;瀏覽器收到“Alt-Svc”幀,會使用 QUIC 異步連接指定的端口,如果連接成功,就會斷開 HTTP/2 連接,改用新的 HTTP/3 收發數據;

參考致謝
本博客爲博主的學習實踐總結,並參考了衆多博主的博文,在此表示感謝,博主若有不足之處,請批評指正。

【1】透視HTTP協議

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章