← 中文资源

简体中文版本

WebSocket Ping Pong 心跳保活

用协议层 ping 帧实现 WebSocket 心跳:读超时随 pong 响应滚动刷新,写侧按周期发 ping,断链双方都能及时感知。

阅读英文原文

示例说明

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() // 阻塞直到任一方向断开
}