实现游戏大厅与房间模块:玩法插件化、匹配与房间生命周期管理
This commit is contained in:
@@ -13,18 +13,27 @@ import (
|
||||
const (
|
||||
TypePing = "ping" // 应用层心跳:客户端周期性发送,服务端原样回 pong
|
||||
|
||||
// TODO(第二步 大厅):在此补充 TypeHallMatch = "hall.match"、TypeHallCancel = "hall.cancel"
|
||||
// TODO(第二步 房间):在此补充 TypeRoomJoin = "room.join"、TypeRoomReady = "room.ready"、
|
||||
// TypeRoomAction = "room.action"、TypeRoomLeave = "room.leave"、
|
||||
// TypeRoomReconnect = "room.reconnect"
|
||||
TypeHallMatch = "hall.match"
|
||||
TypeHallCancel = "hall.cancel"
|
||||
TypeRoomJoin = "room.join"
|
||||
TypeRoomReady = "room.ready"
|
||||
TypeRoomAction = "room.action"
|
||||
TypeRoomLeave = "room.leave"
|
||||
TypeRoomReconnect = "room.reconnect"
|
||||
)
|
||||
|
||||
// ===== 下行消息类型:服务端 → 客户端 =====
|
||||
const (
|
||||
TypeHello = "hello" // 建连成功后下发的首条消息,携带 userID 与心跳建议间隔
|
||||
TypePong = "pong" // 对上行 ping 的应答
|
||||
TypeEvent = "event" // 业务事件统一信封:房间状态、对局事件等都走这个类型,payload 由各模块自定义
|
||||
TypeError = "error" // 错误下行:参数错误、未支持类型、业务错误统一走这里
|
||||
TypeHello = "hello" // 建连成功后下发的首条消息,携带 userID 与心跳建议间隔
|
||||
TypePong = "pong" // 对上行 ping 的应答
|
||||
TypeEvent = "event" // 业务事件统一信封:房间状态、对局事件等都走这个类型,payload 由各模块自定义
|
||||
TypeError = "error" // 错误下行:参数错误、未支持类型、业务错误统一走这里
|
||||
TypeHallMatched = "hall.matched" // 匹配成功,携带房间 ID
|
||||
TypeHallCancelled = "hall.cancelled" // 匹配已取消
|
||||
TypeRoomState = "room.state" // 房间全量快照
|
||||
TypeRoomEvent = "room.event" // 玩法增量事件
|
||||
TypeRoomFinished = "room.finished" // 对局结束与结算
|
||||
TypeRoomClosed = "room.closed" // 房间已关闭
|
||||
)
|
||||
|
||||
// Envelope 是上下行消息的统一信封。
|
||||
|
||||
+23
-10
@@ -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)) // 预分配切片,容量等于目标数
|
||||
|
||||
Reference in New Issue
Block a user