# FluffOS Gateway 接入教程

> 最后更新：2026-03-17
> 目标读者：要把自己的 FluffOS mudlib 接到本仓库 `gateway-go` 的开发者
> 推荐先读：[../api/gateway-protocol.md](../api/gateway-protocol.md)

## 1. 这篇教程解决什么问题

如果你要做的是：

- 让 Go Gateway 接入你的 FluffOS
- 让 LPC 用户对象能从 Go 收到登录和业务消息
- 让 LPC 能把文本、广播、房间事件回推给客户端

那你只需要把下面这 4 件事接好：

1. FluffOS 打开 gateway 监听
2. Go Gateway 配好区服地址并能连到 FluffOS
3. LPC 实现 3 个用户回调和 1 个守护进程回调
4. 登录成功后及时 `login_success` + `route_bind`

## 2. 最小架构图

```text
Client
  |
  | WebSocket + MsgPack
  v
gateway-go
  |
  | TCP + 4-byte BE length + JSON
  v
FluffOS Driver
  |
  | safe_apply / efun
  v
LPC (master.c / user.c / gateway_d.c)
```

## 3. 第一步：打开 FluffOS Gateway 监听

编辑 `duobao/config.ini`，至少保证这几项存在：

```ini
gateway port : 8888
gateway external : 0
gateway debug : 0
gateway packet size : 64000000
```

建议：

- 本机部署时先用 `gateway external : 0`
- 只有 Go 不在同一台机器上时再改成 `1`
- `gateway packet size` 要和 `gateway-go/config.yaml` 保持一致

注意：

- 当前 Driver 启动时只直接读取上面这 4 个 gateway 配置
- 心跳和会话上限更适合在 LPC 启动后用 efun 运行时调整

## 4. 第二步：配置 Go Gateway

编辑 `gateway-go/config.yaml`。

至少确认下面几段：

```yaml
network:
  ws_host: "0.0.0.0"
  ws_port: 8080
  api_port: 8081

database:
  mongo_uri: "mongodb://localhost:27017"
  dbname: "mud_game"

protocol:
  header_size: 4
  max_message_size: 64000000

servers:
  - server_id: "server1"
    name: "江湖一区"
    host: "127.0.0.1"
    port: 8888
    status: 0
    max_players: 1000
```

这里最关键的是 `servers`：

- `server_id`：必须和登录时 `zone_id` 一致
- `host` / `port`：必须指向 FluffOS 的 gateway 监听地址

### 4.1 `servers` 是怎么生效的

Go 启动时会先查 Mongo `servers` 集合：

- 集合里有数据：优先使用数据库
- 集合里没数据：才会用 `config.yaml` 的 `servers:` 初始化一次

这意味着：

- 改完 `config.yaml` 不代表线上会自动热更新
- 线上以数据库中的 `servers` 集合为准

## 5. 第三步：在 `master.c` 接 Gateway 用户

最小实现就是区分 Gateway 用户和传统登录用户：

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

为什么必须这样做：

- Driver 在创建 Gateway Session 时，会先走 `master->connect()`
- 如果这里仍然走旧登录对象，你后面的 `gateway_logon()` 就没有着陆点

## 6. 第四步：在用户对象实现 3 个必需回调

当前项目的真实实现可以直接参考：

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

### 6.1 `gateway_logon(mixed data)`

这是登录入口。

它至少要做这些事：

1. 校验 `data["account"]` / `data["name"]` / `data["session_token"]`
2. 记录 `session_token` / `server_id` / `character_id`
3. 回灌 `game_data`
4. 初始化角色状态
5. 发 `login_success`
6. 绑定路由标签

最小骨架可以长这样：

```lpc
void gateway_logon(mixed data)
{
    if (!mapp(data)) {
        destruct(this_object());
        return;
    }

    set("id", data["account"]);
    set_name(data["name"], ({ data["account"] }));

    set_temp("session_token", data["session_token"]);
    set_temp("server_id", data["server_id"]);
    set_temp("character_id", data["character_id"]);

    if (!undefinedp(data["game_data"])) {
        catch(apply_gateway_game_data(data["game_data"]));
    }

    gateway_session_send(this_object(), ([
        "type": "login_success",
        "data": ([
            "name": query("name"),
            "id": query("id"),
        ]),
    ]));

    gateway_route_bind(this_object(), gateway_route_tags());
}
```

### 6.2 `gateway_receive(mixed data)`

这是所有上行业务消息的统一入口。

推荐先支持两类：

1. 字符串命令
2. 结构化 `mapping`

最小骨架：

```lpc
void gateway_receive(mixed data)
{
    if (stringp(data)) {
        command(data);
        return;
    }

    if (!mapp(data)) {
        return;
    }

    switch (data["type"]) {
    case "cmd":
        if (mapp(data["data"]) && stringp(data["data"]["text"])) {
            command(data["data"]["text"]);
        }
        break;

    case "chat_send":
        // 你的聊天逻辑
        break;

    case "room_action":
        // 你的交互逻辑
        break;
    }
}
```

### 6.3 `gateway_disconnected(string reason_code, string reason_text)`

这是 Go 主动通知断线时的入口。

它至少要做：

1. 记录断线原因
2. `gateway_route_unbind(this_object())`
3. 广播玩家离场
4. 接回旧的 `net_dead` / `user_dump` 清理逻辑

## 7. 第五步：实现 `gateway_d.c`

当前项目里系统级消息放在：

- `duobao/adm/daemons/gateway_d.c`

最小骨架：

```lpc
void receive_system_message(mixed msg)
{
    if (!mapp(msg) || msg["type"] != "sys") {
        return;
    }

    switch (msg["action"]) {
    case "gateway_connected":
        debug_message("[Gateway] connected\n");
        break;

    case "gateway_disconnected":
        debug_message("[Gateway] disconnected\n");
        break;
    }
}
```

适合放进这里的事情：

- 网关连上后的全局初始化
- 地图同步通知
- 指标采集触发
- LPC 层的 RPC 回调匹配

## 8. 第六步：给 LPC 准备几个发送封装

推荐直接照当前项目的 simul_efun 封装思路来做。

参考文件：

- `duobao/adm/simul_efun/serenez.c`
- `duobao/adm/simul_efun/message.c`

建议至少准备这几个帮助函数：

- `tell_obj(ob, data, type)`：给单个用户发单播
- `tell_all(type, data, objs)`：发广播/组播
- `tell_select(type, data, selector, exclude)`：按房间/门派/帮派/队伍广播
- `gateway_route_bind(ob, tags)`：登录或换房时绑定路由
- `gateway_route_unbind(ob)`：断线时解绑路由

### 8.1 单播示例

```lpc
tell_obj(this_object(), ([
    "name": query("name"),
    "id": query("id"),
]), "login_success");
```

### 8.2 房间广播示例

```lpc
tell_select("entity_update", ([
    "action": "add",
    "category": "player",
    "entity": set_room_desc(this_object()),
]), ([
    "scope": "room",
    "value": file_name(environment(this_object())),
]), ({ this_object() }));
```

## 9. 第七步：启动顺序

推荐顺序：

1. 启动 MongoDB
2. 启动 FluffOS
3. 启动 Go Gateway
4. 客户端通过 HTTP/登录接口拿到 JWT
5. 客户端用 WebSocket 发 `login(account, token, zone_id)`

### 9.1 Windows 下启动 Go Gateway

仓库里已有：

- `gateway-go/start_gateway.bat`

它会：

1. 删除旧的 `gateway.exe`
2. `go build -o gateway.exe ./cmd/gateway`
3. 以 `--config config.yaml` 启动

### 9.2 如果 Go 比 FluffOS 先启动

也能工作，但要知道当前行为：

- Go 启动时会先尝试连接所有已知区服
- 没连上的区服不会自动“全库轮询发现”
- 但用户登录到该区服时，Go 会再次尝试建立连接

所以推荐还是先起 FluffOS，再起 Go。

## 10. 第八步：确认登录链路真的通了

最短验证链如下：

1. FluffOS 日志看到 gateway 监听成功
2. Go 日志看到 TCP 已连上
3. `gateway_d.c` 收到 `gateway_connected`
4. 客户端发 `login`
5. `gateway_logon(data)` 被调用
6. LPC 发出 `login_success`
7. Go 清掉 pending login，并给客户端回 `login_success`

### 10.1 你应该能看到的关键日志

Go：

```text
[网关] 已连接到 FluffOS[server1] at 127.0.0.1:8888
[登录] ✅ 登录消息已发送给 FluffOS[server1]，等待 FluffOS 响应...
```

FluffOS：

```text
Accepting [Gateway] connections on 127.0.0.1:8888
[Gateway] Gateway connected
```

## 11. 第九步：业务消息怎么扩

原则很简单：

### 11.1 客户端 -> LPC

客户端给 Go 的业务消息，最终都会被 Go 包成：

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

所以 LPC 里只要在 `gateway_receive()` 里按内层 `type` 分发就行。

### 11.2 LPC -> 客户端

单播：

```lpc
gateway_session_send(ob, ([
    "type": "player_update",
    "data": ([ "hp": 100 ]),
]));
```

广播：

```lpc
gateway_send(([
    "type": "broadcast",
    "data": ([
        "type": "announcement",
        "data": ([ "message": "维护公告" ]),
    ]),
]));
```

### 11.3 Go -> LPC

不要自己发新的顶层 `type`。

稳定做法：

1. 单个玩家回包：`type=sys` + `cid`
2. 全局系统消息：`type=sys`，不带 `cid`
3. 普通业务消息：`type=data`

## 12. 常见坑

### 12.1 登录一直超时

现象：

- Go 12 秒后回 `login timeout, please retry`

原因通常只有三类：

1. `gateway_logon()` 抛错或提前析构对象
2. LPC 没发 `login_success`
3. LPC 发了 `login_success`，但没带 `cid`

### 12.2 玩家广播收不到

优先检查：

1. `broadcast.data` 是否真的是 `type + data` 二层结构
2. `broadcast_select` 的 `selector` 是否合法
3. 玩家是否做过 `route_bind`

### 12.3 Go 回 LPC 的自定义响应没进 `gateway_receive()`

这是最常见误区。

当前 Driver 只认：

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

所以你要回 LPC，必须复用 `data` 或 `sys`。

### 12.4 改了 `config.yaml` 的 `servers:` 却没生效

因为数据库里已有 `servers` 集合数据时，Go 以数据库为准。

### 12.5 断线后房间广播还在发给旧 CID

通常是因为你忘了：

```lpc
gateway_route_unbind(this_object());
```

## 13. 对外发文档时建议一起附上的真源文件

如果你要把这套接入说明发给别人，建议把下面这些路径一起给出去：

- `docs/api/gateway-protocol.md`
- `docs/guides/fluffos-gateway-integration.md`
- `fluffos/src/packages/gateway/gateway.cc`
- `fluffos/src/packages/gateway/gateway_session.cc`
- `duobao/adm/single/master.c`
- `duobao/adm/daemons/gateway_d.c`
- `duobao/clone/user/user.c`
- `gateway-go/cmd/gateway/main.go`
- `gateway-go/session/session.go`
- `gateway-go/config.yaml`
