Files

99 lines
5.1 KiB
Go
Raw Permalink 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 提供 WebSocket 长连接网关。
// 职责边界:本包只做「传输」——建连鉴权、连接管理、心跳保活、限流、消息编解码与路由分发;
// 不理解任何玩法语义,大厅与房间的业务由 internal/hall、internal/game 注册 handler 后接入。
package wsgateway // 包名声明:本目录下所有文件同属 wsgateway 包
import (
"encoding/json" // 标准库:用于把信封与业务体序列化为 JSON、以及把上行 JSON 反序列化
"fmt" // 标准库:用 fmt.Errorf 包装错误,保留上下文
"time" // 标准库:取当前时间戳,填充 hello 下行消息中的 server_time
)
// ===== 上行消息类型:客户端 → 服务端 =====
const (
TypePing = "ping" // 应用层心跳:客户端周期性发送,服务端原样回 pong
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" // 错误下行:参数错误、未支持类型、业务错误统一走这里
TypeHallMatched = "hall.matched" // 匹配成功,携带房间 ID
TypeHallCancelled = "hall.cancelled" // 匹配已取消
TypeRoomState = "room.state" // 房间全量快照
TypeRoomEvent = "room.event" // 玩法增量事件
TypeRoomFinished = "room.finished" // 对局结束与结算
TypeRoomClosed = "room.closed" // 房间已关闭
)
// Envelope 是上下行消息的统一信封。
// 约定:业务差异全部放进 payload,网关只认识 type 与 seq,不解析业务字段。
type Envelope struct {
Type string `json:"type"` // 消息类型:决定路由到哪个 handler
Seq int64 `json:"seq,omitempty"` // 客户端自增序号:服务端回包原样带回,便于客户端对账;为空则不输出该字段
Payload json.RawMessage `json:"payload,omitempty"` // 业务体:保持原始字节,由具体模块的 handler 自行反序列化
}
// ErrorPayload 是 type=error 时的下行体。
// TODO(第二步):Code 建议复用 pkg/errcode 的业务码(例如 errcode.InvalidParams.Code()),保持前后端错误码一致。
type ErrorPayload struct {
Code int `json:"code"` // 业务错误码
Msg string `json:"msg"` // 错误描述,可直接展示给用户
Details string `json:"details,omitempty"` // 可选的补充信息(如参数校验详情),为空则不输出
}
// HelloPayload 是 type=hello 时的下行体。
type HelloPayload struct {
UserID int64 `json:"user_id"` // 服务端从 token 解析出的用户 ID
PingInterval int `json:"ping_interval"` // 心跳间隔(秒):客户端按此间隔发送 {"type":"ping"}
ServerTime int64 `json:"server_time"` // 服务端当前秒级时间戳:供客户端校正时钟
}
// Encode 把任意 payload 打包成信封并返回 JSON 字节。
func Encode(typ string, seq int64, payload any) ([]byte, error) {
env := Envelope{Type: typ, Seq: seq} // 先构造信封,填入类型与序号
if payload != nil { // payload 为空(如 pong)时跳过序列化
raw, err := json.Marshal(payload) // 把业务体序列化为 JSON 字节
if err != nil { // 序列化失败通常意味着 payload 里有不可序列化的字段
return nil, fmt.Errorf("marshal payload: %w", err) // 包装错误并返回给调用方
}
env.Payload = raw // 把序列化结果放入信封
}
return json.Marshal(env) // 把整个信封序列化为最终要发送的字节
}
// Decode 把上行 JSON 字节解析为信封。
func Decode(data []byte) (*Envelope, error) {
var env Envelope // 声明目标结构体
if err := json.Unmarshal(data, &env); err != nil { // 反序列化上行数据
return nil, fmt.Errorf("unmarshal envelope: %w", err) // 格式错误:返回给上层,由上层回 error 消息
}
if env.Type == "" { // 缺少 type 字段无法路由
return nil, fmt.Errorf("envelope type is empty") // 返回明确的错误原因
}
return &env, nil // 返回解析成功的信封
}
// BindPayload 把信封中的 payload 解析到调用方给定的结构体,供各模块 handler 使用。
func BindPayload(env *Envelope, out any) error {
if len(env.Payload) == 0 { // 上行未携带业务体
return fmt.Errorf("payload is empty") // 返回错误,handler 可据此回参数错误
}
return json.Unmarshal(env.Payload, out) // 按 out 的类型反序列化
}
// nowUnix 返回当前秒级时间戳。
func nowUnix() int64 {
return time.Now().Unix() // 取当前时间并转换为 Unix 秒
}