// 扫描状态实时推送:用 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:建立专线后,不停把扫描状态推给前端。 // // 连接一建立,先立刻把当前状态推一份(快照),之后一有更新就推。 // 前端页面刷新、网络断了之后会自己重新连上来,这里只管服务端这一端。 func (c *ScanController) Ws(ctx *gin.Context) { // 升级成 WebSocket 专线 conn, err := wsUpgrader.Upgrade(ctx.Writer, ctx.Request, nil) if err != nil { return } // 订阅进度更新:之后有状态就送到 ch 这个管道里 ch, cancel := c.service.Subscribe() defer func() { cancel() conn.Close() }() // 读消息的协程只是用来"感知"客户端还在不在(配合心跳应答),不处理实际内容 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 } } } }() // 主线程一直等着读:客户端断开或失活时 ReadMessage 会出错,这里就收工 for { if _, _, err := conn.ReadMessage(); err != nil { break } } // 先关掉订阅,让写协程退出,避免白等 cancel() <-writeDone }