server: Go 服务器(REST+WSS 网关+JWT+Argon2+配对+素材直链+任务状态机);修复并发下线 send-on-closed-channel、下发查询 SQL 优先级、配对码原子占用、上传体积上限、JWT 默认密钥告警

This commit is contained in:
Qiufeng
2026-08-20 20:38:21 +08:00
parent bf25d8beac
commit cc8845dcec
44 changed files with 4995 additions and 0 deletions
+229
View File
@@ -0,0 +1,229 @@
package ws
import (
"encoding/json"
"errors"
"strconv"
"sync"
"time"
"github.com/coder/websocket"
"github.com/gin-gonic/gin"
"everypublish/shared/proto"
)
// parseID 解析设备ID字符串(失败返回 0)
func parseID(s string) uint64 {
id, _ := strconv.ParseUint(s, 10, 64)
return id
}
// Conn 一条已认证连接(Agent 或浏览器)
type Conn struct {
ID string
Kind string
WSID uint64
ws *websocket.Conn
send chan []byte
closeOnce sync.Once
mu sync.Mutex
closed bool
}
// close 安全关闭发送通道(仅一次)。
// 关闭在 mu 保护下进行;所有发送侧经 sendRaw 先检查 closed,
// 从根本上避免「send on closed channel」panic(关闭与发送的竞态)。
func (c *Conn) close() {
c.closeOnce.Do(func() {
c.mu.Lock()
c.closed = true
close(c.send)
c.mu.Unlock()
})
}
// sendRaw 向发送通道投递:closed 直接返回 false 不恐慌;
// block=true 时队列满将阻塞最多 1s(可靠消息用),否则满即丢弃(广播用)。
func (c *Conn) sendRaw(raw []byte, block bool) bool {
c.mu.Lock()
defer c.mu.Unlock()
if c.closed {
return false
}
if block {
select {
case c.send <- raw:
return true
case <-time.After(time.Second):
return false
}
}
select {
case c.send <- raw:
return true
default:
return false
}
}
// sendEnvelope 统一写出口:所有消息经 send 通道由 writeLoop 单协程写入(避免并发写)
func (c *Conn) sendEnvelope(msg *proto.Envelope) {
raw, err := json.Marshal(msg)
if err != nil {
return
}
c.sendRaw(raw, true)
}
// Hub 连接中枢(goroutine-safe)
type Hub struct {
mu sync.RWMutex
agents map[uint64]*Conn
browser map[string]*Conn
}
// NewHub 创建中枢
func NewHub() *Hub {
return &Hub{
agents: make(map[uint64]*Conn),
browser: make(map[string]*Conn),
}
}
// RegisterAgent 上线登记(同设备重连替换旧连接)
// 网络关闭放到锁外执行,避免阻塞中枢其它操作。
func (h *Hub) RegisterAgent(c *Conn) {
var old *Conn
h.mu.Lock()
if o, ok := h.agents[parseID(c.ID)]; ok {
old = o
}
h.agents[parseID(c.ID)] = c
h.mu.Unlock()
if old != nil {
old.close()
if old.ws != nil {
_ = old.ws.Close(websocket.StatusGoingAway, "replaced by new connection")
}
}
}
// UnregisterAgent 下线(仅当仍是当前连接时才移除,避免旧连接误删新连接)
func (h *Hub) UnregisterAgent(c *Conn) {
var toClose *Conn
h.mu.Lock()
if cur, ok := h.agents[parseID(c.ID)]; ok && cur == c {
delete(h.agents, parseID(c.ID))
toClose = cur
}
h.mu.Unlock()
if toClose != nil {
toClose.close()
}
}
// RegisterBrowser 浏览器连接登记
func (h *Hub) RegisterBrowser(c *Conn) {
h.mu.Lock()
h.browser[c.ID] = c
h.mu.Unlock()
}
// UnregisterBrowser 浏览器断开(指针比较,防串号)
func (h *Hub) UnregisterBrowser(c *Conn) {
var toClose *Conn
h.mu.Lock()
if cur, ok := h.browser[c.ID]; ok && cur == c {
delete(h.browser, c.ID)
toClose = cur
}
h.mu.Unlock()
if toClose != nil {
toClose.close()
}
}
// Clear 清空所有连接(测试/停机用)
func (h *Hub) Clear() {
h.mu.Lock()
for id, c := range h.agents {
c.close()
_ = c.ws.Close(websocket.StatusGoingAway, "hub clear")
delete(h.agents, id)
}
for id, c := range h.browser {
c.close()
_ = c.ws.Close(websocket.StatusGoingAway, "hub clear")
delete(h.browser, id)
}
h.mu.Unlock()
}
// SendToAgent 给设备发消息(1s 超时;队列满丢弃)
func (h *Hub) SendToAgent(deviceID uint64, msg *proto.Envelope) error {
h.mu.RLock()
c, ok := h.agents[deviceID]
h.mu.RUnlock()
if !ok {
return errOffline
}
raw, err := json.Marshal(msg)
if err != nil {
return err
}
if !c.sendRaw(raw, true) {
return errBusy
}
return nil
}
// KickAgent 强制断开设备连接(吊销/远程指令用)
func (h *Hub) KickAgent(deviceID uint64) {
h.mu.RLock()
c, ok := h.agents[deviceID]
h.mu.RUnlock()
if ok {
_ = c.ws.Close(websocket.StatusPolicyViolation, "device revoked")
}
}
// AgentOnline 设备是否在线
func (h *Hub) AgentOnline(deviceID uint64) bool {
h.mu.RLock()
defer h.mu.RUnlock()
_, ok := h.agents[deviceID]
return ok
}
// OnlineDevices 工作空间在线设备列表
func (h *Hub) OnlineDevices(wsid uint64) []uint64 {
h.mu.RLock()
defer h.mu.RUnlock()
ids := make([]uint64, 0)
for id, c := range h.agents {
if c.WSID == wsid {
ids = append(ids, id)
}
}
return ids
}
// BroadcastWS 向工作空间所有浏览器连接广播事件
func (h *Hub) BroadcastWS(wsid uint64, typ string, payload interface{}) {
raw, err := json.Marshal(gin.H{"type": typ, "payload": payload})
if err != nil {
return
}
h.mu.RLock()
defer h.mu.RUnlock()
for _, c := range h.browser {
if c.WSID != wsid {
continue
}
c.sendRaw(raw, false)
}
}
var errOffline = errors.New("device offline")
var errBusy = errors.New("device send queue full")