2026-04-25 01:18:30 +08:00

200 lines
6.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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
}