光靠“用 go 写 tcp”远不足以支撑万人同屏无延迟,必须绕过 http.server、禁用 nagle、设死读写超时,并接受线上只能做到“可控、可测、不堆积”的确定性延迟。
net.conn 是起点,不是终点——想撑住万人同屏且无延迟,光靠“用 go 写 tcp”远远不够;必须绕过 http.server,禁用 nagle,设死 setreaddeadline,并接受一个事实:真正的“无延迟”只存在于单机模拟中,线上能做到的是“可控、可测、不堆积”的确定性延迟。
为什么 http.Server 不能跑游戏帧同步逻辑
HTTP 协议天生不适合实时战斗场景,这不是性能问题,而是模型冲突:
-
http.ServeMux每次请求都新建 goroutine,而游戏连接要持续数小时,goroutine 生命周期错配,极易泄漏 - 每个请求都要解析
Content-Length、Transfer-Encoding、Cookie等 header,哪怕你只传 4 字节指令,开销也固定在 0.5–2ms -
ResponseWriter不支持主动 push,客户端必须轮询或升级 WebSocket——但标准库没原生实现,得自己处理 ping/pong 和连接复用 - JSON 编解码在高频小包下触发频繁 GC,实测每秒 10k 次
json.Unmarshal就会让 STW 超过 3ms,直接突破 500ms 延迟红线
用 net.Conn 手写 TCP 服务时必设的三个参数
裸连不是“监听+读写”就完事,Linux 和 Windows 下默认 TCP 参数在弱网、高并发下会迅速暴露问题:
- 关闭 Nagle:
tcpConn.SetNoDelay(true),否则按键、移动等小包会被缓冲合并,引入 200ms 级别毛刺 - 调大接收缓冲区:
tcpConn.SetReadBuffer(64 * 1024)(Linux),防止内核缓冲区满导致丢包或阻塞读 - 强制设置读写超时:
conn.SetReadDeadline(time.Now().Add(30 * time.Second)),否则慢客户端或静默断连会卡住 goroutine,且不会触发net.ErrClosed
注意:SetWriteDeadline 也要设,尤其在批量广播时,否则一个卡顿连接会拖垮整个写协程。
conn.Read 返回 0 字节才是真断连,别只看 err != nil
很多“连接已断但程序没反应”的 case,根源是只检查 err,忽略了 n == 0 这个关键信号:
for {
n, err := conn.Read(buf[:])
if n == 0 || errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) {
break // 客户端静默关闭、kill -9、网络闪断,都走这里
}
if err != nil {
if !errors.Is(err, net.ErrTimeout) {
log.Printf("read error: %v", err)
}
continue
}
// 解包、路由、投递到逻辑层
}
漏掉 n == 0,就会让 goroutine 在 conn.Read 上永久挂起——pprof 里看到几百个 runtime.gopark 卡在 net.(*conn).Read,八成是这个原因。
写操作必须单协程 + channel,禁止并发 conn.Write
TCP 连接不是线程安全的,多个 goroutine 同时调 conn.Write 会 panic 或数据错乱。正确做法是:
- 每个连接只启一个写 goroutine,用
select监听专用 channel(如writeCh chan []byte) - channel 设缓冲(建议容量 64),避免逻辑层因写阻塞而卡住
- 写协程内部做
conn.SetWriteDeadline,失败时关 channel 并退出,通知读协程联动下线 - 心跳包、广播消息、逻辑响应全部往同一个
writeCh推,由写协程串行发出
别图省事用 sync.Mutex 包一层 Write——锁竞争在万级连接下会成为瓶颈,而且掩盖了设计缺陷。
真实压测中,最难调的从来不是吞吐,而是连接生命周期管理与错误传播路径。一个未关闭的 time.Timer、一次忘记 defer cancel() 的 context、或者写协程因 channel 关闭未退出,都会在 24 小时后突然爆发 goroutine 泄漏。上线前务必跑 go tool pprof @#@#@#@#@#@#@#@#@#@0,盯着数字别过百。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











