bigywater.tech
Go · 90

HTTP/2:Go 的自动启用条件与流并发

HTTP/2 在 Go 里是自动启用的,但“自动”有一堆前提:要有 TLS、客户端没有自定义 TLS 配置、服务端没有关闭它。多路复用也不意味着一个连接扛所有请求。这篇用自签名证书的本地服务做实验,把启用条件、一个共享 tls.Config 的坑、h2c 和并发流的实际表现都跑出来。

ForceAttemptHTTP2
自己写 Transport 并提供 TLSClientConfig 时,不设它就是 HTTP/1.1
[h2 http/1.1]
第一个 Transport 用过之后,共享的 tls.Config 的 NextProtos 被改成这个
50 个
300 个并发请求、HTTP/2、服务端默认设置、预热之后,新增的 TCP 连接数

启用条件

实测 · 本机执行输出
  HTTPS 服务器(默认配置)+ DefaultTransport 的副本(只改 TLSClientConfig 跳过证书校验):客户端看到 resp.Proto=HTTP/2.0;服务端看到 proto=HTTP/2.0
  自己写 &http.Transport{TLSClientConfig: 新建的配置}(没有 ForceAttemptHTTP2):客户端看到 resp.Proto=HTTP/1.1;服务端看到 proto=HTTP/1.1
  同上并设 ForceAttemptHTTP2: true:客户端看到 resp.Proto=HTTP/2.0;服务端看到 proto=HTTP/2.0
  服务器把 TLSNextProto 设为空 map(关闭 HTTP/2)+ 默认配置的客户端:客户端看到 resp.Proto=HTTP/1.1;服务端看到 proto=HTTP/1.1
  明文 HTTP(没有 TLS)+ 默认客户端:客户端看到 resp.Proto=HTTP/1.1;服务端看到 proto=HTTP/1.1

默认配置的服务端和 DefaultTransport 的副本,跑 TLS 就是 HTTP/2;自己新建 &http.Transport{TLSClientConfig: …} 就退回 HTTP/1.1,要加 ForceAttemptHTTP2: true(源码注释说这是为了不意外地改动用户的 tls.Config);服务端把 TLSNextProto 设成空 map 也能关掉它;明文 HTTP 默认是 HTTP/1.1。

共享 tls.Config 的坑

实测 · 本机执行输出
  共享的 tls.Config 在使用前 NextProtos = []
  第一个 Transport(ForceAttemptHTTP2)发一个请求:客户端看到 resp.Proto=HTTP/2.0;服务端看到 proto=HTTP/2.0
  之后同一个 tls.Config 的 NextProtos = [h2 http/1.1](被 Transport 改过了)
  第二个 Transport 用同一个 tls.Config、不设 ForceAttemptHTTP2:错误:Get "https://127.0.0.1:52929": net/http: HTTP/1.x transport connection broken: malformed HTTP response "\x00\x00$\x04\x00\x00\x00\x00\x00\x00\x05\x00\x10\x00\x00\x00\x03\x00\x00\x00\xfa\x00\x06\x00\x10\x01@\x00\x01\x00\x00\x10\x00\x00\x04\x00\x10\x00\x00\x00\t\x00\x00\x00\x01"

开启了 HTTP/2 的 Transport 会把 NextProtos 改成 [h2 http/1.1]。如果另一个没开 h2 的 Transport 共用同一个 tls.Config,它会向服务器宣告支持 h2,服务器选了 h2,而它自己只会说 HTTP/1.x,于是出现 “malformed HTTP response” 之类难懂的错误。每个 Transport 用自己的 tls.Config。

h2c

实测 · 本机执行输出
  明文 TCP 上的 HTTP/2(h2c):Server.Protocols 设 HTTP1 + UnencryptedHTTP2;客户端 Transport.Protocols 只设 UnencryptedHTTP2:客户端看到 resp.Proto=HTTP/2.0;服务端看到 proto=HTTP/2.0

Go 1.24 起可以用 Protocols 在明文 TCP 上跑 HTTP/2:服务端启用 UnencryptedHTTP2,客户端的 Transport 也只启用它,实验里两边都看到了 HTTP/2.0。

多路复用的实际表现

实测 · 本机执行输出
  HTTP/2,服务端默认 MaxConcurrentStreams:TCP 连接 50–50 个;同时在跑的 handler 最多 300–300 个;总用时 212–222 ms
  HTTP/2,Server.HTTP2.MaxConcurrentStreams = 10:TCP 连接 227–383 个;同时在跑的 handler 最多 96–160 个;总用时 1248–3264 ms
  HTTP/1.1(服务器关闭 h2):TCP 连接 196–299 个;同时在跑的 handler 最多 196–300 个;总用时 233–258 ms
  HTTP/2,服务端默认(冷启动):TCP 连接 153–370 个;总用时 237–267 ms
  服务端限 10 个流 + 客户端 StrictMaxConcurrentRequests = true:TCP 连接 0–0 个;同时在跑的 handler 最多 1–10 个;成功 1–10 个、失败 20–29 个

300 个并发请求,服务端默认设置、预热过一个连接后:新增了 50 个 TCP 连接,300 个 handler 同时在跑,约 215 ms 完成——单个连接的并发流有上限,超过后客户端会再开连接。把服务端的 MaxConcurrentStreams 设成 10,连接数涨到 227–383 个,用时 1.2–3.3 秒,比 HTTP/1.1(233–258 ms)还慢。冷启动时(没有现成连接)并发请求会各自拨号,TCP 连接 153–370 个。客户端设 StrictMaxConcurrentRequests 的那组实验 30 个请求在 5 秒内最多成功 10 个,原因我没有查清,所以不下结论。

文档

实测 · 本机执行输出
  MaxConcurrentStreams optionally specifies the number of concurrent streams that a client may have open at a time. If zero, MaxConcurrentStreams defaults to at least 100. // This parameter only applies to Servers.
  StrictMaxConcurrentRequests controls whether an HTTP/2 server's concurrency limit should be respected across all connections to that server. If true, new requests sent when a connection's concurrency limit has been exceeded will block until an existing request completes. If false, an additional connection will be opened if all existing connections are at their limit. //
  - HTTP1 is the HTTP/1.0 and HTTP/1.1 protocols. HTTP1 is supported on both unsecured TCP and secured TLS connections.
  - HTTP2 is the HTTP/2 protocol over a TLS connection.
  - UnencryptedHTTP2 is the HTTP/2 protocol over an unsecured TCP connection.

MaxConcurrentStreams 默认至少 100,Protocols 的三种取值把 “TLS 上的 HTTP/2” 和 “明文上的 HTTP/2” 分开。

分步演示

HTTP/2
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew);用 go run / go build / go test 直接编译运行,需要第三方库时经代理从 proxy.golang.org 下载;计时用 time.Now / testing.B,数字每次运行会有小幅抖动。服务端和客户端在同一进程里,用进程内生成的 ECDSA 自签名证书;并发流实验重复 5 次,结果是最小–最大;“预热”指先发一个请求把连接建好;读过的源码与文档:transport.go 的 onceSetNextProtoDefaults/protocols,go doc net/http.HTTP2Config / Protocols
    • 没有验证:Linux 上的结果、真实网络延迟下 HTTP/2 的收益、StrictMaxConcurrentRequests 的行为(这次的实验结果无法解释)、h2 的流控制与队头阻塞、服务端推送、HTTP/3(标准库未提供)
    京ICP备2026067502号-1京公网安备11010502063487号