tjhttp 二、《圖解HTTP》- HTTP協議歷史發展(重點)
知識點
- 請求和響應報文的結構。
- HTTP協議進化歷史,介紹不同HTTP版本從無到有的重大特性改變。(重點)
- HTTP幾個比較常見的問題討論。
2.0 介紹
這一章節基本上大部分為個人擴展,因為書中的內容講的實在是比較淺。本文內容非常長,另外哪怕這麼長也只是講到了HTTP協議的一部分而已,HTTP協議本身十分複雜。
2.1 請求和響應報文結構
請求報文的基本內容:
請求內容需要客户端發給服務端:
GET /index.htm HTTP/1.1
Host: hackr.jp
響應報文的基本內容:
服務器按照請求內容處理結果返回:
開頭部分是HTTP協議版本,緊接着是狀態碼 200 以及原因短語。
下一行則包含了創建響應的日期時間,包括了首部字段的屬性。
HTTP/1.1 200 OK
Date: Tue, 10 Jul 2012 06:50:15 GMT
Content-Length: 362
Content-Type: text/html
<html> ……
2.2 HTTP進化歷史
| 協議版本 | 解決的核心問題 | 解決方式 |
|---|---|---|
| 0.9 | HTML 文件傳輸 | 確立了客户端請求、服務端響應的通信流程 |
| 1.0 | 不同類型文件傳輸 | 設立頭部字段 |
| 1.1 | 創建/斷開 TCP 連接開銷大 | 建立長連接進行復用 |
| 2 | 併發數有限 | 二進制分幀 |
| 3 | TCP 丟包阻塞 | 採用 UDP 協議 |
| SPDY | HTTP1.X的請求延遲 | 多路複用 |
2.2.1 概覽
我們覆盤HTTP的進化歷史,下面是拋去所有細節,整個HTTP連接大致的進化路線。
注意:有關協議的升級內容挑了具備代表性的部分,完整內容需要閲讀RFC原始協議瞭解
- http0.9:只具備最基礎的HTTP連接模型,在非常短的一段時間內存在,後面被快速完善。
- http1.0: 1.0版本中每個TCP連接只能發送一個請求,數據發送完畢連接就關閉,如果還要請求其他資源,就必須重新建立TCP連接。(TCP為了保證正確性和可靠性需要客户端和服務器三次握手和四次揮手,因此建立連接成本很高)
-
http1.1:
- 長連接:新增Connection字段,默認為keep-alive,保持連接不斷開,即 TCP 連接默認不關閉,可以被多個請求複用;
- 管道化:在同一個TCP連接中,客户端可以發送多個請求,但響應的順序還是按照請求的順序返回,在服務端只有處理完一個迴應,才會進行下一個迴應;
- host字段:Host字段用來指定服務器的域名,這樣就可以將多種請求發往同一台服務器上的不同網站,提高了機器的複用,這個也是重要的優化;
-
HTTP/2:
- 二進制格式:1.x是文本協議,然而2.0是以二進制幀為基本單位,可以説是一個二進制協議,將所有傳輸的信息分割為消息和幀,並採用二進制格式的編碼,一幀中包含數據和標識符,使得網絡傳輸變得高效而靈活;
- 多路複用:2.0版本的多路複用多個請求共用一個連接,多個請求可以同時在一個TCP連接上併發,主要藉助於二進制幀中的標識進行區分實現鏈路的複用;
- 頭部壓縮:2.0版本使用使用HPACK算法對頭部header數據進行壓縮,從而減少請求的大小提高效率,這個非常好理解,之前每次發送都要帶相同的header,顯得很冗餘,2.0版本對頭部信息進行增量更新有效減少了頭部數據的傳輸;
- 服務端推送:在2.0版本允許服務器主動向客户端發送資源,這樣在客户端可以起到加速的作用;
- HTTP/3:
這個版本是劃時代的改變,在HTTP/3中,將棄用TCP協議,改為使用基於UDP協議的QUIC協議實現。需要注意QUIC是谷歌提出的(和2.0 的SPDY 一樣),QUIC指的是快速 UDP Internet 連接,既然使用了UDP,那麼也意味着網絡可能存在丟包和穩定性下降。谷歌當然不會讓這樣的事情發生,所以他們提出的QUIC既可以保證穩定性,又可以保證SSL的兼容,因為HTTP3上來就會和TLS1.3一起上線。
基於這些原因,制定網絡協議IETF的人馬上基本都同意了QUIC的提案(太好了又能白嫖成果),於是HTTP3.0 就這樣來了。但是這只是最基本的草案,後續的討論中希望QUIC可以兼容其他的傳輸協議,最終的排序如下IP / UDP / QUIC / HTTP。另外TLS有一個細節優化是在進行連接的時候瀏覽器第一次就把自己的密鑰交換的素材發給服務器,這樣進一步縮短了交換的時間。
為什麼HTTP3.0要從協議根本上動刀,那是因為HTTP/2雖然解決了HTTP協議無法多路複用的問題,但是沒有從TCP層面解決問題,具體的TCP問題體現如下:
- 隊頭阻塞,
HTTP/2多個請求跑在一個TCP連接中,如果此時序號較低的網絡請求被阻塞,那麼即使序列號較高的TCP段已經被接收了,應用層也無法從內核中讀取到這部分數據,從 HTTP 視角看就是多個請求被阻塞了,並且頁面也只是加載了一部分內容; TCP和TLS握手時延縮短:TCL三次握手和TLS四次握手,共有3-RTT的時延,HTPT/3最終壓縮到1 RTT(難以想象有多快);- 連接遷移需要重新連接,移動設備從
4G網絡環境切換到WIFI時,由於TCP是基於四元組來確認一條TCP連接的,那麼網絡環境變化後,就會導致IP地址或端口變化,於是TCP只能斷開連接,然後再重新建立連接,切換網絡環境的成本高;
RTT:RTT是Round Trip Time的縮寫,簡單來説就是通信一來一回的時間。
下面是官方對於RTT速度縮短的對比,最終只在初次連接需要1RTT的密鑰交換,之後的連接均為0RTT!
2.2.2 HTTP 0.9
這個版本基本就是草稿紙協議,但是它具備了HTTP最原始的基礎模型,比如只有GET命令,沒有 Header 信息,傳達的目的地也十分簡單,沒有多重數據格式,只有最簡單的文本。
此外服務器一次建立發送請求內容之後就會立馬關閉TCP連接,這時候的版本一個TCP還只能發送一個HTTP請求,採用一應一答的方式。
當然在後面的版本中對於這些內容進行升級改進。
2.2.3 HTTP 1.0
協議原文:https://datatracker.ietf.org/doc/html/rfc1945
顯然HTTP 0.9缺陷非常多並且不能滿足網絡傳輸要求。瀏覽器現在需要傳輸更為複雜的圖片,腳本,音頻視頻數據。
1996年HTTP進行了一次大升級,主要的更新如下:
- 增加更多請求方法:POST、HEAD
- 添加Header 頭部支持更多的情況變化
- 第一次引入協議版本號的概念
- 傳輸不再限於文本數據
- 添加響應狀態碼
在HTTP1.0 協議原文中開頭有一句話:
原文:
Status of This Memo:
This memo provides information for the Internet community. This memo
does not specify an Internet standard of any kind. Distribution of
this memo is unlimited.
這份協議用了memo這個單詞,memo 的意思是備忘錄,也就是説雖然洋洋灑灑寫了一大堆看似類似標準的規定,但是實際上還是草稿,沒有規定任何的協議和標準,另外這份協議是在麻省理工的一個分校起草的,所以可以認為是討論之後臨時的一份方案。
HTTP1.0主要改動點介紹
在瞭解了這是一份備忘錄的前提下,我們來介紹協議的一些重要概念提出。
HTTP1.0 定義了無狀態、無連接的應用層協議,紙面化定義了HTTP協議本身。
無狀態、無連接定義:HTTP1.0 規定服務器和客户端之間可以保持短暫連接,每次請求都需要發起一次新的TCP連接(無連接),連接完成之後立馬斷開連接,同時服務器不負責記錄過去的請求(無狀態)。
這樣就出現一個問題,那就是通常一次訪問需要多個HTTP請求,所以每一次請求都要建立一次TCP連接效率非常低,此外還存在兩個比較嚴重的問題:隊頭阻塞和無法複用連接。
隊頭阻塞:因為TCP連接是類似排隊的方式發送,如果前一個請求沒有到達或者丟失,後一個請求就需要等待前面的請求完成或者完成重傳才能進行請求。
無法複用連接:TCO連接資源本身就是有限的,同時因為TCP自身調節(滑動窗口)的關係,TCP為了防止網絡擁堵會有一個慢啓動的過程。
RTT時間計算:TCP三次握手共計需要至少1.5個RTT,注意是HTTP連接不是HTTPS連接。
滑動窗口:簡單理解是TCP 提供一種可以讓「發送方」根據「接收方」的實際接收能力控制發送的數據量的機制。
2.2.4 HTTP1.1
HTTP 1.1 的升級改動較大,主要的改動點是解決建立連接和傳輸數據的問題,在HTTP1.1中引入了下面的內容進行改進:
- 長連接:也就是
Keep-alive頭部字段,讓TCP默認不進行關閉,保證一個TCP可以傳遞多個HTTP請求 - 併發連接:一個域名允許指定多個長連接(注意如果超出上限依然阻塞);
- 管道機制:一個TCP可以同時發送多個請求(但是實際效果很雞肋還會增加服務器壓力,所以通常被禁用);
- 增加更多方法:PUT、DELETE、OPTIONS、PATCH等;
- HTTP1.0新增緩存字段(If-Modified-Since, If-None-Match),而HTTP1.1則引入了更多字段,比如
Entity tag,If-Unmodified-Since, If-Match, If-None-Match等更多緩存頭部的緩存策略。 - 允許數據分塊傳輸(Chunked),對於大數據傳輸很重要;
- 強制使用Host頭部,為互聯網的主機託管創造條件;
- 請求頭中引入了 支持斷點續傳的
range字段;
下面為書中第二章節記錄的筆記內容,寫書日期是HTTP1.1蓬勃發展的時候 ,基本對應了HTTP1.1協議中的一些顯著特點。
無狀態協議
HTTP 協議自身不具備保存之前發送過的請求或響應的功能,換句話説HTTP協議本身只保證協議報文的格式符合HTTP的要求,除此之外的傳輸和網絡通信其實都是需要依賴更下層的協議完成。
HTTP設計成如此簡單的形式,本質上就是除開協議本身外的內容一切都不考慮,達到高速傳輸的效果。但是因為HTTP的簡單粗暴,協議本身需要很多輔助的組件來完成WEB的各種訪問效果,比如保持登陸狀態,保存近期的瀏覽器訪問信息,記憶密碼等功能,這些都需要Cookie以及Session來完成。
HTTP/1.1引入了Cookie以及Session協助HTTP完成狀態存儲等操作。
請求資源定位
HTTP大多數時候是通過URL的域名來訪問資源的,定位URL要訪問的真實服務需要DNS的配合,DNS是什麼這裏不再贅述。
如果是對服務器訪問請求,可以通過*的方式發起請求,比如OPTIONS * HTTP/1.1。
請求方法
實際上用的比較多的還是GET和POST。
GET:通常視為無需要服務端校驗可以直接通過URL公開訪問的資源,但是通常會在URL中攜帶大量的請求參數,但是這些參數通常無關敏感信息所以放在URL當中非常方便簡單。
POST:通常情況為表單提交的參數,需要服務端的攔截校驗才能獲取,比如下載文件或者訪問一些敏感資源,實際上POST請求要比GET請求使用更為頻繁,因為POST請求對於請求的數據進行“加密”保護。相比於GET請求要安全和靠譜很多。
持久連接
所謂的持久連接包含一定的歷史原因,HTTP1.0最早期每次訪問和響應都是一些非常小的資源交互,所以一次請求結束之後基本就可以和服務端斷開,等到下一次需要再次請求再次連接。
但是隨着互聯網發展,資源包越來越大,對於互聯網的需求和挑戰也越來越大。
在後續 HTTP/1.1 中所有的連接默認都是持久連接,目的是減少客户端和服務端的頻繁請求連接和響應。
支持HTTP1.1需要雙方都能支持持久連接才能完成通信。
管道化
注意HTTP真正意義上的全雙工的協議是在HTTP/2才實現的,實現的核心是多路複用。
管道化可以看做是為了讓半雙工的HTTP1.1也能支持全雙工協議的一種強化,通俗的話説就是圍魏救趙。
全雙工協議:指的是HTTP連接的兩端不需要等待響應數據給對方就可以直接發送請求給對方,實現同一時間內同時處理多個請求和響應的功能。
HTTP/1.1允許多個http請求通過一個套接字同時被輸出 ,而不用等待相應的響應(這裏提示一下管道化同樣需要連接雙方都支持才能完成)。
需要注意這裏本質上是在一個TCP請求封裝了多次請求然後直接丟給服務端去處理,客户端接下來可以幹別的事情,要麼等待服務端慢慢等待,要麼自己去訪問別的資源。
客户端通過FIFO隊列把多個TCP請求封裝成一個發給了服務端,服務端雖然可以通過處理FIFO隊列的多個請求,但是必須等所有請求完成再按照FIFO發送的順序挨個響應回去,也就是説其實並沒有根本上解決堵塞問題。
管道化的技術雖然很方便,但是限制和規矩比好處要多得多,並且有點脱褲子放屁的意思。結果是並沒有十分普及也沒有多少服務端使用,多數的HTTP請求也會禁用管道化防止服務端請求堵塞遲遲不進行響應。
管道化小結
- 實際上管道化可以看做原本阻塞在客户端一條條處理的請求,變為阻塞在服務端的一條條請求。
- 管道化請求通常是GET和HEAD請求,POST和PUT不需要管道化, 管道化只能利用已存在的
keep-alive連接。 - 管道化是HTTP1.1協議下,服務器不能很好處理並行請求的改進,但是這個方案不理想,圍魏救趙失敗並且最終被各大瀏覽器禁用掉。
- FIFO隊列的有序和TCP 的有序性區別可以簡單認為是強一致性和弱一致性的區別。FIFO隊列有序性指的是請求和響應必須按照隊列發送的規則完全一樣,而TCP僅僅是保證了發送和響應的大致邏輯順序,真實的情況和描述的情況可能不一致。
- 因為管道是把累贅丟給了服務端,從客户端的角度來看自己完成了全雙工的通信。實際上這只是偽全雙工通信。
Cookie
Cookie的內容不是本書重點,如果需要了解相關知識可以直接往上查詢資料瞭解,基本一抓一大把。
2.2.5 HTTP/2](https://www.ietf.org/archive/...))
HTTP2的協議改動比較大,從整體上來看主要是下面一些重要調整:
- SPDY:這個概念是谷歌提出的,起初是希望作為一個獨立協議,但是最終SPDY的相關技術人員參與到了HTTP/2,所以谷歌瀏覽器後面全面支持HTTP/2放棄了SPDY單獨成為協議的想法,對於SPDY,具有如下的改進點:
- HTTP Speed + Mobility:微軟提出改善移動端通信的速度和性能標準,它建立在 Google 公司提出的 SPDY與 WebSocket 的基礎之上。
- Network-Friendly HTTP Upgrade:移動端通信時改善 HTTP 性能。
從三者的影響力來看,顯然是Google的影響力是最大的,從HTTP3.0開始以谷歌發起可以看出HTTP協議的標準制定現在基本就是谷歌説了算。
接着我們就來看看最重要SPDY,谷歌是一個極客公司,SPDY可以看做是HTTP1.1和HTTP/2正式發佈之間谷歌弄出來的一個提高HTTP協議傳輸效率的“玩具” ,重點優化了HTTP1.X的請求延遲問題,以及解決HTTP1.X的安全性問題:
-
降低延遲(多路複用):使用多路複用來降低高延遲的問題,多路複用指的是使用Stream讓多個請求可以共享一個TCP連接,解決HOL Blocking(head of line blocking)(隊頭阻塞)的問題,同時提升帶寬利用率。
- HTTP1.1中
keep-alive用的是http pipelining本質上也是multiplexing,但是具體實現方案不理想 。 - 主流瀏覽器都默認禁止
pipelining,也是因為HOL阻塞問題導致。
- HTTP1.1中
- 服務端推送:HTTP1.X的推送都是半雙工,所以在2.0是實現真正的服務端發起請求的全雙工,另外在WebSocket在這全雙工一塊大放異彩。
- 請求優先級:針對引入多路複用的一個兜底方案,多路複用使用多個Stream的時候容易單請求阻塞問題。也就是前文所説的和管道連接一樣的問題,SPDY通過設置優先級的方式讓重要請求優先處理,比如頁面的內容應該先進行展示,之後再加載CSS文件美化以及加載腳本互動等等,實際減少用户不會在等待過程中關閉頁面到機率。
- Header壓縮:HTTP1.X的header很多時候都是多餘的,所以2.0 會自動選擇合適的壓縮算法自動壓縮請求加快請求和響應速度。
- 基於HTTPS的加密協議傳輸:HTTP1.X 本身是不會加入SSL加密的,而2.0 讓HTTP自帶SSL,從而提高傳輸可靠和穩定性。
這些內容在後續大部分都被HTTP/2 採納,下面就來看看HTTP/2具體的實施細節。
HTTP/2具體實施(重點)
當然這一部分也只講到了協議中一些重點的升級內容,詳細內容請參考“參考資料”活着點擊HTTP/2的標題。
二進制幀(Stream)
HTTP/2 使用流(二進制)替代 ASCII 編碼傳輸提升傳輸效率,客户端發送的請求都會封裝為帶有編號的二進制幀,然後再發送給服務端處理。
HTTP/2 通過 一個TCP連接完成多次請求操作,服務端接受流數據並且檢查編號將其合併為一個完整的請求內容,這樣同樣需要按照二進制幀的拆分規則拆分響應。像這樣利用二進制分幀 的方式切分數據,客户端和服務端只需要一個請求就可以完成通信,也就是SPDY提到的多個Stream 合併到一個TCP連接中完成。
二進制分幀把數據切分成更小的消息和幀,採用了二進制的格式進行編碼,在HTTP1.1 當中首部消息封裝到Headers當中,然後把Request body 封裝到 Data幀。
使用二進制分幀目的是向前兼容,需要在應用層和傳輸層之間加一層二進制分幀層,讓HTTP1.X 協議更加簡單的升級同時不會對過去的協議產生衝突 。
幀、消息、Stream之間的關係
- 幀:可以認為是流當中的最小單位。
- 消息:表示HTTP1.X中的一次請求。
- Stream:包含1條或者多條message。
二進制分幀結構
二進制分幀結構主要包含了頭部幀和數據幀兩個部分,頭部在幀數只有9個字節,注意R屬於標誌位保留。所以整個算下來是:
3個字節幀長度+1個字節幀類型 + 31bit流標識符、1bit未使用標誌位 構成。
幀長度:數據幀長度,24位的3字節大小,取值為 2^14(16384) - 2^24(1677215)之間,接收方的 SETTINGS_MAS_FRAM_SIZE 設置。
幀類型:分辨數據幀還是控制幀。
標誌位:攜帶簡單控制信息,標誌位表示流的優先級。
流標識符:表示幀屬於哪一個流的,上限為2的31次方,接收方需要根據流標識的ID組裝還原報文,同一個Stream的消息必須是有序的。此外客户端和服務端分別用奇數和偶數標識流,併發流使用了標識才可以應用多路複用。
R:1位保留標誌位,暫未定義,0x0為結尾。
幀數據:實際傳輸內容由幀類型指定。
如果想要知道更多細節,可以參考“參考資料”部分的官方介紹以及結合WireShark抓包使用,本讀書筆記沒法面面俱到和深入
最後是補充幀類型的具體內容,幀類型定義了10種類型的幀數:
多路複用 (Multiplexing)
有了前面二進制幀結構的鋪墊,現在再來看看多路複用是怎麼回事,這裏首先需要説明在過去的HTTP1.1中存在的問題:
同一時間同一域名的請求存在訪問限制,超過限制的請求會自動阻塞。
在傳統的解決方案中是利用多域名訪問以及服務器分發的方式讓資源到特定服務器加載,讓整個頁面的響應速度提升。比如利用多個域名的CDN進行訪問加速
隨着HTTP/2的更新,HTTP2改用了二進制幀作為替代方案,允許單一的HTTP2請求複用多個請求和響應內容,也就是説可以一個包裏面打包很多份“外賣”一起給你送過來。
此外,流控制數據也意味着可以支持多流並行而不過多依賴TCP,因為通信縮小為一個個幀,幀內部對應了一個個消息,可以實現並行的交換消息。
Header壓縮(Header Compression)
HTTP1.X 不支持Header壓縮,如果頁面非常多的去看下會導致帶寬消耗和不必要的浪費。
針對這個問題在SPDY中 的解決方案是利用DEFLATE 格式的字段,這種設計非常有效,但是實際上存在CRIME信息泄露的攻擊手段。
在HTTP/2 當中定義了HPACK,HPACK算法通過靜態的哈夫曼編碼對於請求頭部進行編碼減少傳輸大小,但是需要讓客户端和服務端之間維護首部表,首部表可以維護和存儲之前發過的鍵值對信息,對於重複發送的報文內容可以直接通過查表獲取,減少冗餘數據產生,後續的第二個請求將會發送不重複的數據。
HPACK壓縮算法主要包含兩個模塊,索引表和哈夫曼編碼,索引表同時分為動態表和靜態表,靜態表內部預定義了61個Header的K/V 數值,而動態表是先進先出的隊列,初始情況下內容為空,而解壓header則需要每次添加的時候放到隊頭,移除從隊尾開始。
注意動態表為了防止過度膨脹佔用內存導致客户端崩潰,在超過一定長度過後會自動釋放HTTP/2請求。
HPACK算法
HPACK算法是對於哈夫曼算法的一種應用和改進,哈夫曼算法經典案例是就是ZIP壓縮,也就是雖然我們可能不清楚卻是可能天天在用的一個東西。
HPACK算法的思路是在客户端和服務端兩邊各維護一個哈希表,然後雙端通過表中緩存Headers字段減少流中二進制數據傳輸,進而提高傳輸效率。
HPACK三個主要組件有如下細節:
- 靜態表:HTTP2為出現在頭部的字符串和字段靜態表,包含61個基本的headers內容,
- 動態表:靜態表只有61個字段,所以利用動態表存儲不在靜態表的字段,從62開始進行索引,在傳輸沒有出現的字段時候,首先對於建立索引號,然後字符串需要經過哈夫曼編碼完成二進制轉化發給服務器,如果是第二次發送則找到對應的動態表的索引找到即可,這樣有效避免一些冗餘數據的傳輸。
- 哈夫曼編碼:這一算法非常重要,對於近代互聯網的發展有着重大影響。
哈夫曼編碼:是一種用於無損數據壓縮的熵編碼(權編碼)算法。由美國計算機科學家大衞·霍夫曼(David Albert Huffman)在1952年發明。 霍夫曼在1952年提出了最優二叉樹的構造方法,也就是構造最優二元前綴編碼的方法,所以最優二叉樹也別叫做霍夫曼樹,對應最優二元前綴碼也叫做霍夫曼編碼。
下面為哈夫曼編碼對應的原始論文:
哈夫曼編碼原始論文:
鏈接:https://pan.baidu.com/s/1r_yO...
提取碼:694k
此外這裏有個講的比較通俗的霍夫曼的視頻,強烈建議反覆觀看,能幫你快速瞭解哈夫曼編碼是怎麼回事,當然前提是得會使用魔法。
https://www.youtube.com/watch?v=Jrje7ep5Ff8&t=29s
請求優先級
請求優先級實際上並不是HTTP/2才出現的,在此之前的的RFC7540中定義了一套優先級的相關指令,但是由於它過於複雜最後並沒用被普及,但是裏面的信息依然是值得參考的。
HTTP/2的內容取消了所有關於RFC7540優先級的指令,所有的描述被刪除並且被保留在原本的協議當中。
HTTP/2利用多路複用,所以有必要優先使用重要的資源分配到前面優先加載,但是實際上在實現方案過程中優先級是不均衡的,許多服務器實際上並不會觀察客户端的請求和行為。
最後還有根本性的缺點,也就是TCP層是無法並行的,在單個請求當中的使用優先級甚至有可能性能弱於HTTP1.X。
流量控制
所謂流量控制就是數據流之間的競爭問題,需要注意HTTP2只有流數據才會進行控制,通過使用WINDOW_UPDATE幀來提供流量控制。
注意長度不是4個八位字節的window_update 幀需要視為 frame_size_error的錯誤進行響應。
PS:下面的設計中有效載荷是保留位+ 31位的無符號整數,表示除了現在已經有的流控制窗口之外還能額外傳輸8個字節數的數據,所以最終合法範圍是 1到 2^31 - 1 (2,147,483,647) 個八位字節。
WINDOW_UPDATE Frame {
Length (24) = 0x04,
Type (8) = 0x08,
Unused Flags (8),
Reserved (1),
Stream Identifier (31),
Reserved (1),
Window Size Increment (31),
}
對於流量控制,存在下面幾個顯著特徵:
- 流量控制需要基於HTTP中間的各種代理服務器控制,不是非端到端的控制;
- 基於信用基礎公佈每個流在每個連接上接收了多少字節,WINDOW_UPDATE 框架沒有定義任何標誌,並沒有強制規定;
- 流量的控制存在方向概念,接收方負責流量控制,並且可以設置每一個流窗口的大小;
WINDOW_UPDATE可以對於已設置了END_STREAM標誌的幀進行發送,表示接收方這時候有可能進入了半關閉或者已經關閉的狀態接收到WINDOW_UPDATE幀,但是接收者不能視作錯誤對待;- 接收者必須將接收到流控制窗口增量為 0 的
WINDOW_UPDATE幀視為PROTOCOL_ERROR類型的流錯誤 ;
服務器推送
服務器推送意圖解決HTTP1.X中請求總是從客户端發起的弊端,服務端推送的目的是更少客户端的等待以及延遲。但是實際上服務端推送很難應用,因為這意味着要預測用户的行為。服務端推送包含推送請求和推送響應的部分。
推送請求
推送請求使用PUSH_PROMISE 幀作為發送,這個幀包含字段塊,控制信息和完整的請求頭字段,但是不能攜帶包含消息內容的相關信息,因為是指定的幀結構,所以客户端也需要顯式的和服務端進行關聯,所以服務端推送 請求也叫做“Promised requests”。
當請求客户端接收之後是傳送CONTINUATION幀,CONTINUATION幀頭字段必須是一組有效的請求頭字段,服務器必須通過":method"偽字段頭部添加安全可緩存的方法,如果客户端收到的緩存方法不安全則需要在PUSH_PROMISE幀上響應錯誤,這樣的設計有點類似兩個特務對暗號,一個暗號對錯了就得立馬把對方弊了。
PUSH_PROMISE可以在任意的客户端和服務端進行傳輸,但是有個前提是流對於服務器需要保證“半關閉“或者“打開“的狀態,否則不允許通過CONTINUATION或者HEADERS 字段塊傳輸。
PUSH_PROMISE幀只能通過服務端發起,因為專為服務端推送設計,使用客户端推送是“不合法“的。
PUSH_PROMISE 幀結構:
再次強調有效載荷是一個保留位+ 31位的無符號整數。有效載荷是什麼?是對於HTTP1.1協議中實體的術語重新定義,可以簡單看做是報文的請求Body。
下面是對應得源代碼定義:
PUSH_PROMISE幀定義
PUSH_PROMISE Frame {
Length (24),
Type (8) = 0x05,
Unused Flags (4),
PADDED Flag (1),
END_HEADERS Flag (1),
Unused Flags (2),
Reserved (1),
Stream Identifier (31),
[Pad Length (8)],
Reserved (1),
Promised Stream ID (31),
Field Block Fragment (..),
Padding (..2040),
}
CONTINUATION 幀:用於請求接通之後繼續傳輸,注意這個幀不是專用於服務端推送的。
CONTINUATION Frame {
Length (24),
Type (8) = 0x09,
Unused Flags (5),
END_HEADERS Flag (1),
Unused Flags (2),
Reserved (1),
Stream Identifier (31),
Field Block Fragment (..),
}
推送響應
如果客户端不想接受請求或者服務器發起請求的時間過長,可以通過RST_STREAM 幀代碼標識發送CANCEL 或者REFUSED_STREAM 內容告訴服務器自己不接受服務端請求推送。
而如果客户端需要接收這些響應信息,則需要按照之前所説傳遞CONTINUATION以及PUSH_PROMISE接收服務端請求。
其他特點:
- 客户端可以使用SETTINGS_MAX_CONCURRENT_STREAMS設置來限制服務器可以同時推送的響應數量。
- 如果客户端不想要接收服務端的推送流,可以把SETTINGS_MAX_CONCURRENT_STREAMS設置為0或者重置
PUSH_PROMISE保留流進行處理。
2.2.6 HTTP/3
進度追蹤:RFC 9114 - HTTP/3 (ietf.org)
為什麼會存在3?
可以發現HTTP/2雖然有了質的飛躍,但是因為TCP協議本身的缺陷,隊頭阻塞的問題依然可能存在,同時一旦出現網絡擁堵會比HTTP1.X情況更為嚴重(用户只能看到一個白板)。
所以後續谷歌的研究方向轉為研究QUIC,實際上就是改良UDP協議來解決TCP協議自身存在的問題。但是現在看來這種改良不是很完美,目前國內部分廠商對於QUIC進行自己的改進。
HTTP/3 為什麼選擇UDP
這就引出另一個問題,為什麼3.0有很多協議可以選擇,為什麼使用UDP?通常有下面的幾個點:
- 基於TCP 協議的設備很多,兼容十分困難。
- TCP是Linux內部的重要組成,修改非常麻煩,或者説壓根不敢動。
- UDP本身無連接的,沒有建立連接和斷連的成本。
- UDP數據包本身就不保證穩定傳輸所以不存在阻塞問題(屬於愛要不要)。
- UDP改造相對其他協議改造成本低很多
HTTP/3 新特性
- QUIC(無隊頭阻塞):優化多路複用,使用QUIC協議代替TCP協議解決隊頭阻塞問題,QUIC也是基於流設計但是不同的是一個流丟包只會影響這一條流的數據重傳,TCP 基於IP和端口進行連接,多變的移動網絡環境之下十分麻煩,QUIC通過ID識別連接,只要ID不變,網絡環境變化是可以迅速繼續連接的。
- 0RTT:注意建立連接的0TT在HTTP/3上目前依然沒有實現,至少需要1RTT。
RTT:RTT是Round Trip Time的縮寫,簡單來説就是通信一來一回的時間。 RTT包含三部分:
- 往返傳播延遲。
- 網絡設備排隊延遲。
- 應用程序處理延遲。
HTTPS建立完整連接通常需要TCP握手和TLS握手,至少要2-3個RTT,普通的HTTP也至少要1個RTT。QUIC的目的是除開初次連接需要消耗1RTT時間之外,其他的連接可以實現0RTT。
為什麼無法做到初次交互0RTT? 因為初次傳輸説白了依然需要傳輸兩邊到密鑰信息,因為存在數據傳輸所以依然需要1個RTT的時間完成動作,但是在完成握手之後的數據傳輸只需要0RTT的時間。
-
前向糾錯:QUIC的數據包除了本身的內容之外,還允許攜帶其他數據包,在丟失一個包的時候,通過攜帶其他包的數據獲取到丟包內容。
具體要怎麼做呢?例如3個包丟失一個包,可以通過其他數據包(實際上是校驗包)異或值計算出丟失包的“編號”然後進行重傳,但是這種異或操作只能針對一個數據包丟失計算,如果多個包丟失,用異或值是無法算出一個以上的包的,所以這時候還是需要重傳(但是QUIC重傳代價比TCP的重傳低很多)。
- 連接遷移:QUIC放棄了TCP的五元組概念,使用了64位的隨機數ID充當連接ID,QUIC 協議在切換網絡環境的時候只要ID一致就可以立馬重連。對於現代社會經常wifi和手機流量切換的情況十分好用的一次改進。
術語解釋⚠️:
5元組:是一個通信術語,英文名稱為five-tuple,或5-tuple,通常指由源Ip (source IP), 源端口(source port),目標Ip (destination IP), 目標端口(destination port),4層通信協議 (the layer 4 protocol)等5個字段來表示一個會話,是會話哦。
這個概念在《網絡是怎麼樣連接的》這本書中也有提到類似的概念。那就是在第一章中創建套接字的步驟,創建套接字實際上就需要用到這個五元祖的概念,因為要創建“通道”需要雙方給自告知自己的信息給對方自己的IP和端口,這樣才能完成通道創建和後續的協議通信。順帶拓展一下4元組和7元組。
4元組:即用4個維度來確定唯一連接,這4個維度分別是源Ip (source IP), 源端口(source port),目標Ip (destination IP), 目標端口(destination port)。
7元組:即用7個字段來確定網絡流量,即源Ip (source IP), 源端口(source port),目標Ip (destination IP), 目標端口(destination port),4層通信協議 (the layer 4 protocol),服務類型(ToS byte),接口索引(Input logical interface (ifIndex))
- 加密認證的報文:
QUIC默認會對於報文頭部加密,因為TCP頭部公開傳輸,這項改進非常重要。 - 流量控制,傳輸可靠性:
QUIC在UDP協議上加了一層數據可靠傳輸的可靠性傳輸,因此流量控制和傳輸可靠性都可以得到保證。 -
幀格式變化
下面是網上資料對比HTTP2和3之間的格式差距,可以發現
HTTP/3幀頭只有兩個字段:類型和長度。幀類型用來區分數據幀和控制幀,這一點是繼承自HTTP/2的變化,數據幀包含HEADERS幀,DATA幀,HTTP包體。 -
關於2.0的頭部壓縮算法升級成了
QPACK算法:需要注意HTTP3的QPACK算法與HTTP/2中的HPACK編碼方式相似,HTTP/3中的QPACK也採用了靜態表、動態表及Huffman編碼。那麼相對於之前的算法
HPACK,QPACK算法有什麼升級呢?首先HTTP/2中的HPACK的靜態表只有 61 項,而HTTP/3中的QPACK的靜態表擴大到 91 項。最大的區別是對於動態表做了優化,因為在HTTP2.0中動態表存在時序性的問題。
所謂時序性問題是在傳輸的時候如果出現丟包,此時一端的動態表做了改動,但是另一端是沒改變的,同樣需要把編碼重傳,這也意味着整個請求都會阻塞掉。
因此HTTP3使用UDP的高速,同時保持QUIC的穩定性,並且沒有忘記TLS的安全性,在2018年的YTB直播中宣佈QUIC作為HTTP3的標準。
YTB 地址:(2) IETF103-HTTPBIS-20181108-0900 - YouTube,可憐互聯網的天花板協議制定團隊IETF連1萬粉絲都沒有。
2.3 HTTP部分問題討論
2.3.1 隊頭阻塞問題(head of line blocking)
隊頭阻塞問題不僅僅只是處在HTTP的問題,實際上更加底層的協議以及網絡設備通信也會存在線頭阻塞問題。
交換機
當交換機使用FIFO隊列作為緩衝端口的緩衝區的時候,按照先進先出的原則,每次都只能是最舊的網絡包被髮送,這時候如果交換機輸出端口存在阻塞,則會發生網絡包等待進而造成網絡延遲問題。
但是哪怕沒有隊頭阻塞,FIFO隊列緩衝區本身也會卡住新的網絡包,在舊的網絡包後面排隊發送,所以這是FIFO隊列本身帶來的問題。
有點類似核酸排隊,前面的人不做完後面的人做不了,但是前面的人一直不做,後面也只能等着。
交換機HO問題解決方案
使用虛擬輸出隊列的解決方案,這種方案的思路是隻有在輸入緩衝區的網絡包才會出現HOL阻塞,帶寬足夠的時候不需要經過緩衝區直接輸出,這樣就避免HOL阻塞問題。
無輸入緩衝的架構在中小型的交換機比較常見。
線頭阻塞問題演示
交換機:_交換機根據 MAC 地址表查找 MAC 地址, 然後將信號發送到相應的端口_一個網絡信號轉接設備,有點類似電話局中轉站。
線頭阻塞示例:第 1 和第 3 個輸入流競爭時,將數據包發送到同一輸出接口,在這種情況下如果交換結構決定從第 3 個輸入流傳輸數據包,則無法在同一時隙中處理第 1 個輸入流。
請注意,第一個輸入流阻塞了輸出接口 3 的數據包,該數據包可用於處理。
無序傳輸:
因為TCP不保證網絡包的傳輸順序,所以可能會導致亂序傳輸,HOL阻塞會顯著的增加數據包重新排序問題。
同樣為了保證有損網絡可靠消息傳輸,原子廣播算法雖然解決這個問題,但是本身也會產生HOL阻塞問題,同樣是由於無序傳輸帶來的通病。
Bimodal Multicast 算法是一種使用 gossip 協議的隨機算法,通過允許亂序接收某些消息來避免線頭阻塞。
HTTP線頭阻塞
HTTP 在 2.0 通過多路複用的方式解決了HTTP協議的弱點並且真正意義上消除應用層HOL阻塞問題,但是TCP協議層的無序傳輸依然是無法解決的。
於是在3.0中直接更換TCP協議為 QUIC 協議消除傳輸層的HOL阻塞問題。
2.4.2 HTTP/2 全雙工支持
注意HTTP直到2.0才是真正意義上的全雙工,所謂的HTTP支持全雙工是混淆了TCP協議來講的,因為TCP是支持全雙工的,TCP可以利用網卡同時收發數據。
為了搞清楚TCP和HTTP全雙工的概念, 應該理解HTTP中雙工的兩種模式:半雙工(http 1.0/1.1),全雙工(http 2.0)。
半雙工:同一時間內鏈接上只能有一方發送數據而另一方接受數據。
- http 1.0 是短連接模式,每個請求都要建立新的 tcp 連接,一次請求響應之後直接斷開,下一個請求重複此步驟。
- http 1.1 是長連接模式,可以多路複用,建立 tcp 連接不會立刻斷開,資源1 發送響應,資源2 發送響應,資源3 發送響應,免去了要為每個資源都建立一次 tcp 的開銷。
全雙工:同一時間內兩端都可以發送或接受數據 。
- http 2.0 資源1客户端發送請求不必等待響應就可以繼續發送資源2 的請求,最終實現一邊發,一邊收。
2.4.3 HTTP 2.0 缺點
- 解決了HTTP的隊頭請求阻塞問題,但是沒有解決TC P協議的隊頭請求阻塞問題,此外HTTP/2需要同時使用TLS握手和 HTTP握手耗時,同時在HTTPS連接建立之上需要使用TLS進行傳輸。
- HTTP/2的隊頭阻塞出現在當TCP出現丟包的時候,因為所有的請求被放到一個包當中,所以需要重傳,TCP 此時會阻塞所有的請求。但是如果是HTTP1.X,那麼至少是多個TCP連接效率還要高一些,
- 多路複用會增大服務器壓力,因為沒有請求數量限制,短時間大量請求會瞬間增大服務器壓力
- 多路複用容易超時,因為多路複用無法鑑定帶寬以及服務器能否承受多少請求。
丟包不如HTTP1.X
丟包的時候出現的情況是HTT P2.0因為請求幀都在一個TCP連接,意味着所有的請求全部要跟着TCP阻塞,在以前使用多個TCP連接來完成數據交互,其中一個阻塞其他請求依然可以正常抵達反而效率高。
二進制分幀目的
根本目的其實是為了讓更加有效的利用TCP底層協議,使用二進制傳輸進一步減少數據在不同通信層的轉化開銷。
HTTP1.X的Keep-alive缺點
- 必須按照請求響應的順序進行交互,HTTP2的多路複用則必須要按順序響應。
- 單個TCP一個時刻處理一個請求,但是HTTP2同一個時刻可以同時發送多個請求,同時沒有請求上限。
2.4.4 HTTP協議真的是無狀態的麼?
仔細閲讀HTTP1.x和HTTP/2以及HTTP3.0三個版本的對比,其實會發現HTTP無狀態的定義偷偷發生了變化的,為什麼這麼説?
在講解具體內容之前,我們需要弄清一個概念,那就是Cookie和Session雖然讓HTTP實現了“有狀態”,但是其實這和HTTP協議本身的概念是沒有關係的。
Cookie和Session的出現根本目的是保證會話狀態本身的可見性,兩者通過創立多種獨立的狀態“模擬”用户上一次的訪問狀態,但是每一次的HTTP請求本身並不會依賴上一次HTTP的請求,單純從廣義的角度看待其實所有的服務都是有狀態的,但是這並不會干擾HTTP1.X本身無狀態的定義。
此外HTTP協議所謂的無狀態指的是每個請求是完全的獨立的,在1.0備忘錄定義也可以看出一次HTTP連接其實就是一次TCP連接,到了HTTP1.1實現了一個TCP多個HTTP連接依然是可以看做獨立的HTTP請求。
説了這麼多,其實就是説HTTP1.X在不靠Cookie和Session扶着的時候看做無狀態是對的,就好比遊戲裏面的角色本身的數值和武器附加值的對比,武器雖然可以讓角色獲得某種狀態,但是這種狀態並不是角色本身特有的,而是靠外力借來的。
然而隨着互聯網發展,到了HTTP/2和HTTP3之後,HTTP本身擁有了“狀態”定義。比如2.0關於HEADER壓縮產生的HPACK算法(需要維護靜態表和動態表),3.0還對HPACK算法再次升級為QPACK讓傳輸更加高效。
所以總結就是嚴謹的來説HTTP1.X是無狀態的,在Cookie和Session的輔助下實現了會話訪問狀態的保留。
到了HTTP/2之後HTTP是有狀態的, 因為在通信協議中出現了一些狀態表來維護雙方重複傳遞的Header字段減少數據傳輸。
2.4 小結
這一章節本來應該是全書的核心內容,奈何作者似乎並不想讓讀者畏懼,所以講的比較淺顯,個人花費了不少精力收集網上資料結合自己的思考整理出第二章的內容。
關於HTTP的整個發展史是有必要掌握的,因為八股有時候會提到相關問題,問的深入一些確實有些頂不住,HTTP 協議也是應用層通信協議的核心,其次作為WEB開發人員個人認為是更是有必要掌握的。
另外瞭解HTTP的設計本身可以讓我們過渡到TCP協議的瞭解,TCP的設計導致了HTTP設計的影響等問題可以做更多思考。
關於更多內容建議可以看看《網絡是怎麼樣連接》的這一篇讀書筆記,原書從整個TCP/IP 結構的角度通俗的講述了有關互聯網發展的基本脈絡,而這一篇講述了HTTP發展的基本歷史和未來的發展方向。