1. 4.2 会话管理(Session)
4.1 把 Gate 定位为路由枢纽;本节拆开 SessionManager(业务侧 authId → 分片) 与 BaseServer(authId → IChannel) 的职责边界。类型与注释以 zhenyi/zgate/session.go 与 zhenyi-base 中 SetChannelAuth / GetChannelByAuthId 为准。
1.1. 4.2.1 会话(Session)是什么?
在 zhenyi 中,Session 不是传统意义上的"会话对象",而是一组映射关系,用来回答两个问题:
- 这个用户在哪个连接上?(authId → Channel)
- 这个用户被分配到了哪些业务 Actor?(authId → actorType → actorId)
为什么要维护这些映射?因为网关收到消息后需要路由,路由需要知道目标在哪。
1.2. 4.2.2 两层映射
zhenyi 的 Session 实际上分两层管理:
1.2.1. 第一层:authId → Channel(网络层)
由 BaseServer 维护,在连接级别:
// BaseServer 中的映射(实现为 zcoll.SyncMap,示意)
// authId → IChannel
// 绑定:登录成功后调用(Gate 侧常用 zgate.Server.SetSessionAuth 包装)
func (b *BaseServer) SetChannelAuth(channelId uint64, authId uint64) {
channel := b.GetChannel(channelId)
if channel == nil {
return
}
channel.SetAuthId(authId)
b.authChannels.Store(authId, channel)
}
// 查询:通过 authId 找连接
func (b *BaseServer) GetChannelByAuthId(authId uint64) IChannel {
ch, ok := b.authChannels.Load(authId)
if !ok {
return nil
}
channel, ok1 := ch.(IChannel)
if !ok1 {
return nil
}
return channel
}
绑定时机: 业务 Actor 处理完登录逻辑后,调用 Gate 提供的方法绑定。
解绑时机: 连接断开时自动清理。
1.2.2. 第二层:authId → Actor(业务层)
由 SessionManager 维护,在业务级别:
// SessionManager 中的映射
info map[int64]map[int32]int32 // authId → actorType → actorId
// 绑定:用户被分配到某个业务 Actor 后(info 为空时返回错误)
func (m *SessionManager) SetSessionActorId(authId int64, info map[int32]int32) error {
// info: {ActorType_IM: 2, ActorType_Match: 3}
if len(info) == 0 {
return errors.New("not actor")
}
if _, ok := m.info[authId]; !ok {
m.info[authId] = make(map[int32]int32)
}
for k, v := range info {
m.info[authId][k] = v
}
return nil
}
// 查询:知道用户在哪个 IM Actor 上(未找到返回 0)
func (m *SessionManager) GetSessionActorId(authId int64, actorType int32) int32 {
info1, ok := m.info[authId]
if !ok {
return 0
}
if actorId, ok1 := info1[actorType]; ok1 {
return actorId
}
return 0
}
// 清理:用户下线
func (m *SessionManager) DelSessionActorId(authId int64) {
delete(m.info, authId)
}
绑定时机: 由业务 Actor 决定,框架不强制。
1.2.3. 为什么分两层?
| 层 | 管理什么 | 谁维护 |
|---|---|---|
| 网络层 | authId → Channel | BaseServer(自动) |
| 业务层 | authId → ActorType → ActorId | SessionManager(业务驱动) |
网络层解决的是"消息发回给哪个连接",业务层解决的是"消息转发给哪个 Actor"。
两个问题不一样:
- 给用户发消息 → 需要知道 Channel(通过 authId 查 BaseServer)
- 查用户的业务状态 → 需要知道 ActorId(通过 authId 查 SessionManager)
1.3. 4.2.3 线程安全:不需要锁
两层映射都在 Gate Actor 的主线程中访问:
// SessionManager 的注释
// 线程安全约定:所有方法仅在 GateServer Actor 的 Run goroutine 中调用(单线程),
// 通过 Actor mailbox 消息驱动,因此无需加锁。
为什么不需要锁?因为 Gate Actor 的消息是串行处理的(第三章讲过的 Mailbox 机制),同一时间只有一个 goroutine 在读写这些 map。
但如果从 AsyncRun 的协程池中直接访问 SessionManager,就会产生 data race。所以代码注释明确说了:禁止从 AsyncRun/Worker goroutine 直接调用。
需要访问的话,应通过 safeUpdate / 向 Gate 发消息 等路径回到邮箱线程(见 3.4)。
1.4. 4.2.4 用户上线到下线的完整流程
1. 客户端连接 → OnAccept
→ channelId 分配(自动递增)
→ authId = 0(未认证)
→ 进入在线列表
2. 客户端发送登录请求 → Gate 路由到 IM Actor
→ IM Actor 验证用户身份
3. 登录成功 → 经 Gate API 绑定(示例)
→ `Server.SetSessionAuth(channelId, authId)` → 底层 `SetChannelAuth`(网络层)
→ `SetSessionActorId(authId, ...)`(业务层,`int64` / `int32` 以 `session.go` 为准)
→ 此时 authId > 0,用户已认证
4. 正常通信
→ 其他 Actor 通过 userId 找到 Channel 发消息
→ Gate 通过 userId 找到 ActorId 路由消息
5. 连接断开 → OnClose
→ authChannels.Delete(authId) (自动清理网络层映射)
→ 业务 Actor 清理 SessionManager (需要业务层自己处理)
注意第 5 步:网络层的映射是自动清理的,但业务层的映射需要业务 Actor 自己清理。这是因为框架不知道业务层什么时候该清理——用户可能断线重连,中间状态需要业务自己判断。
1.5. 4.2.5 一个用户多条连接?
当前的 SessionManager 设计是 authId → actorId(一对多:一个用户可能在多个 Actor 上),但 authId → Channel(一对一:一个 authId 只对应一个 Channel)。
这意味着:
- 同一个用户不能同时有两条连接(新的绑定会覆盖旧的)
- 但同一个用户可以同时被多个业务 Actor 服务(IM + Match + Chat)
如果业务需要支持多设备登录(手机 + PC 同时在线),需要自己在业务层扩展,比如 authId → deviceId → Channel。
1.6. 4.2.6 本节要点
- SessionManager:
authId(int64) → actorType(int32) → actorId(int32),供本地/远程路由查分片。 - BaseServer:
authId → IChannel,供下行GetChannelByAuthId等路径。 - 线程安全:仅假设在 Gate Actor 邮箱线程调用;禁止 worker 直连。
- 生命周期:断线清 Channel 侧绑定;
SessionManager条目须业务在合适时机DelSessionActorId。 - 多连接:当前 authId ↔ Channel 为常用的一对一模型;多设备需业务扩展映射。
4.3 节:本地表、远程候选与 PickOne 策略。