心跳包丢弃时Write不报错但连接失效的典型现象是conn.Write([]byte("PING"))返回nil而后续Read立刻返回io.EOF或超时,根本原因是TCP层仍认为连接存活但应用层数据被静默丢弃;解决办法是发PING后立即Read等待PONG并每次Read前重设SetReadDeadline,超时或异常则关闭连接。

心跳包丢弃时,Write 不报错但连接已失效
常见现象是 conn.Write([]byte("PING")) 返回 nil,但后续 Read 立刻返回 io.EOF 或超时。这是因为 TCP 层仍认为连接“活着”(比如中间 NAT 设备未清理),而应用层数据已被静默丢弃。
解决办法不是只盯 Write 结果,而是必须配合 Read 验证响应:
- 发完
PING后立刻调用conn.Read等待PONG,并设略大于心跳间隔的 deadline(如心跳 20s,deadline 设 25s) -
SetReadDeadline必须每次Read前重设,不能只设一次 - 若
Read返回io.EOF、net.OpError(timeout 或 connection reset)、或非预期内容,立即关闭连接
高并发下避免为每个连接起 goroutine 做心跳检测
每连接一个 time.Ticker + goroutine 在万级连接时会吃光调度器资源,且 ticker 本身有内存开销。
推荐用集中式心跳扫描器:
立即学习“go语言免费学习笔记(深入)”;
- 所有连接注册到一个
sync.Map,key 是 conn 或唯一 ID,value 包含lastPingAt和conn - 单个全局
time.Ticker(如 5s 一 tick)遍历 map,对超时连接(如time.Since(lastPingAt) > 60s)执行conn.Close() - 写操作加读锁,关连接前用
LoadAndDelete防止重复 close
SetKeepAlive 开了也断连?那是没配对生效
conn.SetKeepAlive(true) 只触发 OS 级探测,Linux 默认 2 小时才发第一个 probe,远不够应对 NAT 超时(通常 30–180s)。而且它不保证应用层能及时感知断连。
必须组合使用:
- Go 1.19+:调用
conn.SetKeepAlivePeriod(30 * time.Second)缩短探测间隔(注意最小值受系统限制,实际可能四舍五入到秒) - 旧版 Go:需转成
*net.TCPConn,用syscall.SetsockoptInt手动设TCP_KEEPINTVL和TCP_KEEPIDLE - 即便开了 keepalive,仍要应用层心跳——OS 探测失败后,连接状态变为
ESTABLISHED → FIN_WAIT1,但你的Read可能卡住数秒才返回 error
心跳内容轻量且协议中立
见过用 json.Marshal(map[string]interface{}{"type":"ping","ts":time.Now().UnixMilli()}) 发心跳,结果被云防火墙或 L4 代理当成非法协议直接 reset 连接。
真正可靠的心跳包应:
- 固定长度、无空格/换行/特殊字符,例如纯 ASCII:
"P"或"\x01" - 服务端只校验前几个字节,不解析结构体
- 避免依赖时间戳、随机数等导致长度/内容不可控的字段
- 如果走 WebSocket,优先用
websocket.PingMessage,它由 gorilla/websocket 库原生支持,且 Pong 自动回传
最易被忽略的点:心跳逻辑里没有双向验证,只发不收;或 deadline 没重设,导致某次 read 超时后,后续所有 read 都立刻失败。保活不是“发出去就行”,而是“确认对方收到了”。


















