internals.md - 文档

内部设计决策与关键实现备忘

记录那些"代码注释说不清楚、architecture.md 又太宏观"的关键设计决策和实现细节。 后续开发者(包括 AI)在修改代码前务必阅读,避免重复踩坑。


一、为什么有两个 API 包?

gateway_api  — Worker 进程内部使用(包级函数)
gateway_sdk  — 外部进程使用(实例方法)

PHP 版只有一个 Gateway 类,通过 static::$businessWorker 是否存在来判断当前环境。Go 版拆分的原因:

  1. Go 是静态语言,不能运行时切换 static 行为
  2. 职责清晰gateway_api 复用 Worker 连接(零额外开销),gateway_sdk 自建连接池(独立生命周期)
  3. 避免循环依赖gateway_api 依赖 workergateway_sdk 不依赖任何框架包

⚠️ 两个包的协议命令完全相同,只是传输通道不同。不要为它们维护两套命令处理逻辑。


二、查询连接池(queryConnPool)为什么存在

问题:Worker 回调(OnMessage 等)运行在 Gateway 连接的读取 goroutine 中。如果在回调中通过同一连接发送查询请求并等待响应,读取循环被阻塞,响应永远收不到 → 死锁

解决gateway_api 包有两套连接:

两套连接使用相同的 CmdGatewayClientConnect 认证,但查询连接是独立的 TCP 连接。

📁 实现文件:pkg/gateway_api/gateway_conn.go


三、锁的设计原则

绝对不能做的事

  1. 持锁做网络 I/O(已修复多次)

    • 锁内只做内存操作(快照/拷贝)
    • 锁外做 Write/Dial/Read
    • 典型模式:RLock → 复制列表 → RUnlock → 遍历发送
  2. 在 RLock 回调中调用需要 Lock 的方法

    • handleSendToAll 持 RLock 遍历 → 调用 sendEncrypted → 如果 sendEncrypted 内部需要 Lock → 死锁

Gateway 核心锁

g.mu sync.RWMutex  // 保护 clientConns, workerConns, uidMap, groupMap

GatewaySDK 连接池锁

c.mu sync.Mutex  // 保护 connPool, addrCache

四、Client ID 编码是全局路由键

client_id = hex(local_ip[4B] + local_port[2B] + connection_id[4B]) = 20 字符

这不只是标识符,而是路由地址。通过解码 client_id 可以直接定位到具体的 Gateway 实例和连接。所以:

⚠️ connection_id 是 uint32,上限 ~42.9 亿。单 Gateway 实例不会溢出,但长期运行需要关注回绕。


五、加密通讯的分层

外部客户端 → Gateway    : WebSocket/TCP 明文协议(仅应用层协议编解码)
Gateway ↔ Worker       : GatewayProtocol 二进制 + AES-256-CBC 全包加密
Gateway ↔ GatewaySDK   : 同上
所有组件 ↔ Register     : JSON + AES-256-CBC + Base64 + \n 文本行协议

密钥

所有组件使用同一个 -key 参数,派生方式:

aesKey = sha256(secretKey)  // 32 字节,直接作为 AES-256 密钥

加密包格式

[4B 密文长度 BigEndian] [密文]

密文 = AES-256-CBC(随机 IV + GatewayProtocol 编码数据)

包大小限制

两层独立常量:

常量 保护对象
protocol.MaxEncryptedPacketSize 50MB 内部组件间通讯
maxPacketSize(gateway 包内) 10MB 外部客户端协议

六、心跳机制

Gateway → 客户端

配置 PingInterval + PingNotResponseLimit

Gateway ↔ Worker

Worker 每 25 秒发一次 CMD_PING,Gateway 回复 CMD_PING

📁 pkg/gateway/gateway.gopingWorkerLoop 使用预编码的心跳包,避免循环内重复分配。


七、Worker 路由策略

Gateway 收到客户端消息后,选择一个 Worker 转发:

路由决策发生在 Gateway 端,Worker 无感知。

⚠️ 同一客户端的不同消息可能路由到不同 Worker(无亲和性),所以业务状态不要放在 Worker 内存中,用 Session 或外部存储。


八、项目目录结构

cmd/
├── register/      # Register 注册中心入口
├── gateway/       # Gateway 网关入口
├── worker/        # Worker 业务处理入口(内置 echo 示例)
├── gateway-edit/  # 自定义协议示例(JsonNL)
├── dashboard/     # Web 监控面板入口
├── test-ws-tui/   # WebSocket TUI 测试客户端
├── tui-ws-chat/   # TUI 聊天客户端
├── build_all.sh   # 一键编译所有组件
└── start.sh       # 一键启动 register + gateway + worker

pkg/
├── register/      # Register 实现
├── gateway/       # Gateway 核心(连接管理、协议、Worker 通讯)
├── worker/        # BusinessWorker 实现
├── gateway_api/   # Worker 内部 API(包级函数)
├── gateway_sdk/   # 外部 SDK(GatewaySDK 结构体)
├── protocol/      # GatewayProtocol 二进制协议 + 命令常量
├── context/       # Client ID 编解码、Session 序列化
└── crypto/        # AES-256-CBC 加解密工具

九、已知的设计约束与后续方向

当前约束

  1. 单 Worker 单 goroutine 处理一个 Gateway 的消息:每个 Gateway 连接在 Worker 中是一个读循环 goroutine,消息串行处理。如果回调阻塞,该 Gateway 上所有客户端的消息都排队。

    • 解决方式:回调中用 go func(){}() 异步处理
  2. Session 存储在 Gateway 内存:不支持持久化。Gateway 重启后 Session 丢失。

    • 后续方向:可接 Redis/etcd 存储
  3. Dashboard 状态依赖内存:统计数据非持久化。

    • 后续方向:可接 Prometheus 指标导出
  4. 无 TLS 支持:当前 WebSocket/TCP 客户端连接是明文。

    • 可在前端加 Nginx/Caddy 做 TLS 终止

后续可扩展方向