跳至主要內容
Skip to content

分塊傳輸編碼:不知道終點的串流傳輸

當伺服器開始產生回應時,有時候並不知道最終內容有多大。分塊傳輸編碼(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\n

2.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 進位)內容
177"Hello, "
266"World!"
結束00(空)

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: chunkedContent-Length 不能同時使用

http
# ❌ 錯誤:不能同時存在
Content-Length: 1234
Transfer-Encoding: chunked

# ✅ 正確:只用其中一個
Transfer-Encoding: chunked

# ✅ 正確:只用其中一個
Content-Length: 1234

5.2 優先級

根據 RFC 9112,如果同時存在:

  1. Transfer-Encoding 優先
  2. 忽略 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 分塊傳輸可以快取嗎?

可以,但快取時通常會:

  1. 接收完整回應
  2. 計算 Content-Length
  3. 以非分塊形式儲存

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 的重要機制,讓伺服器能「邊想邊說」,而不需要「想好了再說」。


進階挑戰

  1. 使用 Node.js 實作一個 SSE(Server-Sent Events)端點,每秒推送當前時間。
  2. 使用 cURL 的 --trace 選項觀察分塊傳輸的原始格式。
  3. 思考:如果客戶端需要顯示下載進度,分塊傳輸和 Content-Length 哪個更適合?為什麼?

延伸閱讀與資源