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
This commit is contained in:
1 parent
d0e3b93e76
commit
bc8b1ee6c0
145 files changed
+25159
-321
No files matched your search
@@ -0,0 +1,162 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user