server: Go 服务器(REST+WSS 网关+JWT+Argon2+配对+素材直链+任务状态机);修复并发下线 send-on-closed-channel、下发查询 SQL 优先级、配对码原子占用、上传体积上限、JWT 默认密钥告警
This commit is contained in:
@@ -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")
|
||||
Reference in New Issue
Block a user