// 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 // TODO(第二步 大厅):在此补充 TypeHallMatch = "hall.match"、TypeHallCancel = "hall.cancel" // TODO(第二步 房间):在此补充 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" // 错误下行:参数错误、未支持类型、业务错误统一走这里 ) // 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 秒 }