← 中文资源

简体中文版本

HTTP/3 与 QUIC

HTTP/3 基于 QUIC 传输,QUIC 是 UDP 之上的流式传输协议,自带 TLS 1.3,按流独立丢包恢复,彻底消除 TCP 层队头阻塞。

阅读英文原文

概念定义

HTTP/3 保留了方法、头部、状态码等 HTTP 语义,但把传输层换成 QUIC。QUIC 在 UDP 之上以用户态实现了流、流控、拥塞控制与 TLS 1.3 加密。每个流独立进行丢包恢复,单个丢包只会停顿它所在的流。

工作机制

客户端通常先经 HTTP/2 的 Alt-Svc 响应头发现 HTTP/3 端点,再在 UDP 上发起 QUIC 连接。加密与传输握手合并,通常一个往返即可建立密钥和连接状态。连接标识符独立于 IP 地址,使 Wi-Fi 与蜂窝网络切换时连接可以迁移。

使用场景

高丢包或高延迟网络收益最大:按流恢复避免停顿,更少的握手缩短首字节时间。视频会议、大文件传输与移动网页浏览是典型赢家。服务端需具备 UDP 容量和成熟的 QUIC 实现再开启。

常见误区

QUIC 不是 TLS 的替代,而是内嵌 TLS 1.3 完成握手与加密。QUIC 基于 UDP 但自行实现了可靠有序传输。部分企业防火墙会限速或封禁 UDP,务必保留 HTTP/2 回退;软件实现的 QUIC 每连接 CPU 开销可能高于内核 TCP。灰度后应持续观察视频卡顿率与连接错误率。

代码示例

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)
}
package main

import (
	"context"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"time"
)

type fetchResult struct {
	Status int
	Body   string
}

func fetchWithDeadline(ctx context.Context, client *http.Client, url string, timeout time.Duration) (fetchResult, error) {
	ctx, cancel := context.WithTimeout(ctx, timeout)
	defer cancel()

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return fetchResult{}, fmt.Errorf("build request: %w", err)
	}

	resp, err := client.Do(req)
	if err != nil {
		if errors.Is(ctx.Err(), context.DeadlineExceeded) {
			return fetchResult{}, fmt.Errorf("fetch %s: deadline exceeded after %s: %w", url, timeout, context.DeadlineExceeded)
		}
		return fetchResult{}, fmt.Errorf("fetch %s: %w", url, err)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
	if err != nil {
		return fetchResult{}, fmt.Errorf("read body: %w", err)
	}
	return fetchResult{Status: resp.StatusCode, Body: string(body)}, nil
}

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/fast", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "ok")
	})
	mux.HandleFunc("/slow", func(w http.ResponseWriter, r *http.Request) {
		select {
		case <-time.After(200 * time.Millisecond):
			fmt.Fprint(w, "late")
		case <-r.Context().Done():
		}
	})
	server := httptest.NewServer(mux)
	defer server.Close()

	client := &http.Client{Timeout: 5 * time.Second}
	ctx := context.Background()

	result, err := fetchWithDeadline(ctx, client, server.URL+"/fast", 500*time.Millisecond)
	fmt.Printf("fast: status=%d body=%q err=%v\n", result.Status, result.Body, err)

	result, err = fetchWithDeadline(ctx, client, server.URL+"/slow", 50*time.Millisecond)
	fmt.Printf("slow: status=%d body=%q err=%v\n", result.Status, result.Body, err)
	if errors.Is(err, context.DeadlineExceeded) {
		fmt.Println("slow request classified as a deadline error")
	}
}