← 中文资源

简体中文版本

HTTP/2 连接合并

连接合并让浏览器在多个域名解析到同一 IP 且证书覆盖两个主机名时,用一条 HTTP/2 连接服务多个源,省去重复握手。

阅读英文原文

概念定义

当多个主机名终结在同一台服务器时,按源各开一条 TLS 连接很浪费。合并把这些源折叠进一条连接:浏览器确认目标解析到同一地址、证书覆盖所请求的名称,然后在该连接上复用两个源的请求。

工作机制

连接到源 A 之后,对源 B 的请求若 DNS 结果相同且证书对 B 有效,即可复用连接。浏览器仍执行基于源的安全规则:Cookie、CORS 与缓存分区都视两源为独立。HTTP/3 中 Alt-Svc 可把兄弟主机指向同一 QUIC 端点,达到同样效果。

使用场景

把 API、应用与静态主机放在同一 CDN 入口的架构,配合泛域名或 SAN 证书几乎免费获得合并收益,移动端握手延迟显著下降,服务端连接状态也减少。

常见误区

DNS 变更导致源不再共享 IP 时会静默失去合并能力,这种回归极易漏察。证书缺少某个主机名时即使 IP 相同也会校验失败。合并只是传输层行为,不是安全边界:跨源的 Cookie 与 CORS 规则依然完全适用。

代码示例

package main

import (
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
)

var payload = []byte("configuration-v1")

func fetchConditional(
	client *http.Client,
	url string,
	etag string,
) (body []byte, newETag string, notModified bool, err error) {
	request, err := http.NewRequest(http.MethodGet, url, nil)
	if err != nil {
		return nil, "", false, err
	}
	if etag != "" {
		request.Header.Set("If-None-Match", etag)
	}

	response, err := client.Do(request)
	if err != nil {
		return nil, "", false, err
	}
	defer response.Body.Close()

	if response.StatusCode == http.StatusNotModified {
		return nil, etag, true, nil
	}
	if response.StatusCode != http.StatusOK {
		return nil, "", false, fmt.Errorf("unexpected status %d", response.StatusCode)
	}
	body, err = io.ReadAll(io.LimitReader(response.Body, 1<<20))
	if err != nil {
		return nil, "", false, err
	}
	return body, response.Header.Get("ETag"), false, nil
}

func main() {
	requests := 0
	server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		requests++
		writer.Header().Set("ETag", "\"v1\"")
		if request.Header.Get("If-None-Match") == "\"v1\"" {
			writer.WriteHeader(http.StatusNotModified)
			return
		}
		_, _ = writer.Write(payload)
	}))
	defer server.Close()

	client := server.Client()
	body, etag, notModified, err := fetchConditional(client, server.URL, "")
	if err != nil || notModified || string(body) != string(payload) || etag == "" {
		panic("first request should download the payload")
	}
	fmt.Printf("first: body=%q etag=%s\n", body, etag)

	body, _, notModified, err = fetchConditional(client, server.URL, etag)
	if err != nil || !notModified || len(body) != 0 || requests != 2 {
		panic("second request should return 304 without a body")
	}
	fmt.Printf("second: notModified=%v requests=%d\n", notModified, requests)
}
import { Agent, fetch } from 'undici';

const agent = new Agent({
  connections: 128,
  pipelining: 10,
  keepAliveTimeout: 60_000,
  keepAliveMaxTimeout: 300_000,
});

const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 5000);

const res = await fetch('https://api.example.com/users', {
  dispatcher: agent,
  signal: controller.signal,
});
const users = await res.json();
clearTimeout(timer);

agent.close();