实现游戏大厅与房间模块:玩法插件化、匹配与房间生命周期管理

This commit is contained in:
2026-09-28 00:10:11 +08:00
parent 99d1827262
commit fb5a730ef3
19 changed files with 2492 additions and 18 deletions
+23 -10
View File
@@ -3,7 +3,7 @@ package wsgateway // 包名声明:与 envelope.go、connection.go、server.go
import (
"context" // 标准库:handler 的入参上下文,连接关闭即取消
"log/slog" // 标准库:结构化日志,未注册消息类型时打 debug 日志
"sync" // 标准库:RWMutex,保护 Hub 内部的两张表
"sync" // 标准库:RWMutex,保护 Hub 内部的表与回调列表
"git.lzcwy.cn/zhengchiliu/zt-online-game-backend/global" // 本项目:读取 global.WSSetting 判断是否允许多设备
)
@@ -18,9 +18,10 @@ var DefaultHub = NewHub() // 包初始化时即创建,server.go 与路由共
// Hub 是连接注册表与消息路由器,本身不理解任何玩法。
type Hub struct {
mu sync.RWMutex // 读写锁:保护下面两个 map,读多写少所以用 RWMutex
conns map[int64]*Connection // userID -> 连接:一个用户当前对应的那条连接
handlers map[string]Handler // 消息类型 -> 处理器:决定上行消息由谁处理
mu sync.RWMutex // 读写锁:保护下面三张表/列表
conns map[int64]*Connection // userID -> 连接:一个用户当前对应的那条连接
handlers map[string]Handler // 消息类型 -> 处理器:决定上行消息由谁处理
disconnectHooks []func(userID int64) // 断连回调:连接移除后通知业务层(房间据此进入重连宽限)
}
// NewHub 构造一个空的 Hub。
@@ -32,13 +33,20 @@ func NewHub() *Hub {
}
// RegisterHandler 注册某类消息的处理函数。
// TODO(第二步):hall 注册 hall.match / hall.cancel;房间注册 room.join / room.action / room.leave 等。
// 已由 hall 注册 hall.match / hall.cancel;由 room 注册 room.join / room.action / room.leave 等。
func (h *Hub) RegisterHandler(msgType string, fn Handler) {
h.mu.Lock() // 加写锁:修改 handlers 表
defer h.mu.Unlock() // 函数返回时解锁
h.handlers[msgType] = fn // 覆盖式注册:同名类型以最后注册者为准
}
// OnDisconnect 注册断连回调:连接从表中移除后触发(业务层据此处理掉线)。
func (h *Hub) OnDisconnect(fn func(userID int64)) {
h.mu.Lock() // 加写锁:修改回调列表
defer h.mu.Unlock() // 返回时解锁
h.disconnectHooks = append(h.disconnectHooks, fn) // 追加回调
}
// Register 把新建连接登记到 Hub;MultiDevice=false 时先踢掉同账号的旧连接。
func (h *Hub) Register(c *Connection) {
h.mu.Lock() // 加写锁:可能修改 conns 表
@@ -46,7 +54,7 @@ func (h *Hub) Register(c *Connection) {
if global.WSSetting == nil || !global.WSSetting.MultiDevice { // 未配置或明确禁止多设备:执行踢线
if old, ok := h.conns[c.UserID()]; ok && old != c { // 该账号已在线且不是同一条连接
slog.InfoContext(c.Context(), "ws kick old connection", "user_id", c.UserID()) // 记录踢线日志,便于排查重复登录
slog.InfoContext(c.Context(), "ws kick old connection", "user_id", c.UserID()) // 记录踢线日志
old.Close() // 关闭旧连接:旧连接的读写泵会随之退出
delete(h.conns, c.UserID()) // 从表中移除旧连接,给新连接腾位置
}
@@ -55,11 +63,17 @@ func (h *Hub) Register(c *Connection) {
}
// Unregister 从表中注销连接;只有表中仍是自己时才删除,避免误删踢线后新建的连接。
// 回调在解锁之后执行:避免业务回调内部再次操作 Hub 造成死锁。
func (h *Hub) Unregister(c *Connection) {
h.mu.Lock() // 加写锁:修改 conns 表
defer h.mu.Unlock() // 返回时解锁
var hooks []func(int64) // 待触发的回调
if cur, ok := h.conns[c.UserID()]; ok && cur == c { // 表中存在且指向同一条连接
delete(h.conns, c.UserID()) // 安全删除
delete(h.conns, c.UserID()) // 安全删除
hooks = append(hooks, h.disconnectHooks...) // 拷贝回调列表
}
h.mu.Unlock() // 解锁后再执行回调
for _, fn := range hooks { // 逐个触发断连回调
fn(c.UserID()) // 通知业务层(如房间标记掉线)
}
}
@@ -103,8 +117,7 @@ func (h *Hub) SendTo(userID int64, data []byte) bool {
return c.Send(data) // 入队发送
}
// Broadcast 向指定用户集合下发同一条消息。
// TODO(第二步):房间内广播就走这里——玩法产出的 Event 由房间翻译成信封后调用本方法。
// Broadcast 向指定用户集合下发同一条消息(房间内广播与私有事件都走这里)。
func (h *Hub) Broadcast(userIDs []int64, data []byte) {
h.mu.RLock() // 加读锁
targets := make([]*Connection, 0, len(userIDs)) // 预分配切片,容量等于目标数