Files
nl-im-service/internal/manager/client_manager.go

226 lines
5.5 KiB
Go
Raw Normal View History

2025-12-02 21:00:26 +08:00
/**
* 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 (未绑定时为空)
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
}
}
}
}