在前面的介紹中,相信你已經對HTTPS有了一定的了解,並掌握了它在資料加密和傳輸安全上的重要性。然而,除了安全性之外,還有一個經常被忽略的關鍵領域——HTTP版本的演進與其差異。
很多人可能認為HTTP只是一個簡單的通訊協定,不需要特別關注它的細節。但事實上,從HTTP/1.0到HTTP/3的演進,這些技術革新提升了網頁的效能、穩定性以及用戶體驗。尤其是對於開發者來說,理解這些版本之間的差異,能夠幫助我們更好地優化網站效能。
版本基本上我們跳過HTTP0.9,因為它為 HTTP 協定的最早版本,於 1991 年推出。與後來的版本相比,功能非常有限,僅支持GET請求,且僅能傳輸純文字(沒有圖片、CSS 或 JavaScript 等檔案)。
在進入版本差異介紹之前,稍微簡單提一下TCP/IP,縮寫為Transmission Control Protocol(TCP) 及Internet Protocol (IP) 。它為電腦之間溝通的「通訊語言」
你可以把TCP/IP想像成郵件系統中的「郵差」和「信封」:
IP負責「路線」,讓資料知道要從哪裡送到哪裡;而TCP則確保這些資料完整無誤地到達。因此,TCP/IP的合作就像郵差根據信封上的地址,負責把一封封信件送到目的地一樣。
所以說,TCP 屬於 傳輸層(第四層)。傳輸層負責數據的可靠傳輸,確保資料到達目標後是完整的,並且處理數據包的順序和錯誤檢查。而IP 屬於「網路層」(第三層)。在這一層,IP負責將數據包從一台設備傳送到另一台設備,並確保數據能夠跨越不同的網路進行傳遞。
TCP 建立連線時,會基於基於三次握手來確定建立連線,這裡一樣引用 Cloudflare的圖,三次握手的明確步驟為
三次握手的設計是為了確保連線的可靠性,確保雙方都能接收到並準備好進行資料通訊。這也是 TCP 相對於 UDP 更加穩定的原因之一,因為每次連線建立都必須經過確認,避免資料遺失或重傳。
而所有的 HTTP 傳輸協定(從 HTTP/1.0 到 HTTP/3)都是基於 TCP/IP 架構來進行變形和改進的。不同的版本在 TCP/IP 基礎上,針對其效率和使用場景進行了不同的優化與調整。
接著我們可以進入版本差異的探討了,這裡一樣借用bytebytego的圖如下。這張圖主要展示了從 HTTP/1.0 到 HTTP/3.0 協議的演進,並且詳細解釋了不同HTTP版本在資料傳輸層的差異。
HTTP/1.0 和 HTTP/1.1 都是基於 TCP 連接進行傳輸的。可以看到左側方框中,顯示了三次握手過程,但HTTP1.0每次請求都需要建立一個新的TCP連接,這會在多次請求時,需要反覆進行握手,導致效能降低。
所以HTTP1.1引入 Keep-Alive,允許重複使用同一個 TCP連接來發送多個HTTP請求,,減少了每次請求都要重新建立連接的負擔,這就是圖中 Persistent Connection(持久連接)的概念。這樣一來,開啟連接後可以保持多個請求和回應,直到連接關閉。
HTTP/2 使用相同的 TCP 連接,但帶來了許多效能上的提升,特別是引入了「多工處理」和「Frame」機制。
Frame 的引入
HTTP/2 將所有的資料打包成獨立的「Frame」(幀)。每個請求和回應的資料都被切分成不同的 Frame,這些 Frame 可以在同一個 TCP 連接上進行傳輸,並且可以並行處理。每個 Frame 帶有流ID,這樣伺服器和客戶端能夠識別屬於同一個請求或回應的 Frame,並按需重組成完整的數據流(stream)。這使得 HTTP/2 能夠在同一條連接中同時處理多個請求,避免了 HTTP/1.1 中「封包頭阻塞」的問題(Head-of-Line Blocking)。
圖中展示了多個請求和回應(streams)在同一個 TCP 連接上傳輸。每個請求會有自己的標頭和資料,這些資料會被切分成不同的 Frame 同時傳輸,而不需要等候其他請求完成。Frame 的引入使得 HTTP/2 能夠在一個連線上同時傳輸多個請求與回應,實現更高效的多工處理。另外 ,HTTP/2 引入了標頭壓縮機制,進一步減少重複的請求和回應標頭資訊,降低了資料傳輸量。
標頭壓縮
這邊嘴巴癢,在稍微提一下關於標頭壓縮,標頭壓縮在 HTTP/2 中具體是透過一種稱為 HPACK 的壓縮格式進行,主要是使用靜態字典和動態字典來減少重複標頭的傳輸。
如下圖( 引用點我 )
最左邊的方框顯示了傳統的 HTTP 請求標頭,包括常見的 :method
、:scheme
、:host
、:path
、user-agent
和一個自定義標頭 custom-hdr
。這些標頭會隨著每次請求一起傳輸,但在重複請求中,這些資料其實很多都是重複的。
中間的方框展示了兩種字典:靜態字典(Static Table) 和 動態字典(Dynamic Table)。
靜態字典:包含了常用的標頭和標頭值,例如 :method GET
和 :host example.com
,每一個常用項目都有對應的索引值,這些索引可以用來替代實際的文字傳輸,以節省資料量。在這裡,可以看到靜態字典中的 :method GET
對應到索引2。
動態字典:動態字典是隨著請求過程中逐步更新的。當某個標頭在請求中首次出現,它會被加入到動態字典中,並分配一個索引值。例如,user-agent
和 :host example.com
都被加入到了動態字典中,並分別分配了索引值62和63。在後續的請求中,這些標頭可以直接使用動態字典中的索引,而不需要重複傳送整個字串
右邊的方框展示了最終的編碼結果。這個過程中,Header 被轉換為數字和霍夫曼編碼的組合:
2
代表 :method GET
,63
代表 :host example.com
,62
代表 user-agent
。/resource
和 some-value
就是用霍夫曼編碼進行壓縮的。具體的過程總結,HTTP/2 請求中,標頭資料首先會檢查是否已存在於靜態或動態字典中。如果存在,直接使用對應的索引來替代;如果不存在,則將該標頭或值加入動態字典並使用霍夫曼編碼壓縮數據。後續的請求可以利用動態字典中的索引來減少重複標頭的傳輸,進一步優化效能。
舉個例子,如果第一次請求標頭是
GET /index.html HTTP/2
Host: www.example.com
User-Agent: Mozilla/5.0
Accept: text/html
在後續請求中,如果 Host
和 User-Agent
沒有變化,HPACK 可以直接用動態字典的索引來表示這些標頭,例如將 Host
設為 1
,User-Agent
設為 2
。這樣接下來的請求不會再傳完整的字串,而是使用編號來代表標頭,減少傳輸量。
HTTP/3 則完全跳脫了TCP的限制,改用基於 UDP 的 QUIC 協議來進行資料傳輸。在圖的右下角展示了QUIC的傳輸過程。QUIC不再依賴三次握手,而是使用UDP進行更快的連接建立與資料傳輸。
QUIC 的多工處理功能比 HTTP/2 的 TCP 方式更有效率,因為在 TCP 中,如果一個資料包丟失,整個連線的所有請求都必須等待重傳。但在 QUIC 中,資料包的丟失只會影響單一請求(單一資料流),而不會阻塞其他請求的傳輸,這進一步提升了傳輸效能,特別是針對高延遲或不穩定的網路環境。 圖中展示的 QUIC 連線允許多個資料包(以編號 1 到 7 表示)同時傳輸,而不會互相阻塞。
此外,QUIC 也提供了零 RTT 連接建立(0-RTT connection establishment)和內建的 TLS 加密,進一步改善了效率和安全性。
以下為表格總結
HTTP版本 | 傳輸方式 | 效能特點 | 主要應用場景 |
---|---|---|---|
HTTP/1.0 | 基於TCP,單一請求 | 每次請求都要建立新連線,延遲高效能低 | 早期靜態網頁傳輸 |
HTTP/1.1 | 基於TCP,持久連線 | 支援持久連線和管道化,但仍有隊頭阻塞問題 | 現代網頁應用,但效能有限 |
HTTP/2.0 | 基於TCP,多工處理 | 支援多工處理、標頭壓縮和伺服器推送,效能顯著提升 | 現代高效能Web應用,API服務等 |
HTTP/3.0 | 基於UDP,使用QUIC協定 | 基於UDP的低延遲、高效能傳輸,資料包丟失只影響單一請求,適合不穩定網路環境 | 影音串流、即時通訊、互動遊戲、低延遲應用場景 |
以下是我用Telnet去觀察格式差異紀錄。因為是紀錄,所以就不多加解釋了,不過也建議你可以跟著玩看看去看HTTP的細部格式與差異。
使用 telnet 指令來手動發送一個 HTTP/1.0 請求,請求的目的是連線到 Google 的伺服器
Status Line:
HTTP/1.0 301 Moved Permanently
: Status Line,HTTP/1.0 表示協議版本。301 表示 HTTP 狀態碼,301 意思是所請求的資源已被永久移動到新的位置。Moved Permanently 是與狀態碼相關的人類可讀的狀態描述。
Headers:
Location: https://about.google/
Content-Type: text/html; charset=UTF-8
X-Content-Type-Options: nosniff
Date: Sat, 15 Jul 2023 01:29:28 GMT
Expires: Sat, 15 Jul 2023 01:59:28 GMT
Cache-Control: public, max-age=1800
Server: sffe
Content-Length: 218
X-XSS-Protection: 0
Blank Line:
Headers 和 Body 之間的空白行就是這裡的 Blank Line,這表示 Headers 的結束。
Body:
**<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">...
**回應的 Body 是一個 HTML 文件,它告訴用戶資源已經被移動,並提供了一個連結到新位置。
Connection closed by foreign host.
是在 HTTP Response 外的資訊,這是 telnet 客戶端告訴我們遠端伺服器已經關閉了連線。(HTTP1.0拿到資料後就會關閉連線但HTTP1.1不會)。
HTTP/2 的請求和回應:
Request Headers:
GET / HTTP/2
- 這是請求行,包含了 HTTP 方法 (GET)、路徑 (/)、和協議版本 (HTTP/2)。Host: www.google.com
user-agent: curl/7.68.0
accept: */*
Response Headers:
HTTP/2 200
- 這是狀態行,包含了協議版本 (HTTP/2) 和狀態碼 (200,表示 OK)。date: Sat, 15 Jul 2023 01:48:22 GMT
expires: -1
cache-control: private, max-age=0
vary: Accept-Encoding
這一行。Body:這是 HTML 文檔的開始,從 <!doctype html><html itemscope=""...
開始,實際的 HTML 內容在這段輸出中被省略了。
Connection state changed (MAX_CONCURRENT_STREAMS == 100)!
。在 HTTP/2 中,一個連接可以有多個串流(stream),每個串流都是一個獨立的請求/回應交換。這個訊息表示該連接的最大並行串流數已經被設定為 100。
最後,TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
以及後續的內容是關於 TLS 連接的細節,並不屬於 HTTP/2 請求/回應的一部分。
HTTP/2 與 HTTP/3,由於它們是二進位協議,並使用較複雜的機制,如Frames來組織資料, telnet 這種基於文本的工具來無法模擬或解析這些協議。(目前curl要使用HTTP3測試會比較麻煩,建議去看curl source readme操作看看)
一般Chrome開發者模式即可看HTTP發送與回應詳細格式,你可你看到目前大多是2.0與3.0混用狀況。
點擊後訊息如下
Headers
General:這部分包含一般性資訊,如請求的 URL、請求方法(例如 GET 或 POST)、回應的 HTTP 狀態碼(例如 200, 404等)、遠端地址(即服務器的 IP 和端口)和參照策略。
Response Headers:顯示從服務器返回的 HTTP 標頭。這些標頭可能包括內容類型、設置的 cookies、是否允許跨域請求等等。
Request Headers:顯示出發給服務器的 HTTP 標頭,可能包含用戶代理、接受的內容類型、引薦來源等資訊。
Payload:顯示了請求的主體數據,只有當請求方法為 POST 或 PUT 時才有。
Preview:顯示 HTTP 回應的內容。如果回應是 JSON,它會以更易於閱讀的方式格式化。
Response:顯示 HTTP 回應的原始內容。
Initiator:顯示發起該 HTTP 請求的檔案或函式的資訊。
Timing:顯示 HTTP 請求相關的各種時間點,例如,發送請求、接收回應等等的時間。