前後端開發必會的 HTTP 協定「十全大補丸」(萬字長文)

2022-12-26 06:00:39

本文全面介紹了 HTTP 協定相關知識,包括 HTTP 請求報文、響應報文、持久連線、快取、Cookie 以及 HTTP 版本升級等!

HTTP 協定全稱為 HyperText Transfer Protocol,即超文字傳輸協定。

  • 超文字:指文字、圖片、音訊、視訊、檔案等的混合體,比如最常見的 HTML。
  • 傳輸:指資料從一方轉移到另一方,二者之間可能相距數千裡。
  • 協定:指通訊雙方所做的一些約定,比如怎麼開始通訊、資訊的格式與順序、怎麼結束通訊等。

HTTP 協定是幹啥的呢? 答案是用於使用者端與伺服器端之間的通訊。我們日常上網過程中最常見的就是 HTTP 協定了,瀏覽器是最常見的 HTTP 使用者端。

比如我們使用瀏覽器存取淘寶時,瀏覽器就會傳送一個遵循 HTTP 協定的請求報文到淘寶伺服器,告訴淘寶伺服器自己想要獲取淘寶首頁資訊。

淘寶伺服器收到此報文後,則會傳送一個同樣遵循 HTTP 協定的響應報文到瀏覽器,此響應報文中包含淘寶首頁的內容。

瀏覽器收到響應報文後解析內容並展示在介面上。

1. HTTP 請求

使用者端向伺服器端傳送的資訊稱為請求報文,一般結構如下:

(1)請求行

請求行用於說明要做些什麼,包含三部分內容,中間用空格分割。

  • 方法,指定要對請求資源做什麼樣的操作(比如查詢、修改等)。常見的方法有:GET、POST、PUT、DELETE、HEAD 等。

    在前後端分離開發中,經常會遵循 RESTful 設計風格,其使用 POST、DELETE、PUT、GET 分別表示對資料的增、刪、改、查。

  • 資源路徑,指定所要請求資源在伺服器中的位置。比如 /index.html,表示存取伺服器根目錄下名字為 index 的 html 檔案。

  • HTTP 版本,指定所使用的 HTTP 版本。目前使用最多的版本為 HTTP/1.1。

舉個栗子:

面試中常見的一個問題: GET 和 POST 的區別是什麼?,在這裡做一下解答。

  • 首先,一般 GET 請求引數存放在 URL 中,而 POST 請求引數儲存在請求體中

    因為引數放在 URL 中可以直接被看到,則 GET 請求相對 POST 請求更不安全。但並不是說 POST 請求安全,因為引數放在請求體中,如果不採取加密手段的話,技術人員抓包就能看到明文。

    同時各個瀏覽器對 URL 長度做了限制,比如 IE 瀏覽器限制 URL 的長度最大為 2KB,這就使得了 GET 請求傳輸的資料長度有了限制,而 POST 請求傳輸資料長度無限制。

  • 其次,一般 GET 請求用於獲取資料,POST 請求用於新增資料

    這裡需要提一下冪等性的概念。冪等性是指對於同一個系統,在同樣條件下,一次請求和重複多次請求對資源的影響是一致的,不會因為多次請求而產生了副作用。

    GET 請求用於獲取資源,不會對系統資源進行改變,因此是冪等的。POST 用於新增資源,這意味著多次請求將建立多個資源,因此不是冪等的。

    基於這個特點,GET 請求可被快取、可保留在瀏覽器歷史記錄中、瀏覽器回退不會產生副作用,而 POST 請求反之。

  • 最後,GET 和 POST 在本質上並無區別

    HTTP 的底層是 TCP,所以無論是 GET 還是 POST 底層都是通過 TCP 進行連線通訊。

    我們可以給 GET 加請求體,給 POST 帶上 URL 引數,可以用 GET 請求新增資料,POST 請求查詢資料,實際上也是完全可行的。

(2)請求頭

請求頭用於向伺服器傳遞一些額外的重要資訊,比如所能接收的語言等。

請求頭由欄位名和欄位值構成,二者之間用冒號進行分隔。常見的一些請求頭有:

請求頭 含義
Host 接收請求的域名
User-Agent 使用者端軟體的名稱和版本號等相關資訊
Connection 設定傳送響應之後 TCP 連線是否繼續保持的通訊選項
Cache-Control 控制快取的相關資訊
Referer 記錄請求的來源(當通過點選超級連結進入下一個頁面時,會記錄上一個頁面的 URI)
Accept 使用者端可支援的資料型別, 以 MIME 型別來表示
Accept-Encoding 使用者端可支援的編碼格式
Accept-Language 使用者端可支援的語言
If-Modified-Since 用於判斷資源的快取是否有效(使用者端通知伺服器,本地快取的最後變更時間)
If-None-Match 用於判斷資源的快取是否有效
Range 用於斷點續傳,指定第一個位元組的位置和最後一個位元組的位置。
Cookie 表示請求者的身份,用於儲存狀態資訊

(3)請求空行

請求空行用於表明請求頭已經結束。

(4)請求體

請求體用於傳送使用者端要發給伺服器的資料,比如請求引數,通常出現在 POST 請求方法中,而 GET 方法無請求體,它的請求引數直接會顯示在網址上面。

請求行和請求頭的資料都是文字形式且格式化的,而請求體不同,其可以包含任意的二進位制資料,比如文字、圖片、視訊等等。

2. HTTP 響應

伺服器向用戶端傳送的資訊稱為響應報文,響應報文的結構一般如下:

(1)響應行

響應行用於說明對請求的處理情況,包含三部分內容,中間用空格分割。

  • HTTP 版本,指定所使用的 HTTP 版本。比如 HTTP/1.1 表示使用的 HTTP 版本是 1.1。

  • 狀態碼,以三位數位形式描述伺服器對請求的處理結果。比如 200 表示成功。

  • 訊息短語,以文字形式描述伺服器對請求的處理結果。比如 OK 表示成功。

舉個栗子:

面試中常見的一個問題: HTTP 有哪些常見狀態碼?,在這裡做一下解答。

  • 200 OK :表示請求被正常處理,這是最常見的狀態碼。

  • 204 No Content:表示請求被正常處理,但在返回的響應報文中不含響應體內容。一般用在只是使用者端向伺服器傳送資訊,而伺服器不用向用戶端返回什麼資訊的情況。不會重新整理頁面。

  • 301 Moved Permanently :永久重定向,表示請求的資源已經被永久轉移了,新的 URL 定義在響應報文的 Location 欄位中,瀏覽器將自動獲取新的 URL 發出新的請求。

    對搜尋引擎來說,如果是 301 永久性重定向,頁面會刪除失效的 URL 收錄、索引,並替換為新的 URL。兩個不同的 URL 有指向相同的內容,搜尋引擎對這兩個 URL 收錄的情況不同,會導致 URL 的權重和排名被分散。做了重定向後,可以將權重和排名集中到重定向後的 URL,因此可以提升自然排名。

    場景:比如建設一個網站後,將網站的 url 變換了,重新申請一個域名,但是希望之前 url 仍然可以存取到,就可以做一個重定向到新的 url 下面。比如京東最早網址 http://www.360buy.com 重定向到 http://www.jd.com

  • 302 Found :臨時重定向(即以後還可能有變化),表示請求的資源已被臨時分配了新的 URL,新的 URL 會在響應報文中的 Location 欄位中返回,瀏覽器將會自動使用新的 URL 發出新的請求。

    搜尋引擎會保留原 URL 的收錄和索引,並新增新 URL 的收錄、索引,這樣更有利於頁面的程式化處理。

    比如使用者在未登入時存取個人中心頁面,這時可以臨時重定向到登入的 url;

    或者協定發生變化,比如京東 http://www.jd.com 重定向到 https://www.jd.com

    再比如,今天夜裡網站後臺要系統維護,服務暫時不可用,這就屬於『臨時』的,可以設定成 302 跳轉,把流量臨時切換到一個靜態通知頁面,瀏覽器看到這個 302 就知道這只是暫時的情況,不會做快取優化,第二天還會存取原來的地址。

  • 304 Not Modified:代表上次的檔案已經被快取了,還可以繼續使用,即存取快取。

  • 400 Bad Request :一個通用差錯狀態碼,表示請求報文中存在語法錯誤,使用者端發生的錯誤。

  • 401 Unauthorized :使用者未認證。

  • 403 Forbidden :表示伺服器雖然收到了請求,但是拒絕提供服務,常見的原因是為沒有存取許可權(即使用者未授權)。

  • 404 Not Found :表示請求資源不存在。

  • 500 Internal Server Error :表示伺服器出現錯誤,可能是出現了一些 Bug 或故障。

  • 502 Bad Gateway:通常是伺服器作為閘道器或代理時返回的錯誤碼,表示伺服器自身工作正常,存取後端伺服器發生了錯誤(可能後端伺服器宕機了)。

  • 503 Service Unavailable:表示伺服器暫時處於超負載或者正在進行停機維護,暫時無法處理請求,可以稍後再試。Web 伺服器如果限流,就可以給超載的流量直接響應 503 狀態碼。

(2)響應頭

響應頭用於向用戶端傳遞一些額外的重要資訊,比如響應內容的長度等。

響應頭由欄位名和欄位值構成,二者之間用冒號進行分隔。常見的一些響應頭有:

響應頭 含義
Date 日期時間資訊,表示伺服器產生並行送響應報文的日期和時間。
Server 表示HTTP伺服器應用程式的資訊,類似於請求報文中的 User-Agent
Location 此欄位會配合重定向使用,用於提供重定向後新的 URI。
Connection 設定傳送響應之後 TCP 連線是否繼續保持的通訊選項
Cache-Control 控制快取的相關資訊
Content-Type 伺服器返回的響應型別
Content-length 伺服器返回的響應長度
Content-Encoding 伺服器返回的響應編碼
Content-Language 伺服器返回的響應語言
Last-Modified 指定響應內容最後的修改時間
Expires 表示資源失效的時間,瀏覽器會在指定過期時間內使用本地快取
Etag 用於協商快取,返回一個摘要值
Accept-Ranges 用於斷點續傳,指定伺服器所支援的內容範圍
Set-Cookie 設定狀態資訊

(3)響應空行

響應空行用於表明響應頭已經結束。

(4)響應體

響應體用於傳送伺服器要發給瀏覽器的正文。

同請求報文的請求體一樣,響應體可包含任意的二進位制資料。瀏覽器收到響應報文後,則會將正文載入到記憶體,然後解析渲染,最後顯示頁面內容。

3. HTTP 持久連線

使用者端傳送一系列請求給伺服器,如果伺服器與使用者端對每個請求/響應對都經過一個單獨的 TCP 連線傳送,則稱為非持續連線,也稱為短連線;如果經過相同的 TCP 連線傳送,則稱為持續連線,也稱為長連線。

比如開啟一個 Web 頁面時,假設該頁面含有一個 HTML 基礎檔案和 2 張圖片,如果使用者端與伺服器通過同一個 TCP 連線來獲取這 3 個資料,則為持續連線,如果通過建立 3 次不同的 TCP 連線,則為非持續連線。

非持續連線的缺點:

  • 每次建立連線需要三次握手過程,導致總的請求響應時間變長。當然也不是絕對的,如果多個連線可以並行請求,總響應時間可能變短,比如 Chrome 瀏覽器為了提升載入速度,可以同時開啟 6 個並行連線,但多個並行連線會加重 Web 伺服器負擔。
  • 必須為每一個請求的物件建立和維護一個全新的連線,而每一個連線都需要客戶和伺服器分配 TCP 的緩衝區和保持 TCP 變數,使得 Web 伺服器存在嚴重的負擔,因為一臺 Web 伺服器可能同時服務於數以百計不同的客戶的請求。

HTTP(1.1 及之後) 預設採用持續連線方式,但也可設定成非持續連線方式。在報文中使用 Connection 欄位來表示是否使用持久連線。

  • 如果 Connection 欄位的值為 keep-alive,則表明此連線為持久連線,HTTP1.1 及以後可預設不寫。
  • 如果 Connection 欄位的值為 close,則表明要關閉連線。

注意:持久連線不是永久連線,一般在一個可設定的超時間隔後,如果此連線仍未被使用,HTTP 伺服器就會關閉該連線。

4. HTTP 快取

對於一些短時間內不會產生變化的資源,使用者端(瀏覽器)可以在一次請求後將伺服器響應的資源快取在本地,之後直接讀取原生的資料,而不必再重新傳送請求。

我們經常會接觸到『快取』這一概念,比如由於記憶體和 CPU 之間速度差距較大,為了進一步提升電腦效能,於是設計了 L1 快取、L2 快取等,讓 CPU 先從快取中取資料,如果取不到,再去記憶體取。

又比如在後端開發中,由於資料庫一般儲存在硬碟上,讀取速度較慢,於是可能會採用 Redis 等記憶體資料庫作為快取,先去 Redis 中取資料,如果取不到,再去資料庫中取。

再比如在作業系統中,由於頁表進行地址轉換的速度較慢,於是有了 TLB 快表,當需要進行邏輯地址到實體地址的轉換時,先去查詢速度更快的 TLB 快表,如果查不到,再去查詢頁表,此時 TLB 快表就是一種快取。

快取的主要目的在於提升查詢速度,一般邏輯如圖所示。

同樣,在 HTTP 設計中也有快取的概念,主要是為了加快響應速度,HTTP 快取的實現依賴於請求報文和響應報文中的一些欄位,分為強快取協商快取

(1)強快取

強快取指的是在快取資料未失效的情況下,那麼就會直接使用瀏覽器的快取資料,不會再向伺服器傳送任何請求,邏輯類似於前面舉的 L1 快取、Redis、TLB 快表。

具體實現主要是通過 Cache-Control 欄位和 Expires 欄位。

Cache-Control 是一個相對時間(即多長時間後過期,http1.1 規範),Expires 是一個絕對時間(即在某個時間點過期,http1.0 規範),如果兩個欄位同時存在,Cache-Control 的優先順序更高。

由於伺服器端時間和使用者端時間可能不同步,存在偏差,這也就是導致了使用 Expires 可能會存在時間誤差,因此一般更推薦使用 Cache-Control 來實現強快取

以 Cache-Control 為例,強快取的具體的實現流程如下:

  1. 當瀏覽器第一次請求存取伺服器資源時,伺服器會在響應頭中加上 Cache-Control。Cache-Control 中可以設定以下內容。

    • max-age=秒,表示快取將於指定毫秒值後過期。比如:cache-control: max-age=31536000,表示快取將於 365 天后過期。

    • no-store,表示不允許快取(包括強快取和協商快取)。

    • no-cache,表示不使用強快取,而是使用協商快取,即使用之前必須要先去伺服器端驗證是否失效,如果沒失效,則再使用快取,如果失效了,則返回最新資料。等價於max-age=0, must-revalidate

    • must-revalidate,表示允許快取,並且如果快取不過期的話,先使用快取,如果快取過期的話,再去伺服器端進行驗證快取是否還有效。

      這裡很多小夥伴可能會有疑問,即使沒有加上 must-revalidate,有了 max-age 後,快取過期了不也會去伺服器驗證嗎,加不加 must-revalidate 有什麼區別呢?

      在 HTTP 協定規範中,允許使用者端在某些特殊情況下直接使用過期快取,比如校驗請求錯誤時(如無法再次連通伺服器),而加上了 must-revalidate 後,在校驗請求錯誤時,會返回 504 錯誤碼,而不是使用過期快取。

  2. 瀏覽器再次請求存取伺服器中的該資源時,根據請求資源的時間與 Cache-Control 中設定的過期時間大小,計算出該資源是否過期,

    1. 如果沒有過期(且 Cache-Control 沒有設定 no-cache 屬性和 no-store 屬性),則使用該快取,結束;
    2. 否則重新請求伺服器;

(2)協商快取

協商快取指的是當第一次請求後,伺服器響應頭 Cache-Control 欄位屬性設定為 no-cache 或者快取時間過期了,那麼瀏覽器再次請求時就會與伺服器進行協商,判斷快取資源是否有效,即資源是否進行了修改更新。

  • 如果資源沒有更新,那麼伺服器返回 304 狀態碼,表明快取仍然可用,而不需要再次傳送資源,減少了伺服器的資料傳輸壓力,並更新快取時間。
  • 如果資料有更新,伺服器返回 200 狀態碼,新資源存放在請求體中。

協商快取可以基於以下兩種方式來實現:

第一種(HTTP/1.0 規範):請求頭部中的 If-Modified-Since 欄位與響應頭部中的 Last-Modified 欄位:

  • Last-Modified:標示這個響應資源的最後修改時間。第一次請求資源後,伺服器將在響應頭中帶上此資訊。
  • If-Modified-Since:當資源過期了,瀏覽器再次發起請求的時候帶上 Last-Modified 的時間(放在請求頭 If-Modified-Since 中),伺服器將此時間與被請求資源的最後修改時間進行對比,
    • 如果最後修改時間較大,說明資源有被修改過,則返回最新資源和 200 狀態碼;
    • 否則說明資源無新修改,返回 304 狀態碼。
  • 此種方式存在以下問題:
    • 基於時間實現,可能會由於時間誤差而出現不可靠問題,並且只能精確到秒級,在同一秒內,Last-Modified 無感知。
    • 如果某些檔案被修改了,但是內容並沒有任何變化(比如只是修改時間發生了變化),而 Last-Modified 卻改變了,導致檔案沒法使用快取。

第二種(HTTP/1.1 規範):請求頭部中的 If-None-Match 欄位與響應頭部中的 ETag 欄位:

  • Etag:唯一標識響應資源,是一個 hash 值;第一次請求資源後,伺服器將在響應頭中帶上此資訊。
  • If-None-Match:當資源過期了,瀏覽器再次向伺服器發起請求時,會將請求頭 If-None-Match 值設定為 Etag 中的值。伺服器將此值與資源的 hash 值進行比對,
    • 如果二者相等,則資源沒有變化,則返回 304 狀態碼。
    • 如果資源變化了,則返回新資源和 200 狀態碼。
  • 此種方式存在的問題在於計算 Etag 會消耗系統效能,但可以解決第一種方式所存在的問題,推薦使用。

注意 :

  • 如果 HTTP 響應頭部同時有 Etag 和 Last-Modified 欄位的時候,Etag 的優先順序更高,也就是先會判斷 Etag 是否變化了,如果 Etag 沒有變化,然後再看 Last-Modified。
  • Ctrl + F5 強制重新整理,會直接向伺服器提取資料。
  • F5重新整理或瀏覽器的重新整理按鈕,預設加上 Cache-Control:max-age=0,即會走協商快取。

HTTP 是一種無狀態協定,即其本身不會記憶請求和響應之間的通訊狀態,那麼 Web 伺服器就無法判斷此請求到底來自於哪個使用者,HTTP 協定中並不會儲存關於使用者的任何資訊。這樣設計的好處是不需要額外資源儲存使用者狀態資訊,減少了伺服器的 CPU 及記憶體資源的消耗。

但是隨著 Web 的發展,很多業務需要儲存使用者狀態。

  • 比如電商網站需要在使用者跳轉到其他商品頁面時,仍然可以儲存使用者的登入狀態。不然使用者每存取一次網站都要重新登入一下,過於繁瑣,體驗效果就很差。
  • 比如短視訊網站希望記錄使用者以前看過的視訊,以便之後向其精準化推薦感興趣的視訊。

為了實現保持狀態的功能,這就出現了 Cookie。Cookie (伺服器給的憑證)類似於我們逛商場時的會員卡(商家給的憑證),記錄著我們的身份資訊,只要出示了會員卡,商場工作人員就能確定我們的身份。同樣的,只要給伺服器傳送報文時帶上了 Cookie,他就知道我們是誰了。

Cookie 中可以包含任意資訊,最常見的是包含一個伺服器為了進行跟蹤而產生的獨特的識別碼

舉個栗子:

張三在發出第一次請求後,伺服器將其狀態資訊記錄下來,比如他的名字、年齡、地址、購物歷史等,並通過響應頭 Set-Cookie 欄位,給予其一個 id=12345 的獨特識別碼作為 Cookie,那麼其再次向伺服器發出請求時,瀏覽器會自動在請求報文中的 Cookie 欄位中帶上 id=12345,伺服器就可以通過這個查詢到張三的具體資訊,從而實現了保持狀態的功能。

Cookie 屬性:

  • max-age:過期時間有多長(絕對時間,單位:秒)。
    • 負數,表示瀏覽器關閉即失效。預設即為 -1。
    • 正數:失效時刻= 建立時刻+ max-age。
    • 0:表示 Cookie 立即刪除,即 Cookie 直接過期(從而實現使 cookie 失效)。
  • expires:過期時間(相對時間)。
  • secure:表示這個 Cookie 只會在 https 的時候才會傳送。
  • HttpOnly:設定後無法通過使用 JavaScript 指令碼存取,可以保障安全,防止攻擊者盜用使用者 Cookie。
  • domain:表示該 Cookie 對於哪個域是有效的。 (Cookie 預設是不能直接跨域存取的,但是二級域名是可以共用 cookie 的)

Cookie 的缺點是如果傳遞的狀態資訊較多,使得包過大,將會降低網路傳輸效率。

一般瀏覽器限制 Cookie 大小為 4KB。

6. HTTP 版本

隨著網際網路的發展,HTTP 也在不斷升級打怪,下面分別介紹一下 HTTP1.1、HTTP/2 以及 HTTP/3 在前一版本的改進之處。

(1)HTTP/1.1 相比 HTTP/1.0 效能上的改進

HTTP/1.1 是目前最常見的 HTTP 版本,其相對於 HTTP/1.0 有以下改進。

① 持久連線

這個在前文中已經提到過,HTTP/1.0 中一個 TCP 連線只能傳送一個請求和響應,而 HTTP/1.1 進行了優化,同一個 TCP 連線可以傳送多次 HTTP 請求,減少了建立和關閉連線的效能開銷。

Web 服務軟體一般都會提供 keepalive_timeout 引數,用來指定 HTTP 持久連線的超時時間。比如設定了 HTTP 持久連線的超時時間是 60 秒,Web 服務軟體就會啟動一個定時器,如果完成某個 HTTP 請求後,在 60 秒內都沒有再發起新的請求,就會觸發回撥函數來釋放該連線。

② 管道機制

持久連線雖然可以多個請求複用同一個連線,但是每次都需要等到上一個請求響應完成後,才能傳送下一個請求。

管道機制中,只要第一個請求發出去了,不必等其回來,就可以發第二個請求出去,即相當於同時發出多個請求,因而可以減少整體的響應時間。

雖然使用者端可以同時發出多個 HTTP 請求,不用⼀個個等待響應,但是伺服器必須按照接收請求的順序依次傳送對這些管道化請求的響應,以保證使用者端能夠區分出每次請求的響應內容。這存在下面問題:

如果伺服器端在處理一個請求時耗時比較長,那麼後續請求的處理都會被阻塞住,會導致使用者端遲遲收不到資料,這稱為「隊頭堵塞」。

實際上,雖然管道機制的想法很好,但實現卻非常困難,因而很多瀏覽器根本不支援它。一般為了提升效能,採用並行多個 TCP 連線的形式來實現請求的同時傳送。

③ 快取控制

前文已經提到過,HTTP/1.1 在 HTTP/1.0 基礎之上,增加了一些請求響應頭,以更好的實現對快取的控制。比如

  • 新增 Cache-Control 代替原先的 Expires
  • 新增 If-None-MatchEtag 代替原先的 If-Modified-SinceLast-Modified

④ 斷點續傳

利⽤ HTTP 訊息頭使⽤分塊傳輸編碼,將實體主體分塊傳輸。

(2)HTTP/2 相比 HTTP/1.1 效能上的改進

HTTP/2 協定本身是基於 HTTPS 的,因此更加安全,其相對於 HTTP/1.1 有以下改進。

① 頭部壓縮

HTTP/1.1 中的請求頭攜帶大量資訊,而且每次都要重複傳送,即使是同樣的內容,每次請求都需要附帶,這會造成效能的損耗。HTTP/2 進行了優化,引入了頭資訊壓縮機制

使用者端和伺服器同時維護一張頭資訊表,高頻出現的欄位會存入這個表,生成一個索引號。傳送報文時直接使用索引號替代欄位。另外,索引表中不存在的欄位使用哈夫曼編碼壓縮

同時,多個請求中,如果請求頭相同,則後續請求只需要傳送差異的部分,重複的部分無需再傳送

② 二進位制幀

HTTP/1.1 的報文為純文字格式,而 HTTP/2 的報文全面採用二進位制格式,並將原始的報文拆分為頭資訊幀(Headers Frame)和資料框(Data Frame)。採用二進位制格式有利於提升資料傳輸效率。

③ 多路複用

在 HTTP/2 中定義了流(Stream)的概念,它是二進位制幀的雙向傳輸序列,一個資料流對應著一個完整的請求-響應過程,在同一個請求響應過程中,往返的幀會分配一個唯一的流編號(Stream ID)。

在流的支援下,HTTP/2 可以在一個 TCP 連線中傳輸多個請求或響應,而不用按照順序一一對應(即實現多路複用),因為它們屬於不同的流,所傳送的幀頭部都會攜帶 Stream ID,可以通過此 Stream ID 有效區分不同的請求-響應。

因而 HTTP/2 解決了 HTTP/1.1 的『隊頭阻塞』問題,多個請求 - 響應之間沒有了順序關係,不需要排隊等待,降低了延遲,大幅度提高了連線的利用率。

舉個栗子:

在一個 TCP 連線裡面,伺服器同時收到了 A 請求和 B 請求,於是先回應 A 請求,結果發現處理過程非常耗時,於是就傳送 A 請求已經處理好的部分,接著迴應 B 請求,完成後,再傳送 A 請求剩下的部分。

④ 伺服器端推播

在 HTTP/1.1 中,只能使用者端發起請求,伺服器對請求進行響應。

而在 HTTP/2 中,伺服器端可以主動給使用者端推播必要的資源,以減少請求延遲時間。

比如當用戶端向伺服器請求一個 HTML 檔案後,伺服器除了將此 HTML 檔案響應給使用者端外,還可以提前主動將此 HTML 中所依賴的 JSCSS 檔案推播給使用者端,這樣使用者端在解析 HTML 時,無需耗費額外的請求去得到相應的 JSCSS 檔案。

(3)HTTP/3 相比 HTTP/2 效能上的改進

Google 公司為了解決 HTTP/2 存在的一些問題,提出了 QUIC 協定,而 HTTP-over-QUIC 就是 HTTP/3,其相對於 HTTP/2 有以下改進。

① 無隊頭阻塞

前面提到,HTTP/2 通過多路複用解決了 HTTP1.1 的『隊頭阻塞』問題,但其只是解決了 HTTP 這一層面的『隊頭阻塞』問題,底層仍然採用的 TCP 連線,HTTP/2 並沒有解決 TCP 的『隊頭阻塞』問題。

TCP 是可靠的、面向位元組流的協定。HTTP/2 的多個請求雖然可以跑在同一個 TCP 連線中,但如果出現丟包現象,TCP 就需要進行重傳,這可能就會導致整個 TCP 連線上的所有流阻塞,直到丟的包重傳成功,這就是 TCP 的『隊頭阻塞』問題。

為了解決此問題,HTTP/3 底層不再使用 TCP,而是採用 UDP!而 UDP 是無連線的,多個流互相獨立,之間不再有依賴,因而即使某個流發生了丟包,只會對該流產生影響,並不會使得其他流阻塞!

這時候有的小夥伴可能會問了,HTTP/3 底層不採用 TCP,那怎麼保證可靠傳輸呢?答案就是 HTTP/3 在應用層自己重新實現了可靠性機制。也就是說,HTTP/3 將原先 TCP 協定提供的部分功能上移至 QUIC,而且進行了改進。

② 優化重傳機制

TCP 採用序號+確認號+超時重傳機制來保證訊息的可靠性,即如果某條訊息超過一定時間還沒有得到確認,則重新傳送此訊息。

由於網路擁堵情況不斷變化,因而訊息的超時時間並不是固定的,而是通過不斷取樣訊息的往返時間不斷調整的,但 TCP 超時取樣存在不準確的問題

舉個栗子:

使用者端傳送一個序號為 N 的包,然後超時了(可能丟了,也可能網路堵塞了),於是重新傳送一個序號為 N 的包,之後伺服器收到後返回一個確認號 ACK 為 N+1 的包。但此時使用者端並無法判斷這個確定包是對原始報文的確認還是重傳報文的確認,那麼此時往返時間應該如何計算呢?

  • 如果認為確認包是對原始報文的確認,則可能把時間算長了;
  • 如果認為確認包是對重傳報文的確認,則可能包時間算長了。

因而 TCP 的重傳超時時間計算不準確,如果計算偏大,則效率慢,很久才會重傳,而如果計算偏小,則可能確認報文已經在路上了,但卻重傳了!

QUIC 是如何解決此問題呢?其定義了一個遞增的序列號(不再叫 Seq,而是 Packet Number),每個序列號的包只傳送一次,即使重傳相同的包,其序列號也不一樣

舉個栗子:

使用者端傳送一個序號為 N 的包,然後超時了,於是重新傳送一個相同的包,但序號不再是 N,而是 N+1;那麼如果返回的確認包 ACK 為 N+1,就是對原始報文的響應,如果 ACK 為 N+2,就是對重傳報文的響應,因而取樣時間計算相對更加準確!

那此時怎麼知道包 N 和包 N+1 是同一個包呢?QUIC 定義了一個 Offset 概念。傳送的資料有個偏移量 Offset,可以通過 Offset 知道資料目前傳送到了哪裡,因而如果某個 Offset 的包沒有收到確認,就重發。

③ 連線遷移

眾所周知,一條 TCP 連線是由四元組標識的,分別是源 IP、源埠、目的 IP、目的埠。一旦其中一個元素髮生了變化,就需要斷開重連。

當手機訊號不穩定或者在 WIFI 與行動網路切換時,都將會導致重連,而重連就意味著需要重新進行三次握手,將產生一定的時延,使用者感到卡頓,體驗不友好。

而 QUIC 不採用四元組的方式標識連線,而是以一個 64 位的亂數作為 ID 來標識,通過此連線 ID 標記通訊的兩端,之後即使網路發生變化,IP 或埠變了,但只要 ID 不變,則無需重連,只需要複用原先連線即可,時延低,減少了使用者的卡頓感,實現連線遷移。

(4)總結