SetKeepAlive通常不够用,因其仅触发内核级TCP探测(Linux默认2小时才发首包),无法穿透NAT/防火墙(常30–300秒超时断连),且不验证应用层存活;必须配合time.Ticker定时发心跳包+读写deadline实现可控、可验证、快速失败的保活。

为什么 SetKeepAlive 通常不够用
Go 的 net.Conn 确实提供了 SetKeepAlive 和 SetKeepAlivePeriod,但它们只控制底层 TCP 的 SO_KEEPALIVE 选项,依赖操作系统默认行为(Linux 默认 2 小时才发第一个探测包),远超业务要求的秒级保活。更关键的是:NAT 设备、中间防火墙常在 30–120 秒无流量后主动踢掉连接,TCP keepalive 根本等不到它触发。
所以必须在应用层实现心跳——即定期发送自定义心跳包,并配合读写超时与错误判断。
客户端心跳发送与超时判定
核心是启动一个独立 goroutine,按固定间隔写入心跳消息(如空字节或 "PING"),同时为每次写操作设置短超时。一旦写失败或超时,立即关闭连接并通知上层重连。
-
SetWriteDeadline必须每次写前设置,不能只设一次;否则后续写会沿用过期时间 - 心跳间隔建议设为 15–30 秒,超时设为 5–10 秒,留出网络抖动余量
- 避免用
time.Ticker直接阻塞发送:若写阻塞,ticker 会持续累积,导致爆发式重发
// 示例:客户端心跳 goroutine
go func() {
ticker := time.NewTicker(20 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
_, err := conn.Write([]byte("PING\n"))
if err != nil {
log.Println("heartbeat write failed:", err)
conn.Close()
return
}
}
}
}()服务端如何安全响应并检测断连
服务端不能被动等待心跳,而要对每个连接维护读超时(SetReadDeadline),并在收到 "PING" 时立刻回 "PONG",同时刷新读超时。这样只要客户端停止发心跳,服务端会在超时后自动关闭连接。
- 读超时值应略大于客户端心跳间隔(如客户端 20s 心跳,服务端设 25s 读超时)
- 必须在每次成功读取后调用
SetReadDeadline,包括读到心跳和业务数据两种情况 - 不要单独为心跳开协程处理:容易与主读循环竞争连接状态,直接在主读逻辑里
if bytes.Equal(line, []byte("PING\n"))判断即可
连接关闭时的资源清理陷阱
心跳 goroutine 和主读/写 goroutine 共享同一个 conn,但 conn.Close() 是并发安全的——多次调用无害,但关闭后继续读写会返回 "use of closed network connection" 错误。问题在于:谁该负责关闭?怎么通知其他 goroutine 退出?
- 推荐用
sync.Once+chan struct{}统一关闭信号:任一环节发现异常(写失败、读超时、解析错误),都通过once.Do(closeFunc)触发关闭 - 所有 goroutine 都需监听同一
donechannel,在select中加入case 优雅退出 - 务必在
defer conn.Close()前加recover(),防止 panic 导致连接泄漏
真正难的不是发心跳,而是让所有相关 goroutine 在连接失效瞬间同步感知并退出——漏掉任意一个,都会导致 goroutine 泄漏或反复报错。

















