best-practices.md - 文档

开发注意事项

使用 gatewayworker-go 开发时需要了解的包大小限制、最佳实践和常见陷阱。


一、包大小限制

框架在多个层级设置了包大小上限,超限时连接会被断开或报错。

限制链路图

客户端消息 ─→ Gateway 协议层 ─→ 内部加密传输 ─→ Worker
             ① 10MB              ② 50MB

各层限制详情

位置 限制 超限行为 涉及文件
客户端协议 10MB Input() 返回 -1,连接被关闭 protocol_length_field.go:16
内部通讯 50MB readEncryptedPacket 返回错误,Worker 断开重连 gateway.go, business_worker.go, gateway_conn.go, gateway_sdk.go
Register 通讯 64KB bufio.Scanner 丢弃超长行 register.go:104, business_worker.go:156

常量定义位置

限制值集中定义在两个位置,修改一处即全局生效:

常量 定义位置 影响范围
protocol.MaxEncryptedPacketSize 50MB pkg/protocol/gateway_protocol.go Gateway、Worker、GatewaySDK、gateway_api 四个组件的内部加密通讯
maxPacketSize 10MB pkg/gateway/protocol_length_field.go 面向客户端的 TCP/Frame/Text 协议层

注意:这两层限制是独立的。客户端 10MB 限制保护 Gateway 不被外部恶意大包攻击;内部 50MB 限制保护组件间通讯不因异常数据 OOM。内部限制大于客户端限制是因为内部包含额外的协议头、Session 数据和 AES 加密填充。

各协议客户端限制

协议 限制 说明
tcp:// (LengthField) 10MB body 4 字节头存 body 长度,body > 10MB 断开
frame:// 10MB 总包长 4 字节头存总长,总长 > 10MB 断开
text:// 10MB 单行 无换行符且缓冲区 > 10MB 断开
ws:// (WebSocket) 无框架限制 受 gorilla/websocket 默认限制,需注意

实际建议

推荐单条消息大小: < 1MB(最佳性能和内存表现)
安全上限:         < 5MB(不会触发任何限制)
绝对上限:         < 10MB(超过会被协议层拒绝)

⚠️ 大消息的隐患不只是限制

  • AES 加密需要 PKCS7 填充,10MB 消息加密后还会稍大
  • 广播 10MB 消息给 1000 个客户端 = 10GB 网络 I/O
  • Gateway 会在内存中持有完整消息直到所有客户端写完

二、Session 使用注意

Session 大小

Session 以 JSON 字符串形式存储在 Gateway 内存中,并在 每条消息 的协议包中传输给 Worker。

消息大小 = 28字节头部 + Session长度 + Body长度

Session 并发

同一 client 的 SetSessionUpdateSession 是通过 Gateway 的写锁保护的,不会有竞态。

但如果在 OnMessage 中读取 session、修改后再 SetSession,在极端高频消息下两次 OnMessage 可能看到同一个旧 session。建议使用 UpdateSession(merge)代替 SetSession(覆盖)。


三、回调函数注意事项

panic 处理

Worker 的回调函数(OnMessageOnConnect 等)运行在 Gateway 连接的读取 goroutine 中。如果回调 panic,该 Gateway 连接会断开重连,期间所有经过这个 Gateway 的消息都会丢失。

// ❌ 危险:未处理的 panic 会导致连接断开
bw.OnMessage = func(clientID string, msg []byte) {
    var data map[string]interface{}
    json.Unmarshal(msg, &data)
    name := data["name"].(string)  // 如果 name 不存在或不是 string → panic!
}

// ✅ 安全:做好类型检查
bw.OnMessage = func(clientID string, msg []byte) {
    var data map[string]interface{}
    if err := json.Unmarshal(msg, &data); err != nil {
        return
    }
    name, _ := data["name"].(string)  // 安全的类型断言
}

阻塞风险

OnMessage 回调不要做长时间阻塞操作(如 HTTP 请求、数据库慢查询),否则会阻塞该 Gateway 连接上所有后续消息的处理。

// ❌ 阻塞:HTTP 请求可能耗时数秒
bw.OnMessage = func(clientID string, msg []byte) {
    resp, _ := http.Get("https://slow-api.example.com/query")
    // 阻塞期间,该 Gateway 上所有客户端消息都排队等待
}

// ✅ 非阻塞:开 goroutine 异步处理
bw.OnMessage = func(clientID string, msg []byte) {
    go func() {
        resp, _ := http.Get("https://slow-api.example.com/query")
        gateway_api.SendToClient(clientID, processResponse(resp))
    }()
}

四、查询类 API 注意事项

死锁已解决

GetSessionIsOnline 等查询 API 使用独立连接池,在 OnMessage 回调中调用不会死锁

性能特性

API 类型 网络开销 说明
fire-and-forget(Send/Bind/Join) 极低 单向发送,不等待响应
单点查询(GetSession/IsOnline) 查询 1 个 Gateway
全量查询(GetAllClientCount/GetAllUidList) 并发查询所有 Gateway

全量查询在大集群下(10+ Gateway)会产生显著网络开销,不要在热路径中频繁调用:

// ❌ 每条消息都查在线数
bw.OnMessage = func(clientID string, msg []byte) {
    count := gateway_api.GetAllClientCount()  // 每秒数千次全量查询!
    ...
}

// ✅ 用定时器缓存
var onlineCount int32
go func() {
    for range time.Tick(5 * time.Second) {
        atomic.StoreInt32(&onlineCount, int32(gateway_api.GetAllClientCount()))
    }
}()

五、连接数规划

单 Gateway 连接数

因素 估算
goroutine 内存 ~4KB/连接
ClientConnection 结构 ~0.5KB/连接
系统文件描述符 需设置 ulimit -n

10 万连接 ≈ 500MB 内存。

水平扩展

多 Gateway 部署时,每个 Worker 会连接所有 Gateway:

Worker 连接数 = Gateway 数量 × Worker 数量

10 个 Gateway + 5 个 Worker = 50 条内部 TCP 连接(很轻量)。


六、WebSocket 特殊注意

消息类型

当前 WebSocket 发送默认使用 TextMessage。如果业务需要二进制消息(如 Protobuf),需要注意客户端的解析。

连接关闭

WebSocket 客户端断开时,OnClose 回调会触发。但如果客户端是非正常断开(如断网),需要等到心跳超时(PingInterval × PingNotResponseLimit)后才能检测到。

建议的心跳配置

cfg := &gateway.Config{
    PingInterval:         55,  // 55 秒发一次心跳
    PingNotResponseLimit: 1,   // 1 次未响应就断开
    PingData:             "",  // 空 = 服务端不主动发 ping,依赖客户端心跳
}