setkeepalive(true)仅开启内核so_keepalive开关,不保证连接存活;需配合setkeepaliveperiod(go 1.19+)及系统级tcp_keepidle/keepintvl设置,并辅以应用层心跳验证。

SetKeepAlive(true) 本身不保活,只是打开内核探测开关
调用 conn.(*net.TCPConn).SetKeepAlive(true) 只是启用了操作系统底层的 SO_KEEPALIVE 标志,它不会立刻发包,也不会保证连接“活着”。Linux 默认要空闲 **7200 秒(2 小时)** 才发第一个探测包,且中间 NAT、云负载均衡器(如阿里云 SLB、AWS ALB)普遍忽略或直接丢弃这些 TCP 层探测包。你看到 Write 还成功返回,不代表业务数据能通——连接可能早被网关静默回收了。
必须配合 SetKeepAlivePeriod,且注意 Go 版本和平台限制
Go 1.19+ 才支持 SetKeepAlivePeriod;旧版本需用 syscall.SetsockoptInt 手动设 TCP_KEEPIDLE/TCP_KEEPINTVL。即使设了 30 * time.Second,实际生效也受 OS 约束:
- Linux:传入值会被转为秒填入
TCP_KEEPIDLE,但TCP_KEEPINTVL仍可能走系统默认(如 75 秒),需额外设置 - Windows:
SetKeepAlivePeriod调用无报错,但内核不支持自定义周期,完全由系统策略决定 - macOS/BSD:行为不一致,
TCP_KEEPALIVE仅控制 idle 时间,无 interval 概念
常见误判:在 Linux 上设 30 * time.Second 后抓包发现仍是 2 小时才探测,是因为没同时配置 TCP_KEEPINTVL,或容器环境被 conntrack timeout(如 4 分钟)提前截断。
服务端必须用 ListenConfig.Control 预设,不能 Accept 后再设
对 listener.Accept() 返回的 net.Conn 调 SetKeepAlive 有竞态风险——连接可能刚建立就因未及时设置而被中间设备断开。正确做法是用 net.ListenConfig 的 Control 回调,在 socket 创建后、绑定前注入系统级选项:
lc := net.ListenConfig{
Control: func(fd uintptr) {
syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_KEEPALIVE, 1)
syscall.SetsockoptInt(fd, syscall.IPPROTO_TCP, syscall.TCP_KEEPIDLE, 30)
syscall.SetsockoptInt(fd, syscall.IPPROTO_TCP, syscall.TCP_KEEPINTVL, 10)
syscall.SetsockoptInt(fd, syscall.IPPROTO_TCP, syscall.TCP_KEEPCNT, 3)
},
}
ln, _ := lc.Listen(context.Background(), "tcp", ":8080")
注意:TCP_KEEPIDLE 单位是秒(Linux),且最小值通常 ≥ 1;设太小(如 100ms)会被内核四舍五入到 1 秒,甚至拒绝设置。
SetKeepAlivePeriod 不是银弹,必须和应用层心跳协同
内核探测只回答“链路是否可达”,不回答“对方进程是否崩溃”或“代理是否还转发”。真实生产环境(尤其跨公网、经 NAT)中,SetKeepAlivePeriod(30 * time.Second) 最多帮你把探测频率提上来,但:
- 仍无法绕过 SLB 的 4 分钟空闲断连策略
- 探测包可能被防火墙拦截,导致你收不到 RST 却实际已断
- Write 成功返回 ≠ 数据抵达对端应用,必须靠
PING/PONG响应验证
所以最终保活逻辑一定是:用 time.Ticker 每 20 秒发一次轻量心跳(如 []byte{0x01}),每次 Write 前设 SetWriteDeadline,发完立刻 Read 等 PONG,并每次成功读取后重置 SetReadDeadline。否则,光靠 SetKeepAlivePeriod,你永远不知道连接是“真活”还是“假活”。











