正确做法是用time.newticker驱动心跳,配合select超时控制;错误做法是用time.afterfunc递归调用,易因write阻塞导致间隔漂移失步。

Go里用time.Ticker实现固定节奏Ping-Pong
别用time.AfterFunc递归调Ping,它会因前一次write阻塞而漂移,导致心跳间隔越来越不准。真正稳的方式是time.NewTicker驱动,配合select超时控制,避免单次write卡死整个ticker。
常见错误写法:time.AfterFunc(20*time.Second, sendPing),然后在sendPing末尾再调一次——一旦网络抖动或对方接收缓冲区满,Write返回syscall.EAGAIN或卡住3秒,下一次就晚于20秒,连续几次后心跳彻底失步。
正确做法:
- 启动
ticker := time.NewTicker(20 * time.Second),defer ticker.Stop() - 每次
触发后,先<code>conn.SetWriteDeadline(time.Now().Add(5 * time.Second)) - 再调
conn.Write([]byte("PING")),失败不立即断连,只记日志 - 紧接着
conn.SetReadDeadline(time.Now().Add(10 * time.Second)),conn.Read等"PONG",超时或读到非"PONG"才判定异常
gorilla/websocket的Ping/Pong必须手动配SetPongHandler
gorilla/websocket不会自动响应Ping,也不自动重置读超时。如果只调conn.WriteControl(websocket.PingMessage, nil, ...)却不设SetPongHandler,客户端发来的Ping会被静默丢弃,服务端在ReadMessage时因超时直接关闭连接。
关键点:
-
conn.SetPongHandler必须在握手完成后立刻设置,且handler内只做轻量操作:比如atomic.StoreInt64(&lastPong, time.Now().Unix()) - 不能在handler里查DB、调HTTP、
time.Sleep——它运行在读协程中,一卡就整条连接hang住 - 每次成功收到Pong后,要主动调
conn.SetReadDeadline(time.Now().Add(45 * time.Second)),否则读超时不会刷新 - 服务端主动发Ping时,必须用
WriteControl(不是WriteMessage),并传入写deadline,否则write tcp: use of closed network connection可能悄无声息发生
心跳包内容越简单越可靠
别用JSON、带时间戳、含空格或换行的心跳协议。见过真实案例:json.Marshal(map[string]string{"type":"ping","ts":time.Now().String()})生成的字符串被Nginx或云WAF当成非法协议直接reset连接。
推荐方案:
- 客户端发
"PING\000"(4字节+空终止符),服务端严格比对前4字节 - 服务端回
"PONG\000",不带任何额外字段 - 所有心跳交互走
conn.Write/conn.Read原始字节,绕过bufio或json.Decoder等中间层 - 如果必须扩展,用固定长度二进制头(如1字节type + 4字节seq),避免解析歧义
Nginx等中间件proxy_read_timeout必须显式调大
代码里设了30秒心跳、45秒读超时,但Nginx默认proxy_read_timeout 60s,实际连接会在第61秒被它单方面切断,且不发FIN,客户端和服务端都以为连接还活着。
必须同步调整中间件配置:
- Nginx:把
proxy_read_timeout设为至少75s(大于服务端最大空闲窗口) - 同时加
proxy_set_header Connection '',防止它复用连接干扰WebSocket升级 - AWS ALB/Google Cloud Load Balancing:检查其Idle Timeout,常见值是60~3500秒,不能依赖默认
- K8s Ingress Controller(如nginx-ingress):对应annotation也要显式覆盖,比如
nginx.ingress.kubernetes.io/proxy-read-timeout: "75"
真正容易被忽略的是:这个配置要和代码里的SetReadDeadline值对齐,差5秒都可能引发偶发断连。不是设了心跳就万事大吉,中间链路每一环都得显式兜底。











