/** * package manager * 作用:管理全量的 WebSocket 连接,提供线程安全的注册、注销、绑定和消息发送功能。 * 核心:使用 sync.Map 替代传统 map + RWMutex 以提高高并发下的读写性能。 */ package manager import ( "log" "sync" "time" "github.com/gorilla/websocket" ) /** * Client * 结构体:代表一个 WebSocket 客户端连接会话。 */ type Client struct { ID string // 客户端唯一标识 (格式: NodeID-Timestamp) UserID string // 绑定的用户ID (未绑定时为空) RemoteIP string // WebSocket 连接来源 IP Conn *websocket.Conn // 底层 WebSocket 连接对象 SendQueue chan []byte // 发送缓冲队列 (防止网络阻塞导致写协程卡死) } /** * ClientManager * 结构体:连接管理器,维护 ClientID 映射和 UserID 反向索引。 */ type ClientManager struct { // 客户端列表: map[string]*Client (Key: ClientID) clients sync.Map // 用户反向索引: map[string][]string (Key: UserID, Value: [ClientID1, ClientID2]) userClients sync.Map } // Manager 全局单例 var Manager = &ClientManager{} /** * Register * 功能:注册新建立的 WebSocket 连接。 * @param c *Client 客户端对象 */ func (m *ClientManager) Register(c *Client) { m.clients.Store(c.ID, c) log.Printf("✅ [ClientManager] 客户端注册: %s", c.ID) // 启动该客户端独享的写协程 go c.writePump() } /** * Unregister * 功能:注销连接,清理资源和索引。 * @param c *Client 客户端对象 */ func (m *ClientManager) Unregister(c *Client) { if _, ok := m.clients.Load(c.ID); ok { m.clients.Delete(c.ID) close(c.SendQueue) // 关闭通道,退出 writePump c.Conn.Close() // 如果已绑定用户,清理反向索引 if c.UserID != "" { m.removeUserClient(c.UserID, c.ID) } log.Printf("🔌 [ClientManager] 客户端注销: %s", c.ID) } } /** * BindUser * 功能:将 ClientID 与 UserID 进行绑定,支持多端登录。 * @param clientID 客户端ID * @param userID 用户ID */ func (m *ClientManager) BindUser(clientID, userID string) { clientInterface, ok := m.clients.Load(clientID) if !ok { return } client := clientInterface.(*Client) client.UserID = userID // 更新反向索引 (UserID -> []ClientID) // 使用 LoadOrStore 初始化切片 actual, _ := m.userClients.LoadOrStore(userID, make([]string, 0)) ids := actual.([]string) // 去重逻辑:防止重复添加同一个 ClientID for _, id := range ids { if id == clientID { return } } ids = append(ids, clientID) m.userClients.Store(userID, ids) log.Printf("🔗 [ClientManager] 绑定成功: %s -> %s", clientID, userID) } /** * removeUserClient * 功能:从用户的客户端列表中移除指定的 ClientID。 */ func (m *ClientManager) removeUserClient(userID, clientID string) { val, ok := m.userClients.Load(userID) if !ok { return } ids := val.([]string) newIds := make([]string, 0) for _, id := range ids { if id != clientID { newIds = append(newIds, id) } } if len(newIds) == 0 { m.userClients.Delete(userID) // 如果没有设备了,删除 Key } else { m.userClients.Store(userID, newIds) // 更新列表 } } /** * SendToClient * 功能:向指定客户端发送消息 (非阻塞模式)。 * @param clientID 目标客户端ID * @param message 消息字节数据 */ func (m *ClientManager) SendToClient(clientID string, message []byte) { if val, ok := m.clients.Load(clientID); ok { client := val.(*Client) select { case client.SendQueue <- message: // 成功入队 default: log.Printf("⚠️ [ClientManager] 发送队列已满,丢弃消息: %s", clientID) } } } /** * SendToUser * 功能:向指定用户的所有在线设备广播消息。 * @param userID 目标用户ID * @param message 消息字节数据 */ func (m *ClientManager) SendToUser(userID string, message []byte) { if val, ok := m.userClients.Load(userID); ok { ids := val.([]string) for _, clientID := range ids { m.SendToClient(clientID, message) } } } /** * GetOtherClients * 功能:获取该用户除 excludeClientID 以外的其他所有在线客户端ID。 * 场景:用于多端同步(如一端接听,通知其他端关闭振铃)。 */ func (m *ClientManager) GetOtherClients(userID, excludeClientID string) []string { var results []string if val, ok := m.userClients.Load(userID); ok { ids := val.([]string) for _, id := range ids { if id != excludeClientID { results = append(results, id) } } } return results } /** * writePump * 功能:每个客户端独享的写协程,确保 WebSocket 并发写安全,并处理心跳 Ping。 */ func (c *Client) writePump() { ticker := time.NewTicker(50 * time.Second) // 心跳间隔 defer func() { ticker.Stop() c.Conn.Close() }() for { select { case message, ok := <-c.SendQueue: c.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) if !ok { // 队列关闭,发送 Close 帧 c.Conn.WriteMessage(websocket.CloseMessage, []byte{}) return } w, err := c.Conn.NextWriter(websocket.TextMessage) if err != nil { return } w.Write(message) // 优化:如果队列里堆积了多条消息,一次性写完,减少系统调用次数 n := len(c.SendQueue) for i := 0; i < n; i++ { w.Write([]byte{'\n'}) // JSON消息分隔符 w.Write(<-c.SendQueue) } if err := w.Close(); err != nil { return } case <-ticker.C: // 发送心跳 Ping c.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) if err := c.Conn.WriteMessage(websocket.PingMessage, nil); err != nil { return } } } }