Files
zt-online-game-backend/internal/wsgateway/hub.go
T

134 lines
6.6 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 wsgateway // 包名声明:与 envelope.go、connection.go、server.go 同包
import (
"context" // 标准库:handler 的入参上下文,连接关闭即取消
"log/slog" // 标准库:结构化日志,未注册消息类型时打 debug 日志
"sync" // 标准库:RWMutex,保护 Hub 内部的两张表
"git.lzcwy.cn/zhengchiliu/zt-online-game-backend/global" // 本项目:读取 global.WSSetting 判断是否允许多设备
)
// Handler 是业务消息处理器签名,由 hall / room 模块实现并注册到 Hub。
// ctx 随连接关闭而取消:handler 内部若起 goroutine,必须监听它;绝不要把 *gin.Context 带进来。
type Handler func(ctx context.Context, c *Connection, env *Envelope)
// DefaultHub 是进程级默认 Hub 单例。
// 若将来需要注入(方便单测),可改为在 main 里 NewHub() 并传给 routers.NewRouter。
var DefaultHub = NewHub() // 包初始化时即创建,server.go 与路由共用这一个实例
// Hub 是连接注册表与消息路由器,本身不理解任何玩法。
type Hub struct {
mu sync.RWMutex // 读写锁:保护下面两个 map,读多写少所以用 RWMutex
conns map[int64]*Connection // userID -> 连接:一个用户当前对应的那条连接
handlers map[string]Handler // 消息类型 -> 处理器:决定上行消息由谁处理
}
// NewHub 构造一个空的 Hub。
func NewHub() *Hub {
return &Hub{
conns: make(map[int64]*Connection), // 初始化连接表,避免 nil map 写入 panic
handlers: make(map[string]Handler), // 初始化处理器表,避免 nil map 写入 panic
}
}
// RegisterHandler 注册某类消息的处理函数。
// TODO(第二步):hall 注册 hall.match / hall.cancel;房间注册 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 // 覆盖式注册:同名类型以最后注册者为准
}
// Register 把新建连接登记到 Hub;MultiDevice=false 时先踢掉同账号的旧连接。
func (h *Hub) Register(c *Connection) {
h.mu.Lock() // 加写锁:可能修改 conns 表
defer h.mu.Unlock() // 返回时解锁
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()) // 记录踢线日志,便于排查重复登录
old.Close() // 关闭旧连接:旧连接的读写泵会随之退出
delete(h.conns, c.UserID()) // 从表中移除旧连接,给新连接腾位置
}
}
h.conns[c.UserID()] = c // 登记新连接
}
// Unregister 从表中注销连接;只有表中仍是自己时才删除,避免误删踢线后新建的连接。
func (h *Hub) Unregister(c *Connection) {
h.mu.Lock() // 加写锁:修改 conns 表
defer h.mu.Unlock() // 返回时解锁
if cur, ok := h.conns[c.UserID()]; ok && cur == c { // 表中存在且指向同一条连接
delete(h.conns, c.UserID()) // 安全删除
}
}
// Get 取指定用户当前在线的连接。
func (h *Hub) Get(userID int64) (*Connection, bool) {
h.mu.RLock() // 加读锁:只读不写
defer h.mu.RUnlock() // 返回时解锁
c, ok := h.conns[userID] // 查表
return c, ok // 返回连接与是否在线
}
// Online 返回当前在线连接数(默认一人一条连接,即在线用户数)。
func (h *Hub) Online() int {
h.mu.RLock() // 加读锁
defer h.mu.RUnlock() // 返回时解锁
return len(h.conns) // 表长度即在线数
}
// Route 按消息类型分发上行消息;未注册的类型直接回错误,不静默丢弃,方便客户端自测。
func (h *Hub) Route(ctx context.Context, c *Connection, env *Envelope) {
h.mu.RLock() // 加读锁:查 handlers 表
fn, ok := h.handlers[env.Type] // 按类型找处理器
h.mu.RUnlock() // 立即解锁,避免业务执行期间占用锁
if !ok { // 没找到对应处理器
slog.DebugContext(ctx, "ws unsupported message type", "type", env.Type, "user_id", c.UserID()) // 记录调试日志
c.SendError(env.Seq, 0, "暂不支持的消息类型: "+env.Type) // 回一条错误信封,带上原 seq 便于客户端定位
return // 结束分发
}
fn(ctx, c, env) // 交给业务处理器执行
}
// SendTo 向单个用户下发一条已编码的消息;返回 false 表示已离线或发送队列已满。
func (h *Hub) SendTo(userID int64, data []byte) bool {
h.mu.RLock() // 加读锁:查表
c, ok := h.conns[userID] // 找到目标连接
h.mu.RUnlock() // 解锁
if !ok { // 用户不在线
return false // 返回投递失败,调用方可决定是否落库或重连后再发
}
return c.Send(data) // 入队发送
}
// Broadcast 向指定用户集合下发同一条消息。
// TODO(第二步):房间内广播就走这里——玩法产出的 Event 由房间翻译成信封后调用本方法。
func (h *Hub) Broadcast(userIDs []int64, data []byte) {
h.mu.RLock() // 加读锁
targets := make([]*Connection, 0, len(userIDs)) // 预分配切片,容量等于目标数
for _, uid := range userIDs { // 遍历目标用户 ID
if c, ok := h.conns[uid]; ok { // 在线才加入投递列表
targets = append(targets, c) // 收集连接指针
}
}
h.mu.RUnlock() // 解锁:后续发送不持锁,避免慢客户端阻塞注册表
for _, c := range targets { // 逐个投递
c.Send(data) // 入队发送(队列满会自行断开慢客户端)
}
}
// BroadcastAll 向全部在线连接下发,适用于大厅公告、全服通知等。
func (h *Hub) BroadcastAll(data []byte) {
h.mu.RLock() // 加读锁
targets := make([]*Connection, 0, len(h.conns)) // 预分配切片,容量等于当前在线数
for _, c := range h.conns { // 遍历所有连接
targets = append(targets, c) // 收集连接指针
}
h.mu.RUnlock() // 解锁后再发送
for _, c := range targets { // 逐个投递
c.Send(data) // 入队发送
}
}