package realtime import ( "crypto/rand" "encoding/hex" "sort" "strconv" "time" ) // Instance 是大屏实例的对外快照,供管理端查询「谁在线、在哪一页」。 type Instance struct { ID string `json:"id"` Online bool `json:"online"` Page string `json:"page,omitempty"` LastHeartbeat time.Time `json:"lastHeartbeat"` ConnectedAt time.Time `json:"connectedAt"` RemoteAddr string `json:"remoteAddr,omitempty"` AckVersion int64 `json:"ackVersion"` Slow bool `json:"slow"` } // instanceState 是注册表的内部记录;同一 instanceId 的多条连接合并成一个实例。 type instanceState struct { id string page string connectedAt time.Time lastHeartbeat time.Time remoteAddr string ackVersion int64 slow bool conns int } // registerDisplay 把连接挂到大屏实例上;未带 instanceId 时由服务端生成, // 保证「刷新页面 = 同一实例」之外的连接也能被登记和查询。 func (h *Hub) registerDisplay(c *client, id, page string) string { if id == "" { id = "display-" + randomID() } now := time.Now() c.mu.Lock() prev := c.instanceID c.instanceID = id c.mu.Unlock() // 同一连接重复 hello 或换了 instanceId 时,先归还旧实例的引用,避免计数虚高。 if prev != "" && prev != id { h.releaseDisplayID(prev, c) } h.mu.Lock() defer h.mu.Unlock() st := h.instances[id] if st == nil { st = &instanceState{id: id, connectedAt: now} h.instances[id] = st } if prev != id { st.conns++ } if page != "" { st.page = page } st.lastHeartbeat = now st.remoteAddr = c.remoteAddr return id } // releaseDisplay 断开时只减少引用计数:实例保留在注册表里并标记离线,方便事后排查。 func (h *Hub) releaseDisplay(c *client) { c.mu.Lock() id := c.instanceID c.mu.Unlock() if id == "" { return } h.releaseDisplayID(id, c) } func (h *Hub) releaseDisplayID(id string, c *client) { h.mu.Lock() defer h.mu.Unlock() st := h.instances[id] if st == nil { return } if st.conns > 0 { st.conns-- } st.lastHeartbeat = time.Now() st.ackVersion = c.ackVersion.Load() st.slow = c.slow.Load() } // touchInstance 在收到客户端消息或下发心跳时刷新「最后心跳」。 func (h *Hub) touchInstance(c *client, now time.Time) { c.mu.Lock() id := c.instanceID c.mu.Unlock() if id == "" { return } h.mu.Lock() defer h.mu.Unlock() if st := h.instances[id]; st != nil { st.lastHeartbeat = now st.ackVersion = c.ackVersion.Load() st.slow = c.slow.Load() } } // setInstancePage 记录服务端刚下发的目标页,让注册表里的「当前页」跟得上 goto 指令。 func (h *Hub) setInstancePage(page string) { if page == "" { return } h.mu.Lock() defer h.mu.Unlock() for _, st := range h.instances { if st.conns > 0 { st.page = page } } } // Instances 返回注册表快照,按 instanceId 排序,便于管理端稳定展示。 func (h *Hub) Instances() []Instance { h.mu.RLock() defer h.mu.RUnlock() out := make([]Instance, 0, len(h.instances)) for _, st := range h.instances { out = append(out, Instance{ ID: st.id, Online: st.conns > 0, Page: st.page, LastHeartbeat: st.lastHeartbeat, ConnectedAt: st.connectedAt, RemoteAddr: st.remoteAddr, AckVersion: st.ackVersion, Slow: st.slow, }) } sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID }) return out } func randomID() string { var b [8]byte if _, err := rand.Read(b[:]); err != nil { // 随机源不可用时退回时间戳,实例 id 只需唯一,不承担安全职责。 return strconv.FormatInt(time.Now().UnixNano(), 16) } return hex.EncodeToString(b[:]) }