Go WebSocket如何实现多客户端计时器同步
问题根因
现有实现用单个全局变量wsConn存储WebSocket连接,新客户端建立连接时会直接覆盖该变量,后续计时器触发时只会向最新连接推送消息,旧连接因收不到数据停止更新,本质是未做多连接生命周期管理、未将计时器状态与单连接逻辑解耦。
改造方案
服务端(Go)改造
核心思路是全局维护唯一计时器状态 + 线程安全的多连接池 + 全量广播机制,具体步骤如下:
- 替换单连接全局变量,定义线程安全的共享状态
废弃原有的单wsConn全局变量,用带锁的结构存储连接池和全局统一的计时器状态,避免多goroutine竞态:import ( "sync" "time" "github.com/gorilla/websocket" "github.com/gorilla/mux" ) var ( // 活跃客户端连接池,key为唯一连接ID,value为WebSocket连接实例 clientPool = make(map[string]*websocket.Conn) poolMu sync.RWMutex // 全局唯一计时器状态,所有客户端共享 globalTimer struct { mu sync.Mutex TotalSec int // 倒计时总秒数 RemainSec int // 剩余秒数 IsRunning bool // 运行状态标记 Ticker *time.Ticker // 全局唯一定时器实例 StartAt time.Time // 计时器启动时间,用于校准减少误差 } ) - 改造
/socket端点的连接处理逻辑
每个新连接建立时独立处理读逻辑,不阻塞其他连接:- 连接升级完成后,生成唯一连接ID,加锁将新连接存入连接池
- 若此时全局计时器已在运行,立即向新连接推送当前剩余秒数,让新客户端直接对齐现有计时状态,无需等待下一秒广播
- 为每个连接单独启动读协程,接收客户端传来的启动/重置倒计时请求
- 监听连接断开事件(读消息返回错误时),加锁将失效连接从连接池移除,避免向无效连接写数据引发panic
- 抽离全局唯一计时器调度逻辑,禁止每个连接单独创建Ticker
- 收到任意客户端发来的带
StartTime(初始倒计时秒数)的请求时,加锁重置全局计时器状态:更新总秒数、剩余秒数、记录启动时间,若已有Ticker在运行则先停止旧Ticker,再启动新的1秒周期Ticker - Ticker每次触发时,不依赖纯计数更新剩余时间,通过
time.Since(globalTimer.StartAt)计算已流逝时间,得出准确剩余秒数,避免Ticker累积误差 - 剩余秒数更新后,遍历整个连接池,将最新剩余时间广播给所有活跃连接;若剩余秒数归0,停止Ticker、标记运行状态为结束,广播倒计时结束消息
- 广播时如果遇到写失败的连接,直接异步从连接池移除,不要因为单个连接故障中断全量广播流程
- 收到任意客户端发来的带
核心广播逻辑参考实现:
func broadcastLatestState() { poolMu.RLock() defer poolMu.RUnlock() msg := map[string]any{ "type": "tick", "remainSec": globalTimer.RemainSec, } if globalTimer.RemainSec <= 0 { msg["type"] = "finished" } for connID, conn := range clientPool { if err := conn.WriteJSON(msg); err != nil { // 异步清理失效连接,不阻塞当前广播 go func(id string) { poolMu.Lock() defer poolMu.Unlock() delete(clientPool, id) }(connID) } } }
注意:所有对连接池、全局计时器状态的读写操作必须加对应锁,否则高并发场景下会出现数据竞态,甚至触发panic。
前端(Vue)适配
原有逻辑基本可复用,仅需补充两点:
- WebSocket连接建立后,不需要等待用户输入,直接接收服务端主动推送的初始状态(新连接接入时的对齐消息),直接渲染剩余时间即可
- 消息处理逻辑兼容两种消息类型:普通
tick消息更新剩余秒数并格式化为时分秒展示,收到finished消息时弹出归零提示即可
内容的提问来源于stack exchange,提问作者Áron Pop Adorján
相关产品推荐
相关产品推荐

