分塊傳輸編碼:不知道終點的串流傳輸
當伺服器開始產生回應時,有時候並不知道最終內容有多大。分塊傳輸編碼(Chunked Transfer Encoding)就是為了解決這個問題而設計的機制。
一、 為什麼需要分塊傳輸?
1.1 Content-Length 的限制
傳統 HTTP 回應需要透過 Content-Length 告訴客戶端內容大小:
http
HTTP/1.1 200 OK
Content-Length: 1234
(1234 bytes 的內容)問題:有些情況下,伺服器無法預先知道內容大小:
| 場景 | 說明 |
|---|---|
| 動態生成 | 內容在串流過程中產生 |
| 壓縮 | 壓縮後大小在壓縮完成前未知 |
| 資料庫查詢 | 結果筆數未知 |
| 即時資料 | 持續產生的資料流 |
1.2 分塊傳輸的解決方案
分塊傳輸允許伺服器邊產生邊發送,無需預先計算總大小:
二、 分塊傳輸格式
2.1 基本結構
http
HTTP/1.1 200 OK
Transfer-Encoding: chunked
<chunk-size-in-hex>\r\n
<chunk-data>\r\n
<chunk-size-in-hex>\r\n
<chunk-data>\r\n
...
0\r\n
\r\n2.2 實際範例
http
HTTP/1.1 200 OK
Content-Type: text/plain
Transfer-Encoding: chunked
7\r\n
Hello, \r\n
6\r\n
World!\r\n
0\r\n
\r\n解析:
| Chunk | 大小(16 進位) | 大小(10 進位) | 內容 |
|---|---|---|---|
| 1 | 7 | 7 | "Hello, " |
| 2 | 6 | 6 | "World!" |
| 結束 | 0 | 0 | (空) |
2.3 帶 Trailer 的分塊傳輸
可以在結束後附加額外的 Headers(Trailer):
http
HTTP/1.1 200 OK
Transfer-Encoding: chunked
Trailer: Content-MD5
7\r\n
Hello, \r\n
6\r\n
World!\r\n
0\r\n
Content-MD5: Q2h1bmtlZCBUcmFuc2Zlcg==\r\n
\r\n常見 Trailer 用途:
- 內容校驗碼(MD5、SHA)
- 數位簽章
- 處理結果資訊
三、 使用場景
3.1 Server-Sent Events (SSE)
即時推送事件到客戶端:
http
HTTP/1.1 200 OK
Content-Type: text/event-stream
Transfer-Encoding: chunked
data: {"time": "12:00:00"}\n\n
data: {"time": "12:00:01"}\n\n
data: {"time": "12:00:02"}\n\n
...3.2 大型報表匯出
邊查詢邊回傳:
javascript
// Node.js 範例
app.get("/export", async (req, res) => {
res.setHeader("Content-Type", "text/csv");
res.setHeader("Transfer-Encoding", "chunked");
// 寫入 CSV 標頭
res.write("id,name,email\n");
// 分批查詢並串流輸出
let offset = 0;
while (true) {
const users = await db.query("SELECT * FROM users LIMIT 1000 OFFSET ?", [
offset,
]);
if (users.length === 0) break;
for (const user of users) {
res.write(`${user.id},${user.name},${user.email}\n`);
}
offset += 1000;
}
res.end();
});3.3 串流壓縮
壓縮後大小未知:
http
HTTP/1.1 200 OK
Content-Type: text/html
Content-Encoding: gzip
Transfer-Encoding: chunked
(gzip 壓縮的分塊資料)3.4 AI 對話串流
ChatGPT 風格的串流回應:
javascript
// 模擬 AI 串流回應
app.post("/chat", async (req, res) => {
res.setHeader("Content-Type", "text/plain");
res.setHeader("Transfer-Encoding", "chunked");
const words = ["Hello", " ", "I", " ", "am", " ", "AI", "."];
for (const word of words) {
res.write(word);
await sleep(100); // 模擬思考時間
}
res.end();
});四、 程式碼實作
4.1 Node.js 伺服器端
javascript
const http = require("http");
const server = http.createServer((req, res) => {
// Node.js 預設就會使用 chunked encoding
res.writeHead(200, {
"Content-Type": "text/plain",
});
// 分多次寫入
res.write("第一部分資料\n");
setTimeout(() => {
res.write("第二部分資料\n");
}, 1000);
setTimeout(() => {
res.write("第三部分資料\n");
res.end(); // 結束串流
}, 2000);
});
server.listen(3000);4.2 客戶端讀取(Fetch API)
javascript
async function readChunkedResponse() {
const response = await fetch("/api/stream");
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) {
console.log("傳輸完成");
break;
}
const text = decoder.decode(value, { stream: true });
console.log("收到 chunk:", text);
}
}4.3 Express.js 範例
javascript
const express = require("express");
const app = express();
app.get("/stream", (req, res) => {
res.setHeader("Content-Type", "application/json");
res.setHeader("Transfer-Encoding", "chunked");
let count = 0;
const interval = setInterval(() => {
res.write(JSON.stringify({ count: count++ }) + "\n");
if (count >= 10) {
clearInterval(interval);
res.end();
}
}, 500);
// 處理客戶端斷線
req.on("close", () => {
clearInterval(interval);
});
});
app.listen(3000);五、 與 Content-Length 的關係
5.1 互斥關係
Transfer-Encoding: chunked 和 Content-Length 不能同時使用:
http
# ❌ 錯誤:不能同時存在
Content-Length: 1234
Transfer-Encoding: chunked
# ✅ 正確:只用其中一個
Transfer-Encoding: chunked
# ✅ 正確:只用其中一個
Content-Length: 12345.2 優先級
根據 RFC 9112,如果同時存在:
Transfer-Encoding優先- 忽略
Content-Length
5.3 選擇指南
| 情況 | 建議 |
|---|---|
| 知道確切大小 | 使用 Content-Length |
| 大小未知 | 使用 Transfer-Encoding: chunked |
| 串流資料 | 使用 Transfer-Encoding: chunked |
| 需要進度顯示 | 使用 Content-Length |
六、 HTTP/2 和 HTTP/3 的變化
6.1 HTTP/2 中的分塊傳輸
HTTP/2 沒有 Transfer-Encoding: chunked!
因為 HTTP/2 使用**幀(Frame)**機制,天生就是分塊的:
HTTP/1.1: Transfer-Encoding: chunked
HTTP/2: DATA frames (自動分塊)6.2 升級相容性
當 HTTP/1.1 請求透過代理升級到 HTTP/2:
- 代理會移除
Transfer-Encoding標頭 - 使用 HTTP/2 的 DATA frames 傳輸
七、 常見問題
7.1 如何偵測傳輸結束?
- 有 Content-Length:讀取指定大小後結束
- 有 Chunked:收到
0\r\n\r\n表示結束 - 都沒有:連線關閉表示結束(不建議)
7.2 分塊傳輸可以快取嗎?
可以,但快取時通常會:
- 接收完整回應
- 計算 Content-Length
- 以非分塊形式儲存
7.3 分塊傳輸支援 Range 請求嗎?
不支援。Range 請求需要知道總大小:
http
# ❌ 不合法的組合
Transfer-Encoding: chunked
Content-Range: bytes 0-499/1000總結
| 概念 | 說明 |
|---|---|
| 分塊傳輸 | 大小未知時的傳輸方式 |
| 格式 | <size-hex>\r\n<data>\r\n 重複,以 0\r\n\r\n 結束 |
| Trailer | 傳輸結束後附加的 Headers |
| 使用場景 | SSE、串流、壓縮、大型匯出 |
| HTTP/2 | 不需要,使用 DATA frames |
> **記住**:分塊傳輸是 HTTP/1.1 的重要機制,讓伺服器能「邊想邊說」,而不需要「想好了再說」。
進階挑戰
- 使用 Node.js 實作一個 SSE(Server-Sent Events)端點,每秒推送當前時間。
- 使用 cURL 的
--trace選項觀察分塊傳輸的原始格式。 - 思考:如果客戶端需要顯示下載進度,分塊傳輸和 Content-Length 哪個更適合?為什麼?