← 中文资源

简体中文版本

分块传输编码

分块传输编码把长度未知的响应拆成一系列带长度前缀的块,以零长度块结束,让服务器无需先算出总长即可渐进式发送。

阅读英文原文

概念定义

分块编码是 HTTP/1.1 中传递“总长未知”响应体的机制:每个块以十六进制长度行开头,零长度块表示结束,之后还可跟可选 trailer。它让服务器免于在发送前计算 Content-Length。

工作机制

发送方声明 Transfer-Encoding: chunked 且不带 Content-Length;接收方拼接所有块,并可在终止块后读取 trailer。在 HTTP/2 与 HTTP/3 中该概念消失:帧与流本身已按长度定界,chunked 头会被转译而非原样传输。

使用场景

渐进式 HTML 渲染、事件流、模型逐 token 输出都依赖它。但前置的反向代理若整体缓冲会悄悄摧毁流式特性,因此 nginx 的 proxy_buffering off 之类配置与应用代码同等重要。

常见误区

分块是传输层成帧,不等于向用户流式渲染,flush 时机由应用决定。分块响应难以支持断点续传,因为总长要到结束才知道。trailer 在各客户端支持参差,关键数据应放头部。压缩与分块叠加时,压缩窗口不满可能迟迟不吐字节。

代码示例

package main

import (
	"compress/gzip"
	"fmt"
	"net/http"
	"strings"
)

type gzipWriter struct {
	http.ResponseWriter
	writer *gzip.Writer
}

func (g *gzipWriter) Write(data []byte) (int, error) {
	return g.writer.Write(data)
}

func GzipMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Add("Vary", "Accept-Encoding")
		if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
			next.ServeHTTP(w, r)
			return
		}
		w.Header().Set("Content-Encoding", "gzip")
		compressor := gzip.NewWriter(w)
		defer compressor.Close()
		next.ServeHTTP(&gzipWriter{ResponseWriter: w, writer: compressor}, r)
	})
}

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "text/plain; charset=utf-8")
		fmt.Fprintln(w, strings.Repeat("hello gzip ", 64))
	})
	_ = http.ListenAndServe(":8080", GzipMiddleware(mux))
}
export function parseAcceptHeader(header) {
  if (typeof header !== "string" || header.trim() === "") {
    return [{ type: "*", subtype: "*", q: 1, params: {}, order: 0 }];
  }
  const ranges = [];
  for (const [order, part] of header.split(",").entries()) {
    const segments = part.split(";").map((segment) => segment.trim());
    const mediaRange = segments.shift();
    if (!mediaRange) continue;
    const slash = mediaRange.indexOf("/");
    if (slash <= 0 || slash === mediaRange.length - 1) {
      throw new SyntaxError("invalid media range: " + mediaRange);
    }
    const type = mediaRange.slice(0, slash).toLowerCase();
    const subtype = mediaRange.slice(slash + 1).toLowerCase();
    const params = {};
    let q = 1;
    for (const segment of segments) {
      if (!segment) continue;
      const equals = segment.indexOf("=");
      if (equals <= 0) throw new SyntaxError("invalid parameter: " + segment);
      const name = segment.slice(0, equals).trim().toLowerCase();
      const raw = segment.slice(equals + 1).trim().replace(/^"|"$/g, "");
      if (name === "q") {
        q = Number(raw);
        if (!Number.isFinite(q) || q < 0 || q > 1) {
          throw new RangeError("invalid q value: " + raw);
        }
      } else {
        params[name] = raw;
      }
    }
    ranges.push({ type, subtype, q, params, order });
  }
  if (ranges.length === 0) throw new SyntaxError("Accept header has no media ranges");
  return ranges;
}

export function negotiateContentType(header, supported) {
  if (!Array.isArray(supported) || supported.length === 0) {
    throw new TypeError("supported must be a non-empty array");
  }
  const ranges = parseAcceptHeader(header);
  const candidates = [];
  for (const [order, mediaType] of supported.entries()) {
    const slash = mediaType.indexOf("/");
    if (slash <= 0) throw new TypeError("invalid supported media type: " + mediaType);
    const type = mediaType.slice(0, slash).toLowerCase();
    const subtype = mediaType.slice(slash + 1).toLowerCase();
    let best;
    for (const range of ranges) {
      const typeMatches = range.type === "*" || range.type === type;
      const subtypeMatches = range.subtype === "*" || range.subtype === subtype;
      if (!typeMatches || !subtypeMatches) continue;
      const specificity = (range.type === "*" ? 0 : 1) + (range.subtype === "*" ? 0 : 1);
      if (!best || specificity > best.specificity ||
        (specificity === best.specificity && range.order < best.order)) {
        best = { q: range.q, specificity, order: range.order };
      }
    }
    if (best && best.q > 0) {
      candidates.push({ mediaType, q: best.q, specificity: best.specificity, order });
    }
  }
  candidates.sort((a, b) => b.q - a.q || b.specificity - a.specificity || a.order - b.order);
  return candidates.length > 0 ? candidates[0].mediaType : null;
}