← 中文资源

简体中文版本

HTTP/3

HTTP/3 基于 QUIC 与 UDP 承载 HTTP,消除 TCP 队头阻塞,使丢一个包不阻塞无关的流。

阅读英文原文

概念定义

HTTP/3 保留 HTTP 的方法、状态码与头部,但替换了传输层。它不用 TCP 加 TLS,而用 QUIC:流是 QUIC 的一等概念,因此一个数据包丢失只延迟承载它的那条流。QUIC 基于 UDP 在用户空间运行,这使它比操作系统的 TCP 栈演进更快,并把加密握手整合进来,让连接建立与加密协商一起完成。

为什么重要

在 TCP 下,所有多路复用的 HTTP/2 流共享一条有序字节流,因此一个丢失的分段会卡住所有流直到它被重传——这在移动与易丢包链路上是实实在在的惩罚。HTTP/3 消除了这种耦合,恰在丢包常见之处兑现了多路复用的承诺。它还缩短握手,让重复访问连接更快,并让应用掌控传输而非等待操作系统改动。

工程实践

通过 Alt-Svc 或 DNS HTTPS 记录广播 HTTP/3,让客户端能发现它,并为无法或不宜使用它的客户端保留 HTTP/2。核实负载均衡器、防火墙与监控能正确处理 UDP,因为许多都假设 TCP。在真实的易丢包网络上测量性能,而非干净的实验环境,并意识到 QUIC 在某些栈中比 TCP 更耗 CPU。

代码示例

const HEADER_NAME = /^[!#$%&'*+\-.^_\x60|~0-9a-z]+$/;

export class HeaderBlock {
  #fields = new Map();

  add(name, value) {
    const key = name.toLowerCase();
    if (!HEADER_NAME.test(key)) throw new SyntaxError("invalid header name: " + name);
    const current = this.#fields.get(key);
    if (current === undefined) {
      this.#fields.set(key, value);
    } else if (key === "set-cookie") {
      const list = Array.isArray(current) ? current : [current];
      this.#fields.set(key, [...list, value]);
    } else {
      this.#fields.set(key, current + ", " + value);
    }
  }

  append(name, value) {
    const key = name.toLowerCase();
    const current = this.#fields.get(key);
    if (current === undefined) throw new SyntaxError("continuation line without a header");
    if (Array.isArray(current)) {
      const last = current[current.length - 1];
      this.#fields.set(key, [...current.slice(0, -1), last + " " + value]);
    } else {
      this.#fields.set(key, current + " " + value);
    }
  }

  get(name) {
    const value = this.#fields.get(name.toLowerCase());
    if (value === undefined) return null;
    return Array.isArray(value) ? value.join(", ") : value;
  }

  getAll(name) {
    const value = this.#fields.get(name.toLowerCase());
    if (value === undefined) return [];
    return Array.isArray(value) ? [...value] : [value];
  }

  has(name) {
    return this.#fields.has(name.toLowerCase());
  }

  names() {
    return [...this.#fields.keys()];
  }

  toObject() {
    const output = Object.create(null);
    for (const [name, value] of this.#fields) {
      output[name] = Array.isArray(value) ? [...value] : value;
    }
    return output;
  }
}

export function parseHeaderBlock(raw, options = {}) {
  if (typeof raw !== "string") throw new TypeError("raw headers must be a string");
  const { maxLines = 200, maxBytes = 64 * 1024 } = options;
  if (!Number.isInteger(maxLines) || maxLines < 1) {
    throw new RangeError("maxLines must be a positive integer");
  }
  if (!Number.isInteger(maxBytes) || maxBytes < 1) {
    throw new RangeError("maxBytes must be a positive integer");
  }
  const byteLength = new TextEncoder().encode(raw).length;
  if (byteLength > maxBytes) throw new RangeError("header block exceeds " + maxBytes + " bytes");

  const lines = raw.replace(/\r\n?/g, "\n").split("\n");
  const headers = new HeaderBlock();
  let lastName = null;
  let lineCount = 0;
  for (const line of lines) {
    if (line === "") break;
    lineCount += 1;
    if (lineCount > maxLines) throw new RangeError("header block exceeds " + maxLines + " lines");
    if (/^[ \t]/.test(line)) {
      if (lastName === null) throw new SyntaxError("continuation line without a header");
      headers.append(lastName, line.trim());
      continue;
    }
    const colon = line.indexOf(":");
    if (colon <= 0) throw new SyntaxError("invalid header line: " + line);
    const name = line.slice(0, colon).trim();
    const value = line.slice(colon + 1).trim();
    headers.add(name, value);
    lastName = name.toLowerCase();
  }
  return headers;
}
package main

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"time"
)

type Shipment struct {
	ID     string `json:"id"`
	Status string `json:"status"`
}

func queryShipment(ctx context.Context, baseURL, id string) (*Shipment, error) {
	ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
	defer cancel() // 必须释放计时器资源

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+"/shipments/"+id, nil)
	if err != nil {
		return nil, fmt.Errorf("build request: %w", err)
	}
	req.Header.Set("Accept", "application/json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		if errors.Is(err, context.DeadlineExceeded) {
			return nil, fmt.Errorf("shipment %s timed out after 3s: %w", id, err)
		}
		return nil, fmt.Errorf("request failed: %w", err)
	}
	defer resp.Body.Close()

	var shipment Shipment
	if err := json.NewDecoder(resp.Body).Decode(&shipment); err != nil {
		return nil, fmt.Errorf("decode body: %w", err)
	}
	return &shipment, nil
}

func main() {
	s, err := queryShipment(context.Background(), "https://api.example.com", "sh-1")
	fmt.Println(s, err)
}