bigywater.tech
Go · 87

net/http 服务端:一个连接从 Accept 到 Handler 的路径

一个 HTTP 请求从 Accept 到 Handler,中间发生了什么?这篇用原始 TCP 客户端和 ConnState 钩子,实测连接状态、goroutine 数、响应头的生成规则、panic 的处理和客户端断开的感知,并对着 server.go 的源码确认每个连接多出的那个 goroutine。

+1000
500 个连接各有一个请求在 Handler 里时,goroutine 的增量(每个连接约 2 个)
chunked
Flush 过或超过缓冲的响应用 Transfer-Encoding: chunked,小响应自动算 Content-Length
0 字节
Handler panic 时客户端收到的响应:连接被关掉,进程不崩

连接的生命周期

实测 · 本机执行输出
  一个 TCP 连接上连续发 2 个请求再关闭,ConnState 依次是:new → active → idle → active → idle → closed

用 ConnState 钩子看到,一个 TCP 连接依次是 new → active → idle → active → idle → closed:keep-alive 让同一个连接服务多个请求,请求之间是 idle。

goroutine 数

实测 · 本机执行输出
  500 个连接各有一个请求卡在 Handler 里:goroutine 数 2 → 1002(增加 1000,约每个连接 2.0 个)
  放行并关闭连接后 goroutine 数 2

500 个连接各有一个请求卡在 Handler 里,goroutine 增加 1000,即每个连接约 2 个;放行并关闭连接后回到基线。文档说 Serve 为每个连接创建一个服务 goroutine,多出来的那个是 Handler 运行期间的后台读 goroutine(server.go 里的 startBackgroundRead)。

响应头自动生成

实测 · 本机执行输出
  /small  → HTTP/1.1 200 OK | Content-Length: 100 | Content-Type: text/plain; charset=utf-8;body 100 字节
  /flush  → HTTP/1.1 200 OK | Content-Type: text/plain; charset=utf-8 | Transfer-Encoding: chunked;body 10 字节
  /big    → HTTP/1.1 200 OK | Content-Type: text/plain; charset=utf-8 | Transfer-Encoding: chunked;body 10000 字节
  /twice  → HTTP/1.1 200 OK | Content-Length: 1 | Content-Type: text/plain; charset=utf-8;body 1 字节
  /twice 时服务器的错误日志:"http: superfluous response.WriteHeader call from main.main.func7 (main.go:95)"

100 字节的响应带 Content-Length: 100;Handler 里 Flush 过,或者响应较大(10000 字节),就是 Transfer-Encoding: chunked;没设类型时自动嗅探成 text/plain; charset=utf-8。重复调用 WriteHeader 不生效,服务器日志会出现 “superfluous response.WriteHeader call”。

Handler panic

实测 · 本机执行输出
  请求 /boom:客户端收到 0 字节(""),读结束原因 <nil>——连接被直接关掉了
  之后再请求 /ok:正常返回 "ok"(进程没有崩溃)
  服务器日志第一行:http: panic serving 127.0.0.1:55085: handler 里 panic

服务器恢复这个 panic、记一行 http: panic serving、关掉这个连接:客户端读到 0 字节,其它请求不受影响,进程不崩。

客户端断开

实测 · 本机执行输出
  客户端在请求发出 100 ms 后直接关闭连接:handler 在 100 ms 后发现 r.Context() 已取消:context canceled

客户端中途关闭连接:后台读发现连接关闭,r.Context() 被取消(实验里约 100 ms 的测量粒度内就取消了),Handler 应该监听 ctx.Done() 尽早收手。

源码位置

实测 · 本机执行输出
  A ConnState represents the state of a client connection to a server. It's used by the optional Server.ConnState hook. const StateNew ConnState = iota ... func (c ConnState) String() string
  Serve accepts incoming connections on the Listener l, creating a new service goroutine for each. The service goroutines read requests and then call s.Handler to reply to them. HTTP/2 support is only enabled if the Listener returns *tls.Conn connections and they were configured with "h2" in the TLS Config.NextProtos. Serve always returns a non-nil error and closes l. After Server.Shutdown or Server.Close, the returned error is ErrServerClosed.
  server.go:731: func (cr *connReader) startBackgroundRead() {
  server.go:742: go cr.backgroundRead()
  server.go:2126: w.conn.r.startBackgroundRead()

在本机的 server.go 里,startBackgroundRead 在 731 行,里面第 742 行启动后台 goroutine,第 2126 行调用它。行号只对 Go 1.27.1 有效。

分步演示

net/http 服务端
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew);用 go run / go build / go test 直接编译运行,需要第三方库时经代理从 proxy.golang.org 下载;计时用 time.Now / testing.B,数字每次运行会有小幅抖动。服务端用 net/http.Server,客户端用原始 net.Dial 写 HTTP/1.1 请求并手工解析响应头;ConnState 钩子记录状态序列;goroutine 数用 runtime.NumGoroutine;读过的文档与源码:go doc net/http.ConnState / Server.Serve,src/net/http/server.go 的 startBackgroundRead
    • 没有验证:HTTP/2 的情形(单独一篇)、TLS 连接、请求体很大时的读取行为、Server.Shutdown 的等待逻辑、pipelining、响应头的完整生成规则(只看了 Content-Length、chunked、Content-Type)、Linux 上的差别
    京ICP备2026067502号-1京公网安备11010502063487号