Files
Scoreboard/server/internal/realtime/client.go
T
AskaEth bc8b1ee6c0 feat: M3-M7 全模块落地 + 集成接线
- M3 认证/权限/审计/限流 + 全部 REST 接口
- M4 管理面板(MIUIx + 成绩录入/积分榜下钻/大屏控制)
- M5a 大屏页面(六页 + 明细覆盖层 + 缩放适配)
- M6 WebSocket 实时同步中心
- M7 xlsx 导出 + 赛事包 zip + 备份
- 规则包 YAML 导入导出(差异预览)
- 字体 vendor 子集化(4 套 x2 字重)
- 集成:realtime/export/rules 接入 main+httpapi;backup 子命令
- 修复:中间件包装 ResponseWriter 丢失 Hijacker,导致 WS 握手 501
2026-09-09 19:02:24 +08:00

163 lines
3.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package realtime
import (
"context"
"encoding/json"
"sync"
"sync/atomic"
"time"
"github.com/coder/websocket"
)
// client 是一条 WebSocket 连接。读写各一个 goroutine,
// 写通过带缓冲的 send 通道串行化,通道满即视为慢客户端。
type client struct {
hub *Hub
conn *websocket.Conn
remoteAddr string
send chan []byte
done chan struct{}
closeOnce sync.Once
closeCode websocket.StatusCode
closeReason string
mu sync.Mutex
greeted bool
role string
userID int64
instanceID string
ackVersion atomic.Int64 // 客户端已确认应用到的版本
lastSent atomic.Int64 // 服务端已发出的最大版本
slow atomic.Bool
}
func newClient(h *Hub, conn *websocket.Conn, remoteAddr string) *client {
return &client{
hub: h,
conn: conn,
remoteAddr: remoteAddr,
send: make(chan []byte, defaultSendBuffer),
done: make(chan struct{}),
}
}
// stop 只负责让两条 goroutine 退出;真正的关闭动作由 serve 统一执行,
// 避免在持有广播锁时做可能阻塞的关闭握手。
func (c *client) stop(code websocket.StatusCode, reason string) {
c.closeOnce.Do(func() {
c.mu.Lock()
c.closeCode = code
c.closeReason = reason
c.mu.Unlock()
close(c.done)
})
}
func (c *client) sendEvent(event string, data any) bool {
msg, err := encode(event, data)
if err != nil {
c.hub.log.Error("编码实时消息失败", "event", event, "err", err)
return false
}
return c.sendRaw(msg)
}
func (c *client) sendRaw(msg []byte) bool {
select {
case <-c.done:
return false
default:
}
select {
case c.send <- msg:
return true
default:
c.hub.markSlow(c)
return false
}
}
// readLoop 解析 C→S 消息;首条消息受 helloTimeout 限制,防止空连接占用资源。
func (c *client) readLoop(ctx context.Context) {
defer c.stop(websocket.StatusNormalClosure, "")
for {
c.mu.Lock()
greeted := c.greeted
c.mu.Unlock()
readCtx := ctx
var cancel context.CancelFunc
if !greeted {
readCtx, cancel = context.WithTimeout(ctx, c.hub.helloTimeout())
}
_, data, err := c.conn.Read(readCtx)
if cancel != nil {
cancel()
}
if err != nil {
return
}
c.handleMessage(ctx, data)
}
}
func (c *client) handleMessage(ctx context.Context, data []byte) {
var env envelope
if err := json.Unmarshal(data, &env); err != nil {
c.hub.log.Debug("忽略无法解析的实时消息", "remote", c.remoteAddr, "err", err)
return
}
switch env.Event {
case EventHello:
var p helloPayload
if err := json.Unmarshal(env.Data, &p); err != nil {
c.hub.log.Debug("hello 载荷不合法", "remote", c.remoteAddr, "err", err)
return
}
c.hub.handleHello(ctx, c, p)
case EventAck:
var p ackPayload
if err := json.Unmarshal(env.Data, &p); err != nil {
c.hub.log.Debug("ack 载荷不合法", "remote", c.remoteAddr, "err", err)
return
}
if p.Version > c.ackVersion.Load() {
c.ackVersion.Store(p.Version)
}
c.hub.touchInstance(c, time.Now())
case EventResync:
c.hub.sendInitial(ctx, c, nil)
default:
c.hub.log.Debug("忽略未知实时事件", "event", env.Event, "remote", c.remoteAddr)
}
}
// writeLoop 是唯一的写者,保证同一连接上的帧顺序。
func (c *client) writeLoop(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
case <-c.done:
return
case msg := <-c.send:
writeCtx, cancel := context.WithTimeout(ctx, c.hub.writeTimeout())
err := c.conn.Write(writeCtx, websocket.MessageText, msg)
cancel()
if err != nil {
c.stop(websocket.StatusInternalError, "写入失败")
return
}
}
}
}