# Go Gateway <-> FluffOS 对接协议

> 最后更新：2026-03-17
> 适用代码：`gateway-go/cmd/gateway/main.go` + `fluffos/src/packages/gateway/*`
> 适用范围：Go Gateway 与 FluffOS Driver/LPC 的对接，不是客户端 UI 协议总表

## 1. 先看结论

这条链路现在分成两层协议：

1. 客户端 <-> Go Gateway：`WebSocket + MsgPack`
2. Go Gateway <-> FluffOS：`TCP + 4-byte big-endian length + JSON`

FluffOS Driver 当前只原生识别 5 类来自 Go 的顶层消息：

- `hello`
- `login`
- `data`
- `discon`
- `sys`

如果 Go 直接发送其它顶层类型，Driver 当前不会分发到 LPC。

## 2. 角色分工

### 2.1 Go Gateway 负责

- WebSocket 接入与 MsgPack 编解码
- JWT 验证、账号校验、Session 管理
- 多区服 FluffOS 连接管理
- 把客户端消息统一包装后发给 FluffOS
- 把 FluffOS 发出的单播/广播消息转回客户端

### 2.2 FluffOS Driver 负责

- 维护 Gateway 主连接和 Gateway Session
- 把 `login/data/discon/sys` 路由到 LPC
- 提供 `gateway_send` / `gateway_session_send` / `gateway_status` 等 efun

### 2.3 LPC 负责

- `gateway_logon(data)`：登录落地
- `gateway_receive(data)`：处理上行业务消息
- `gateway_disconnected(reason_code, reason_text)`：处理断线
- `receive_system_message(msg)`：处理无 `cid` 的系统消息

## 3. 线缆协议

### 3.1 帧格式

Go 和 FluffOS 之间的每一帧都长这样：

```text
+----------------------+-------------------+
| 4 bytes length (BE)  | JSON body (UTF-8) |
+----------------------+-------------------+
```

- 长度头是 `big-endian`
- 长度值不包含 4 字节头部本身
- JSON 必须是完整对象

### 3.2 包大小

两边必须对齐：

- Go：`gateway-go/config.yaml` -> `protocol.max_message_size`
- FluffOS：`duobao/config.ini` -> `gateway packet size`

当前仓库默认使用：

```yaml
protocol:
  header_size: 4
  max_message_size: 64000000
```

```ini
gateway packet size : 64000000
```

### 3.3 FluffOS 启动时真正读取的 gateway 配置

Driver 启动阶段当前只直接读取这 4 个 `config.ini` 项：

- `gateway port`
- `gateway external`
- `gateway debug`
- `gateway packet size`

`max_sessions / max_masters / heartbeat_interval / heartbeat_timeout`
当前不是通过 `config.ini` 启动加载，而是走 C++ 默认值，再通过
`gateway_config()` / `gateway_set_heartbeat()` 运行时调整。

## 4. Go -> FluffOS 顶层消息

### 4.1 `hello`

方向：Go -> FluffOS

```json
{
  "type": "hello",
  "data": "go-gateway"
}
```

说明：

- Go 建立 TCP 连接后第一条发送
- Driver 只记录主连接并刷新心跳

### 4.2 `login`

方向：Go -> FluffOS

```json
{
  "type": "login",
  "cid": "20260317-120001-1",
  "data": {
    "account_id": "65f68d4d0d5b4d0a4d7e0101",
    "account": "alice",
    "character_id": "65f68d4d0d5b4d0a4d7e0202",
    "name": "阿离",
    "gender": "f",
    "avatar_id": 1001,
    "server_id": "server1",
    "session_token": "64_hex_chars",
    "game_data": {}
  }
}
```

说明：

- `cid` 是 WebSocket 会话 ID，也是 Gateway Session ID
- `data` 会被完整传入用户对象 `gateway_logon(data)`
- `game_data` 可选；Go 读取 Mongo 后原样回灌
- Driver C++ 兼容读取 `data.ip` / `data.port`，但当前 Go 登录包并不依赖这两个字段

### 4.3 `data`

方向：Go -> FluffOS

```json
{
  "type": "data",
  "cid": "20260317-120001-1",
  "data": {
    "type": "cmd",
    "data": {
      "text": "look"
    }
  }
}
```

说明：

- 除 `login` 外，客户端业务消息统一被 Go 包装成 `data`
- Driver 根据 `cid` 找到用户对象，再调用 `gateway_receive(data)`
- 如果内层可快速提取 `cmd.text`，C++ 会走更快的字符串路径

### 4.4 `discon`

方向：Go -> FluffOS

```json
{
  "type": "discon",
  "cid": "20260317-120001-1",
  "reason_code": "client_disconnected",
  "reason_text": "client disconnected",
  "reason": "client disconnected",
  "source": "go_gateway"
}
```

说明：

- Driver 收到后会销毁对应会话
- 在销毁前先调用用户对象：

```lpc
gateway_disconnected(reason_code, reason_text)
```

当前 Go 侧常见原因码：

- `client_disconnected`
- `account_kicked`
- `account_banned`
- `session_expired`
- `login_handshake_timeout`

### 4.5 `sys`

方向：双向

#### 心跳

```json
{"type":"sys","action":"ping"}
{"type":"sys","action":"pong"}
```

说明：

- Go 心跳协程会定期发 `ping`
- Driver 收到 `ping` 后立即回 `pong`

#### 带 `cid` 的系统消息

```json
{
  "type": "sys",
  "action": "some_action",
  "cid": "20260317-120001-1",
  "data": {
    "foo": "bar"
  }
}
```

说明：

- Driver 会把 `data` 投递给该用户的 `gateway_receive(data)`
- 适合 Go -> 单个玩家 的业务回包

#### 不带 `cid` 的系统消息

```json
{
  "type": "sys",
  "action": "gateway_connected",
  "data": {
    "server_id": "server1",
    "source": "go-gateway"
  }
}
```

说明：

- Driver 会调用 `/adm/daemons/gateway_d.c::receive_system_message(msg)`
- Go 当前连接成功后会主动发一次 `gateway_connected`

## 5. FluffOS -> Go 顶层消息

### 5.1 单播通用规则

只要顶层满足：

```json
{
  "type": "some_type",
  "cid": "20260317-120001-1",
  "data": {}
}
```

Go 就会把它转给对应客户端，最终发出的客户端消息统一为：

```json
{
  "type": "some_type",
  "data": {}
}
```

特例：

- `login_success`：Go 会拦截，不直接透传原始 `data`
- `login_error`：Go 会清理待确认会话，然后继续转发给客户端

### 5.2 `login_success`

方向：FluffOS -> Go

```json
{
  "type": "login_success",
  "cid": "20260317-120001-1",
  "data": {
    "name": "阿离",
    "id": "alice"
  }
}
```

说明：

- Go 只依赖 `cid` 来完成握手确认
- Go 收到后会：
  - 清理 pending login 定时器
  - 从 Session 中取账号和区服信息
  - 刷新 JWT
  - 回发客户端 `login_success`
- `data` 当前不会原样透传给客户端，因此它更像 LPC 自身附带信息，不是 Go 必填字段

### 5.3 `login_error`

方向：FluffOS -> Go

```json
{
  "type": "login_error",
  "cid": "20260317-120001-1",
  "data": {
    "message": "character not found"
  }
}
```

说明：

- Go 会回滚 Session、路由和玩家位置缓存
- 然后把这条消息继续转发给客户端

### 5.4 `output`

方向：FluffOS -> Go

```json
{
  "type": "output",
  "cid": "20260317-120001-1",
  "data": "你看到了一片树林。"
}
```

说明：

- `tell_object` / `write` 这类文本输出最终通常会落到这里
- `gateway_session_send(ob, 非mapping)` 会自动包装成 `output`

### 5.5 `broadcast`

方向：FluffOS -> Go

```json
{
  "type": "broadcast",
  "data": {
    "type": "chat_message",
    "data": {
      "scope": "world",
      "message": "天下大势，分久必合。"
    }
  }
}
```

组播：

```json
{
  "type": "broadcast",
  "cids": ["cid-1", "cid-2"],
  "data": {
    "type": "entity_update",
    "data": {
      "action": "add"
    }
  }
}
```

强约束：

- `data` 必须是二层信封：`{"type":"...","data":...}`
- 不是这个结构，Go 会直接丢弃

### 5.6 `broadcast_select`

方向：FluffOS -> Go

```json
{
  "type": "broadcast_select",
  "selector": {
    "scope": "room",
    "value": "/d/test/ceshi1"
  },
  "exclude": ["cid-2"],
  "data": {
    "type": "entity_update",
    "data": {
      "action": "remove"
    }
  }
}
```

当前支持的 `selector.scope`：

- `all`
- `room`
- `family`
- `guild`
- `team`
- `cid`
- `cids`
- `private`

说明：

- 这是项目自定义扩展，目标解析由 Go 的 `RouteIndex` 完成
- 要先有 `route_bind`，否则按房间/门派/帮派/队伍筛人会找不到目标

### 5.7 `route_bind` / `route_unbind`

方向：FluffOS -> Go

绑定：

```json
{
  "type": "route_bind",
  "cid": "20260317-120001-1",
  "data": {
    "room": "/d/test/ceshi1",
    "family": "武当派",
    "guild": "风云帮",
    "team": "team-1"
  }
}
```

解绑：

```json
{
  "type": "route_unbind",
  "cid": "20260317-120001-1",
  "data": {}
}
```

用途：

- 维护 Go 侧路由索引
- 支持 `broadcast_select`
- 支持后台实时在线玩家所在房间展示

### 5.8 当前项目中已存在的 Go 扩展消息

这些不是 Driver 的“原生顶层协议”，而是当前项目在 Go 的
`handleFluffOSData()` 中额外处理的业务消息：

- `save_data`
- `private_chat_store`
- `server_metrics`
- `item_query`
- `item_create`
- `sync_templates`
- `query_template`
- `query_templates`
- `map_version`
- `map_data`
- `map_room`
- `dungeon_seed_query`
- `dungeon_enter`
- `hook_sync`
- `room_sync`
- `npc_templates_catalog`

这类消息的特点是：

- 方向通常是 FluffOS -> Go
- 由 Go 的业务处理器消费
- 若 Go 需要再回 LPC，建议回到 `sys` 或 `data` 通道

### 5.9 一个容易踩坑的限制

Driver 当前不会分发任意新顶层类型。

也就是说，像下面这种 Go -> FluffOS 的消息：

```json
{
  "type": "save_data_response",
  "cid": "cid-1",
  "data": {
    "success": true
  }
}
```

按当前 C++ 分发器不会进 `gateway_receive()`。

如果你要让 Go 回包给 LPC，稳定做法是二选一：

1. 发 `type=sys`，带或不带 `cid`
2. 发 `type=data`，把真正业务体放进 `data`

## 6. LPC 回调契约

### 6.1 `master::connect()`

最小要求：

```lpc
object connect()
{
    if (is_gateway_user(this_object())) {
        return new(USER_OB);
    }
    return new(LOGIN_OB);
}
```

含义：

- Gateway 用户直接走 `USER_OB`
- 传统 telnet/websocket 直连用户仍可走旧登录对象

### 6.2 用户对象 `gateway_logon(data)`

职责：

- 校验 `account` / `name` / `session_token`
- 恢复角色状态
- 回灌 `game_data`
- 发送 `login_success`
- 初次进图后做一次 `route_bind`

当前仓库的真实入口在：

- `duobao/clone/user/user.c`

### 6.3 用户对象 `gateway_receive(data)`

职责：

- 处理字符串命令
- 处理结构化业务消息

当前项目内层消息示例：

- `cmd`
- `chat_send`
- `room_action`

### 6.4 用户对象 `gateway_disconnected(reason_code, reason_text)`

职责：

- 记录断线原因
- 清理路由
- 广播离场
- 进入 `net_dead` / `user_dump` 等旧流程

### 6.5 `gateway_d::receive_system_message(msg)`

职责：

- 处理无 `cid` 的系统消息
- 例如：
  - `gateway_connected`
  - `gateway_disconnected`
  - `map_update`
  - `map_sync`
  - `map_room_update`
  - `collect_server_metrics`

## 7. 关键 efun

### 7.1 `gateway_session_send(object ob, mixed data)`

对单个 Gateway 用户发包。

行为：

- 如果 `data` 是 `mapping`，C++ 只会给它补 `cid`
- 如果 `data` 不是 `mapping`，C++ 会包装成：

```json
{
  "type": "output",
  "cid": "session-id",
  "data": "..."
}
```

### 7.2 `gateway_send(mapping data, mixed users? )`

对 Go 发系统消息或广播消息。

行为：

- 第一个参数必须是 `mapping`
- 不传 `users`：原样发给所有 master 连接
- 传单个对象或对象数组：Driver 从对象上提取 Gateway `cid`，
  按 master 分组后自动给 JSON 补 `cids`

注意：

- C++ 不会替你补 `type`
- LPC 需要自己构造完整业务包
- 当前项目里的 RPC 匹配放在 `gateway_d.c` 的 `msg_id -> callback`
  映射里做，不要把 `gateway.spec` 里旧注释理解成
  “C++ 仍会直接消费 callback 参数”

### 7.3 其它常用 efun

- `gateway_status()`
- `gateway_config(key, value)`
- `gateway_set_heartbeat(interval, timeout)`
- `gateway_sessions()`
- `gateway_session_info(ob)`
- `gateway_destroy_session(session_id)`
- `is_gateway_user(ob)`

## 8. 时序与超时

### 8.1 登录握手超时

Go 在把 `login` 发给 FluffOS 后，会等待 LPC 回 `login_success`。

当前超时：

- `12s`

超时后 Go 会：

- 删除 Session
- 清理路由和玩家位置缓存
- 向 FluffOS 发 `discon(reason_code=login_handshake_timeout)`
- 向客户端回 `login_error`

### 8.2 会话过期

Go Session 当前默认：

- 最大空闲：`30m`
- 清理周期：`5m`

过期后会主动断线，并带原因码 `session_expired`。

### 8.3 FluffOS 重连

当前行为：

- 已知区服断线后，Go 会自动重连
- 自动“扫描新服务器”轮询默认关闭
- 如需重新扫描数据库里的新服务器，走后台接口：

```text
POST /api/admin/discover/trigger
```

## 9. 排障清单

### 9.1 Go 能连 TCP，但 LPC 没收到消息

先查：

- FluffOS `gateway port` 是否开启
- 包大小是否两边一致
- Go 发出的顶层 `type` 是否在 Driver 识别范围内

### 9.2 登录总是 12 秒后超时

先查：

- `gateway_logon(data)` 是否执行完成
- LPC 是否发出了带 `cid` 的 `login_success`
- 是否在 `gateway_logon()` 里抛错并提前析构了对象

### 9.3 `broadcast` 发了但客户端没收到

先查：

- `broadcast.data` 是否是严格的 `type + data` 二层结构
- 目标是否其实应走 `broadcast_select`
- `cids` 是否为空或不是字符串数组

### 9.4 `broadcast_select(room)` 找不到人

先查：

- 玩家登录后是否执行过 `gateway_route_bind`
- 房间切换时是否更新过 `room` 标签
- `selector.value` 是否与 Go 侧路由值完全一致

### 9.5 Go 回 LPC 的自定义消息不生效

先查：

- 是否误用了新的顶层 `type`
- 是否应该改成 `sys` 或 `data`

## 10. 真源文件

协议文档和教程请以这些文件为准：

- `gateway-go/cmd/gateway/main.go`
- `gateway-go/cmd/gateway/websocket.go`
- `gateway-go/session/session.go`
- `gateway-go/cmd/gateway/route_index.go`
- `fluffos/src/packages/gateway/gateway.cc`
- `fluffos/src/packages/gateway/gateway_session.cc`
- `fluffos/src/packages/gateway/gateway.spec`
- `duobao/adm/single/master.c`
- `duobao/adm/daemons/gateway_d.c`
- `duobao/clone/user/user.c`
