新增 WebSocket 网关:连接管理、心跳保活、限流与消息路由

This commit is contained in:
2026-09-27 19:26:55 +08:00
parent ff9add8655
commit 99d1827262
11 changed files with 632 additions and 8 deletions
+89
View File
@@ -0,0 +1,89 @@
// 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 秒
}