示例说明
TCP 半开连接是最隐蔽的故障:一端已死另一端毫无察觉,消息发进黑洞。WebSocket 协议内置 ping/pong 控制帧——写侧周期发 ping,读侧收到 pong 后刷新读截止时间;两个等待时间形成闭环,任何一方断链,读超时或写失败都会在数十秒内暴露,连接被双向清理。
技术要点
- ping 周期必须小于 pong 等待上限
- pong 处理器滚动刷新读截止
- WriteControl 带独立写超时
代码解读
readPump 设定五百字节读上限与初始六十秒读截止;pong 处理器把截止时间再推一个周期——只要对端活着就永不超时。writePump 的 ticker 每五十秒发一个 ping 控制帧,写超时十秒防止写阻塞。两泵任一退出都会关闭连接触发对方退出。
使用建议
代理与负载均衡的空闲超时要小于 ping 周期设定。控制帧不占应用消息序列,开销极小。重连逻辑叠加在两泵退出之后。
代码示例
package main
import (
"log"
"time"
"github.com/gorilla/websocket"
)
const (
pongWait = 60 * time.Second
pingPeriod = 50 * time.Second // 必须小于 pongWait
)
type Peer struct {
conn *websocket.Conn
}
func (p *Peer) readPump() {
defer p.conn.Close()
p.conn.SetReadLimit(512)
p.conn.SetReadDeadline(time.Now().Add(pongWait))
p.conn.SetPongHandler(func(string) error {
return p.conn.SetReadDeadline(time.Now().Add(pongWait))
})
for {
if _, _, err := p.conn.ReadMessage(); err != nil {
log.Println("read closed:", err)
return
}
}
}
func (p *Peer) writePump() {
ticker := time.NewTicker(pingPeriod)
defer func() {
ticker.Stop()
p.conn.Close()
}()
for range ticker.C {
if err := p.conn.WriteControl(
websocket.PingMessage, nil, time.Now().Add(10*time.Second),
); err != nil {
log.Println("ping failed:", err)
return
}
log.Println("heartbeat sent")
}
}
func main() {
// 客户端侧同样适用:拨号成功后启动两个泵
// conn, _, err := websocket.DefaultDialer.Dial("wss://example.com/ws", nil)
conn, _, _ := websocket.DefaultDialer.Dial("ws://localhost:8080/ws", nil)
peer := &Peer{conn: conn}
go peer.writePump()
peer.readPump() // 阻塞直到任一方向断开
}