// 扫描状态 WebSocket 实时推送端点。 package api import ( "encoding/json" "net/http" "time" "github.com/gin-gonic/gin" "github.com/gorilla/websocket" ) // wsUpgrader WebSocket 升级器 var wsUpgrader = websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 4096, // 同源管理页,允许任意来源,便于局域网访问 CheckOrigin: func(r *http.Request) bool { return true }, } const ( wsWriteWait = 10 * time.Second wsPongWait = 60 * time.Second wsPingPeriod = (wsPongWait * 9) / 10 ) // Ws GET /api/scan/ws 建立 WebSocket 连接,持续推送扫描状态。 // 连接建立后立即推送当前状态快照;断开后前端可自动重连(服务端不做重连保证)。 func (c *ScanController) Ws(ctx *gin.Context) { conn, err := wsUpgrader.Upgrade(ctx.Writer, ctx.Request, nil) if err != nil { return } ch, cancel := c.service.Subscribe() defer func() { cancel() conn.Close() }() // 读协程:仅用于感知客户端断开/失活(配合 pong 心跳),不处理业务消息 conn.SetReadLimit(1024) _ = conn.SetReadDeadline(time.Now().Add(wsPongWait)) conn.SetPongHandler(func(string) error { return conn.SetReadDeadline(time.Now().Add(wsPongWait)) }) // 写协程:先推送快照,再转发订阅更新,并周期性 ping 保活 writeDone := make(chan struct{}) go func() { defer close(writeDone) write := func(msgType int, data []byte) bool { if err := conn.WriteMessage(msgType, data); err != nil { conn.Close() // 写失败即断开,让读协程退出 return false } return true } if b, err := json.Marshal(c.service.Status()); err == nil { if !write(websocket.TextMessage, b) { return } } ticker := time.NewTicker(wsPingPeriod) defer ticker.Stop() for { select { case st, ok := <-ch: if !ok { return } b, err := json.Marshal(st) if err != nil { continue } if !write(websocket.TextMessage, b) { return } case <-ticker.C: if !write(websocket.PingMessage, nil) { return } } } }() for { if _, _, err := conn.ReadMessage(); err != nil { break } } cancel() // 先关闭订阅频道,让写协程退出,避免 <-writeDone 永久阻塞 <-writeDone }