// Package session 维护 im-service 的真实在线连接、用户连接索引和房间订阅索引。 package session import "sync" // Session 表达 im-service 内一条真实长连接对应的在线会话。 type Session struct { // ConnID 是单条 WebSocket 连接的内部标识,所有下行通道都以它为 key。 ConnID string // UserID 是消息投递和 ACK 归属的用户身份。 UserID int64 // DeviceID 标识客户端设备,当前只记录不参与多端同步决策。 DeviceID string // NodeID 标识连接所在 im-service 节点,用于未来多节点在线路由。 NodeID string // ConnectedAtMS 是连接建立时间,使用毫秒便于直接序列化给运维和排障工具。 ConnectedAtMS int64 // LastSeenAtMS 是最后活跃时间,当前由 Touch 更新,后续可用于心跳清理。 LastSeenAtMS int64 // SubscribedRooms 是当前连接已加入的房间频道集合,不等同于 room-service presence。 SubscribedRooms map[string]bool } // Manager 负责 user/session/room 三种索引的统一维护。 type Manager struct { // mu 保护下面三个索引的一致性;增删会话和订阅必须在同一临界区内完成。 mu sync.Mutex // sessions 提供 conn_id -> session 查询,用于 WS 帧进入后的身份定位。 sessions map[string]*Session // userSessions 提供 user_id -> conn_id 集合,用于单聊和系统通知多端推送。 userSessions map[int64]map[string]bool // roomSessions 提供 room_id -> conn_id 集合,用于房间公屏和系统事件 fanout。 roomSessions map[string]map[string]bool } // NewManager 初始化空的在线会话管理器。 func NewManager() *Manager { return &Manager{ sessions: make(map[string]*Session), userSessions: make(map[int64]map[string]bool), roomSessions: make(map[string]map[string]bool), } } // AddSession 注册一条在线会话。 func (m *Manager) AddSession(session *Session) { m.mu.Lock() defer m.mu.Unlock() // 会话主索引先写入,后续用户索引和房间索引都只保存 conn_id。 m.sessions[session.ConnID] = session if m.userSessions[session.UserID] == nil { // 同一用户允许多条连接,索引用集合承载多端和重连窗口。 m.userSessions[session.UserID] = make(map[string]bool) } m.userSessions[session.UserID][session.ConnID] = true } // RemoveSession 移除一条在线会话及其房间订阅索引。 func (m *Manager) RemoveSession(connID string) { m.mu.Lock() defer m.mu.Unlock() session, exists := m.sessions[connID] if !exists { // 断线清理可能被重复调用,缺失连接按幂等成功处理。 return } for roomID := range session.SubscribedRooms { // 删除房间反向索引,避免断开的连接继续收到房间广播。 delete(m.roomSessions[roomID], connID) if len(m.roomSessions[roomID]) == 0 { delete(m.roomSessions, roomID) } } // 删除用户反向索引,避免单聊或系统通知推到已关闭 sink。 delete(m.userSessions[session.UserID], connID) if len(m.userSessions[session.UserID]) == 0 { delete(m.userSessions, session.UserID) } delete(m.sessions, connID) } // GetSession 返回指定连接的当前会话。 func (m *Manager) GetSession(connID string) (*Session, bool) { m.mu.Lock() defer m.mu.Unlock() session, exists := m.sessions[connID] if !exists { return nil, false } // 返回拷贝,避免调用方绕过 Manager 锁直接改内部索引状态。 copied := *session copied.SubscribedRooms = cloneRooms(session.SubscribedRooms) return &copied, true } // Touch 更新会话活跃时间。 func (m *Manager) Touch(connID string, lastSeenAtMS int64) { m.mu.Lock() defer m.mu.Unlock() if session, exists := m.sessions[connID]; exists { // Touch 只更新活跃时间,不创建会话,避免伪造心跳污染在线索引。 session.LastSeenAtMS = lastSeenAtMS } } // SubscribeRoom 把指定连接加入房间频道索引。 func (m *Manager) SubscribeRoom(connID string, roomID string) { m.mu.Lock() defer m.mu.Unlock() session, exists := m.sessions[connID] if !exists { // 订阅必须基于已鉴权连接;连接不存在时保持幂等无副作用。 return } if session.SubscribedRooms == nil { // 防御外部构造 Session 时漏初始化订阅集合。 session.SubscribedRooms = make(map[string]bool) } session.SubscribedRooms[roomID] = true if m.roomSessions[roomID] == nil { // 首条订阅创建房间 fanout 索引;房间是否存在由上层 room-service guard 保证。 m.roomSessions[roomID] = make(map[string]bool) } m.roomSessions[roomID][connID] = true } // UnsubscribeRoom 把指定连接从房间频道索引移除。 func (m *Manager) UnsubscribeRoom(connID string, roomID string) { m.mu.Lock() defer m.mu.Unlock() session, exists := m.sessions[connID] if !exists { // 取消订阅在断线和主动退出之间可能重复发生,缺失连接按幂等处理。 return } // 同时删除连接内订阅集合和房间反向索引,保证广播视图一致。 delete(session.SubscribedRooms, roomID) delete(m.roomSessions[roomID], connID) if len(m.roomSessions[roomID]) == 0 { delete(m.roomSessions, roomID) } } // ListByRoom 返回订阅指定房间的全部连接 ID。 func (m *Manager) ListByRoom(roomID string) []string { m.mu.Lock() defer m.mu.Unlock() // 返回连接 ID 切片快照,调用方发送消息时不持有 Manager 锁。 result := make([]string, 0, len(m.roomSessions[roomID])) for connID := range m.roomSessions[roomID] { result = append(result, connID) } return result } // ListByUser 返回属于指定用户的全部连接 ID。 func (m *Manager) ListByUser(userID int64) []string { m.mu.Lock() defer m.mu.Unlock() // 多端在线时返回全部连接,投递层负责逐连接写入对应 sink。 result := make([]string, 0, len(m.userSessions[userID])) for connID := range m.userSessions[userID] { result = append(result, connID) } return result } func cloneRooms(values map[string]bool) map[string]bool { // nil map 会得到空 map,调用方可以安全读写返回值而不影响 Manager。 cloned := make(map[string]bool, len(values)) for roomID, subscribed := range values { cloned[roomID] = subscribed } return cloned }