200 lines
6.1 KiB
Go
200 lines
6.1 KiB
Go
// 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
|
||
}
|