HTTP 協定結構解剖
上一篇我們了解了一個網路請求的完整生命週期。現在,讓我們把焦點放在 HTTP 協定本身,深入解剖它的報文結構。
HTTP(HyperText Transfer Protocol)是一個純文字協定,這意味著你可以用肉眼直接閱讀它的內容。理解報文結構是掌握 HTTP 的基礎中的基礎。
一、 HTTP 報文總覽
HTTP 通訊基於「請求-回應」模式:客戶端發送請求報文(Request Message),伺服器回傳回應報文(Response Message)。
兩種報文的結構非常相似:
┌─────────────────────────────────────┐
│ 起始行 (Start Line) │
├─────────────────────────────────────┤
│ 標頭 (Headers) │
│ Header: Value │
│ Header: Value │
│ ... │
├─────────────────────────────────────┤
│ 空行 (CRLF) │
├─────────────────────────────────────┤
│ 主體 (Body) │
│ (可選) │
└─────────────────────────────────────┘| 組成部分 | 說明 |
|---|---|
| 起始行 | 描述請求或回應的基本資訊 |
| 標頭 | 鍵值對形式的元資料(Metadata) |
| 空行 | 標頭與主體之間的分隔符(CRLF) |
| 主體 | 實際傳輸的資料(可選) |
二、 請求報文結構
完整範例
讓我們看一個實際的 HTTP 請求:
POST /api/users HTTP/1.1
Host: api.example.com
Content-Type: application/json
Content-Length: 42
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Accept: application/json
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)
{"name": "John", "email": "john@example.com"}請求行(Request Line)
請求報文的第一行稱為請求行,由三個部分組成:
POST /api/users HTTP/1.1
│ │ │
│ │ └─ HTTP 版本(HTTP-Version)
│ └─ 請求目標(Request-Target / URI)
└─ 請求方法(Method)請求方法(Method)
HTTP 定義了多種請求方法,每種有不同的語義:
| 方法 | 語義 | 有 Body | 冪等性 | 安全性 |
|---|---|---|---|---|
| GET | 獲取資源 | ❌ | ✅ | ✅ |
| POST | 建立資源 | ✅ | ❌ | ❌ |
| PUT | 完整更新資源 | ✅ | ✅ | ❌ |
| PATCH | 部分更新資源 | ✅ | ❌ | ❌ |
| DELETE | 刪除資源 | ❌ | ✅ | ❌ |
| HEAD | 獲取標頭(無 Body) | ❌ | ✅ | ✅ |
| OPTIONS | 查詢支援的方法 | ❌ | ✅ | ✅ |
> **冪等性**:多次執行相同請求,結果相同。**安全性**:不會改變伺服器狀態。後續文章會深入探討。
請求目標(Request-Target)
請求目標通常是資源的路徑,有四種形式:
| 形式 | 範例 | 使用場景 |
|---|---|---|
| origin-form | /api/users?page=1 | 最常見,一般請求 |
| absolute-form | http://example.com/api | 代理請求 |
| authority-form | example.com:443 | CONNECT 方法(隧道) |
| asterisk-form | * | OPTIONS 方法 |
HTTP 版本
常見的版本:
| 版本 | 說明 |
|---|---|
| HTTP/1.0 | 每次請求新建連線 |
| HTTP/1.1 | 持久連線,最廣泛使用 |
| HTTP/2 | 二進位協定,多路複用 |
| HTTP/3 | 基於 QUIC,使用 UDP |
三、 回應報文結構
完整範例
HTTP/1.1 200 OK
Date: Wed, 08 Jan 2025 10:30:00 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 85
Cache-Control: no-cache
X-Request-Id: abc123
{"id": 1, "name": "John", "email": "john@example.com", "createdAt": "2025-01-08T10:30:00Z"}狀態行(Status Line)
回應報文的第一行稱為狀態行,由三個部分組成:
HTTP/1.1 200 OK
│ │ │
│ │ └─ 原因短語(Reason Phrase)
│ └─ 狀態碼(Status Code)
└─ HTTP 版本狀態碼(Status Code)
狀態碼是三位數字,分為五大類:
| 範圍 | 類別 | 意義 | 範例 |
|---|---|---|---|
| 1xx | Informational | 資訊性,處理中 | 100 Continue |
| 2xx | Success | 成功 | 200 OK, 201 Created |
| 3xx | Redirection | 重導向 | 301 Moved, 304 Not Modified |
| 4xx | Client Error | 客戶端錯誤 | 400 Bad Request, 404 Not Found |
| 5xx | Server Error | 伺服器錯誤 | 500 Internal Error, 503 Unavailable |
原因短語(Reason Phrase)
原因短語是狀態碼的人類可讀描述。在 HTTP/1.1 中是必要的,但在 HTTP/2 和 HTTP/3 中已被移除(因為是二進位協定)。
IMPORTANT
不要依賴原因短語來判斷結果,永遠以狀態碼為準。不同伺服器可能回傳不同的原因短語。
四、 標頭(Headers)
標頭是 HTTP 報文的核心,攜帶了大量的元資料。
標頭格式
每個標頭由名稱和值組成,以冒號分隔:
Header-Name: Header-Value規則:
- 名稱不區分大小寫(
Content-Type等同content-type) - 名稱與冒號之間不能有空格
- 值前可以有一個空格(慣例)
- 一行一個標頭
- 以 CRLF(
\r\n)結尾
標頭分類
HTTP 標頭可以分為四大類:
通用標頭(General Headers)
請求和回應都可使用:
| 標頭 | 說明 | 範例 |
|---|---|---|
| Date | 報文建立時間 | Wed, 08 Jan 2025 10:30:00 GMT |
| Cache-Control | 快取指令 | max-age=3600, public |
| Connection | 連線管理 | keep-alive, close |
| Transfer-Encoding | 傳輸編碼 | chunked |
請求標頭(Request Headers)
僅用於請求:
| 標頭 | 說明 | 範例 |
|---|---|---|
| Host | 目標主機(必填) | api.example.com |
| User-Agent | 客戶端資訊 | Mozilla/5.0 (...) |
| Accept | 可接受的回應類型 | application/json |
| Accept-Language | 偏好語言 | zh-TW, en;q=0.9 |
| Accept-Encoding | 可接受的壓縮方式 | gzip, deflate, br |
| Authorization | 認證資訊 | Bearer token123 |
| Cookie | 客戶端 Cookie | session=abc123 |
| Referer | 來源頁面 | https://example.com/page |
| Origin | 請求來源(CORS) | https://example.com |
回應標頭(Response Headers)
僅用於回應:
| 標頭 | 說明 | 範例 |
|---|---|---|
| Server | 伺服器軟體 | nginx/1.18.0 |
| Set-Cookie | 設定 Cookie | session=abc; Path=/; HttpOnly |
| Location | 重導向目標 | https://example.com/new |
| WWW-Authenticate | 認證要求 | Bearer realm="api" |
| Access-Control-* | CORS 相關 | Access-Control-Allow-Origin: * |
實體標頭(Entity Headers)
描述 Body 的屬性:
| 標頭 | 說明 | 範例 |
|---|---|---|
| Content-Type | 內容的 MIME 類型 | application/json; charset=utf-8 |
| Content-Length | 內容長度(bytes) | 1234 |
| Content-Encoding | 內容壓縮方式 | gzip |
| Content-Language | 內容語言 | zh-TW |
| Content-Disposition | 處理方式(下載等) | attachment; filename="file.pdf" |
自訂標頭
你可以定義自己的標頭。歷史上常用 X- 前綴,但 RFC 6648 已廢棄此慣例:
# 舊式(已不建議)
X-Request-Id: abc123
# 現代做法
Request-Id: abc123五、 主體(Body)
Body 是 HTTP 報文中實際傳輸的資料。
何時有 Body?
| 報文類型 | 有 Body 的情況 |
|---|---|
| 請求 | POST、PUT、PATCH 通常有;GET、DELETE 通常沒有 |
| 回應 | 大多數有;204、304 沒有 |
Content-Type 常見值
Content-Type 告訴接收方如何解讀 Body:
| MIME 類型 | 說明 | 用途 |
|---|---|---|
text/html | HTML 文件 | 網頁 |
text/plain | 純文字 | 文字檔 |
application/json | JSON 格式 | API 資料交換 |
application/xml | XML 格式 | 舊式 API |
application/x-www-form-urlencoded | 表單編碼 | HTML 表單 |
multipart/form-data | 多部分表單 | 檔案上傳 |
application/octet-stream | 二進位資料 | 檔案下載 |
表單編碼格式比較
HTML 表單有兩種編碼方式:
application/x-www-form-urlencoded
POST /login HTTP/1.1
Content-Type: application/x-www-form-urlencoded
username=john&password=secret123特點:
- 資料以
key=value&key=value格式編碼 - 特殊字元會被 URL 編碼(如空格變
%20) - 適合簡單表單
multipart/form-data
POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxk
------WebKitFormBoundary7MA4YWxk
Content-Disposition: form-data; name="title"
My Document
------WebKitFormBoundary7MA4YWxk
Content-Disposition: form-data; name="file"; filename="doc.pdf"
Content-Type: application/pdf
(二進位內容)
------WebKitFormBoundary7MA4YWxk--特點:
- 每個欄位用 boundary 分隔
- 可傳輸二進位檔案
- 適合檔案上傳
六、 行結束符:CRLF
HTTP/1.x 使用 CRLF(Carriage Return + Line Feed)作為行結束符:
CR = \r = 0x0D (Carriage Return)
LF = \n = 0x0A (Line Feed)
CRLF = \r\n報文結構圖解
POST /api HTTP/1.1\r\n ← 請求行 + CRLF
Host: example.com\r\n ← 標頭 + CRLF
Content-Type: application/json\r\n
Content-Length: 13\r\n
\r\n ← 空行(只有 CRLF)
{"id": 1} ← Body(無 CRLF)為什麼是 CRLF?
這是歷史遺留問題:
- CR:打字機時代,將打印頭移回行首
- LF:換到下一行
- Windows 使用
\r\n,Unix/Linux 使用\n,HTTP 選擇了 Windows 風格
TIP
雖然規範要求 CRLF,但大多數伺服器也能容忍只有 LF 的情況。
七、 用 cURL 觀察報文
查看完整請求與回應
# -v 顯示詳細資訊(包含標頭)
curl -v https://httpbin.org/get
# 輸出範例:
# > GET /get HTTP/2
# > Host: httpbin.org
# > User-Agent: curl/8.1.2
# > Accept: */*
# >
# < HTTP/2 200
# < content-type: application/json
# < content-length: 256
# <
# { "args": {}, "headers": { ... } }發送 POST 請求
# -d 指定 body,-H 指定標頭
curl -v -X POST https://httpbin.org/post \
-H "Content-Type: application/json" \
-d '{"name": "John"}'只查看標頭
# -I 只取標頭(發送 HEAD 請求)
curl -I https://example.com
# -i 顯示標頭 + body
curl -i https://example.com八、 HTTP/1.1 vs HTTP/2 報文差異
HTTP/2 將文字協定改為二進位協定:
| 特性 | HTTP/1.1 | HTTP/2 |
|---|---|---|
| 格式 | 純文字 | 二進位 |
| 讀取方式 | 行導向(CRLF 分隔) | 幀導向(Frame) |
| 標頭 | 原樣傳輸 | HPACK 壓縮 |
| 多路複用 | ❌ | ✅ |
總結
HTTP 報文結構是協定的基礎,無論是請求還是回應,都遵循相似的格式:
| 組成部分 | 請求報文 | 回應報文 |
|---|---|---|
| 起始行 | 請求行(Method + URI + Version) | 狀態行(Version + Status + Reason) |
| 標頭 | Host、User-Agent、Accept 等 | Server、Set-Cookie、Content-Type 等 |
| 空行 | CRLF | CRLF |
| Body | 請求資料(可選) | 回應資料(可選) |
掌握這個結構後,你就能:
- 閱讀和理解任何 HTTP 通訊
- 使用 cURL 等工具進行除錯
- 理解各種 HTTP 工具的運作原理
進階挑戰
實作挑戰:使用
nc(netcat)手動發送一個 HTTP 請求到httpbin.org:bashecho -e "GET /get HTTP/1.1\r\nHost: httpbin.org\r\n\r\n" | nc httpbin.org 80觀察回應並理解每個部分。
深度思考:為什麼 HTTP/2 要將協定改為二進位?純文字協定有什麼缺點?(提示:解析效率、標頭冗餘)
探索實驗:使用瀏覽器的 DevTools Network 面板,找一個 POST 請求,觀察它的 Request Headers 和 Request Payload。
延伸閱讀與資源
- RFC 9110:HTTP Semantics
- RFC 9112:HTTP/1.1
- MDN Web Docs:HTTP Messages