因为TCP层不感知应用层存活,NAT/防火墙等中间设备在30–300秒空闲后静默断连,而Go标准库及gorilla/websocket默认无心跳机制,需业务层主动实现Ping/Pong并设超时控制。

为什么 WebSocket 连接会悄无声息断开?
因为 TCP 层不感知应用层是否“活着”,浏览器或服务端空闲超时后直接关闭连接,net.Conn 不报错,Read 也不返回,直到你试图写数据才发现 write: broken pipe 或 i/o timeout。Go 的 gorilla/websocket 默认不启用心跳,必须自己加 Ping/Pong 逻辑。
用 SetPingHandler 和 SetPongHandler 接住心跳帧
客户端发 Ping,服务端要响应 Pong;反过来也一样。关键不是“谁发”,而是双方都得注册 handler,否则收到 Ping 会触发默认关闭行为。
-
conn.SetPingHandler:设置收到Ping时的回调,通常只做conn.SetWriteDeadline并返回nil(让库自动回Pong) -
conn.SetPongHandler:设置收到Pong时的回调,常用来刷新读写 deadline,避免被误判超时 - 两个 handler 都必须在
conn.ReadMessage循环之前注册,否则第一次Ping就断连
示例片段:
conn.SetPingHandler(func(appData string) error {
conn.SetWriteDeadline(time.Now().Add(writeWait))
return nil
})
conn.SetPongHandler(func(string) error {
conn.SetReadDeadline(time.Now().Add(pongWait))
return nil
})
定时发 Ping 不能只靠 time.Ticker
单纯起一个 goroutine 每 30 秒调一次 conn.WriteMessage(websocket.PingMessage, nil) 是危险的——如果连接已断,WriteMessage 会阻塞或 panic,且没有重试/降级机制。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
立即学习“go语言免费学习笔记(深入)”;
- 必须配合
conn.SetWriteDeadline,否则卡死 goroutine - 写失败时应主动关闭连接,而不是忽略错误或无限重试
- 推荐把
Ping放在读循环里,每次成功ReadMessage后重置 ticker,避免“空跑”发送 - 不要在
WriteMessage前加锁,gorilla/websocket.Conn写操作本身是线程安全的
pingWait、pongWait、writeWait 怎么设才合理?
这三个 timeout 不是随便填的数字,它们互相约束:
-
pongWait(读超时)应 >ping间隔,比如ping每 30s 发一次,pongWait至少设 45s,给网络抖动留余量 -
writeWait(写超时)应 ≥ping间隔,否则还没发出去就超时了;建议设为 10–20s,短于ping间隔也没问题,只要能覆盖单次写耗时 -
pingWait不是标准字段,是你的业务逻辑控制的 ticker 周期,通常设为pongWait × 0.7左右,确保在对方超时前发出去 - 所有 deadline 都要用
time.Now().Add(...)动态设置,别用固定时间点
容易忽略的一点:浏览器 WebSocket 实现对 Ping 帧不敏感,它只认 close 帧和 TCP 层保活,所以服务端必须主动发 Ping,不能依赖客户端发起。

















