客户端对接
这页写给客户端开发者:怎么接入 Cherry 服务端、握手和心跳怎么走、路由怎么写、消息怎么收发。
协议本身的字节格式见 Pomelo 协议规范,这页只讲怎么用。
先决定用哪种序列化
服务端的序列化方式决定了你该用哪个客户端 SDK。两者不能混用。
| 服务端 | 客户端 SDK | body 格式 |
|---|---|---|
SetSerializer(NewJSON()) | pomelo-client-json.js | JSON 字符串的 UTF-8 字节 |
| 默认(protobuf) | pomelo-client-protobuf.js + pb.js | protobuf 二进制 |
不确定服务端用哪种?握手响应里的 sys.serializer 字段会告诉你,值是 "json" 或 "protobuf"。
Web 端引入顺序
引入顺序有依赖关系,写错会报 Protocol is not defined。
JSON 方案
<script src="jquery-3.6.1.min.js"></script>
<script src="protocol.js"></script> <!-- 定义 window.Protocol,必须最先 -->
<script src="pomelo-client-json.js"></script> <!-- 依赖 window.Protocol -->Protobuf 方案
<script src="static/pb.js"></script> <!-- 预编译的 proto 定义 -->
<script src="static/protocol.js"></script>
<script src="static/pomelo-client-protobuf.js"></script>protocol.js 必须在前
pomelo-client-*.js 在脚本加载时就会读取 window.Protocol,不是等到第一次调用。所以 protocol.js 一定要先加载完。
protobuf 方案的 pb.js 是用 protoc 的 JS 插件从同一份 .proto 生成的,和服务端的 pb.go 同源:
protoc --js_out=import_style=commonjs,binary:. protocol/*.proto连接时序
pomelo.init(...)
↓ WebSocket 建立
发 Handshake(请求)
↓
收 Handshake(响应)→ 拿到 heartbeat / dict / serializer
↓
发 HandshakeAck
↓
握手完成 → 启动心跳定时器 → 执行 init 回调
↓
业务消息API
| 方法 | 说明 |
|---|---|
pomelo.init(params, cb) | 建立连接并完成握手,cb 在握手成功后调用 |
pomelo.request(route, msg, cb) | 发 request,等响应 |
pomelo.notify(route, msg) | 发 notify,不等响应 |
pomelo.on(event, fn) | 订阅推送或事件 |
pomelo.disconnect() | 主动断开 |
init 的参数:
| 参数 | 说明 |
|---|---|
host / port | 服务器地址与端口 |
path | WebSocket 路径,通常 '/' |
user | 自定义握手数据,服务端可从握手请求里读到 |
maxReconnectAttempts | 最大重连次数 |
基本用法
pomelo.init({ host: "127.0.0.1", port: 34590, path: "/" }, function () {
// 握手完成,可以发业务消息了
// 登录:request 有响应
pomelo.request("room.room.login", { nickname: "myname" }, function (rsp) {
console.log("登录结果", rsp);
});
// 发言:notify 不关心响应
pomelo.notify("room.room.syncMessage", { name: "myname", content: "hello" });
});
// 订阅服务端推送
pomelo.on("onMessage", function (data) {
console.log("收到消息", data);
});路由字符串
路由是三段式,用点号分隔:
room.room.login
└─┬─┘ └─┬─┘ └─┬─┘
│ │ └── 方法名
│ └──────── Actor 名(服务端 Actor 的 AliasID)
└────────────── 节点类型| 段 | 对应服务端 |
|---|---|
| 第 1 段 | route.NodeType(),用于判断本节点能否处理 |
| 第 2 段 | route.HandleName(),即目标 Actor 的 AliasID() |
| 第 3 段 | route.Method(),即 Actor 上注册的函数名 |
服务端投递时,第 3 段会作为 message.FuncName,最终匹配到 p.Local().Register("login", p.login) 里的 "login"。
段数必须正好是 3
服务端解析时会按 . 切分并要求恰好 3 段,否则返回 RouteInvalid。
最容易踩的坑是第 2 段写错:消息发出去了、服务端也不报错,但没有任何 Actor 处理它。第 2 段必须是目标 Actor 的 AliasID() 返回值。
收推送与错误响应
推送(push)
服务端用 Push(route, data) 主动下发,客户端按 route 名订阅:
pomelo.on("onNewUser", function (data) { /* 新用户加入 */ });
pomelo.on("onMessage", function (data) { /* 新消息 */ });route 名是业务自己定的,服务端推什么名字,客户端就订阅什么名字。
错误响应
服务端的 ResponseCode 会返回一个带状态码的响应:
pomelo.request("gate.user.login", loginData, function (data) {
if (data.code !== 0) {
console.log("失败,状态码", data.code);
}
});状态码含义见 错误码。
protobuf 客户端拿到的是整个对象
JSON 版 SDK 回调里直接是 msg.body;protobuf 版回调里是整个 msg 对象,要自己取 data.body / data.isError:
pomelo.request("gate.user.login", loginData, function (data) {
if (data.isError) { /* 处理错误 */ }
var rsp = proto.pb.LoginResponse.deserializeBinary(data.body).toObject();
});被踢下线
服务端主动断开时会先发一个 Kick 包,客户端可以据此区分「被踢」和「网络断了」:
pomelo.on("onKick", function (reason) {
console.log("被服务器踢下线", reason);
// 提示玩家,跳回登录界面
});服务端触发踢人的入口:Agent.Kick(reason, closed)、Actor.KickUID。
断线不等于被踢
心跳超时、网络异常导致的断开不会发 Kick 包。只有服务端主动调用 Kick 才会。所以断线重连逻辑和被踢逻辑要分开处理。
心跳
握手响应里的 sys.heartbeat 是心跳间隔(秒)。SDK 会自动维护心跳,你不用管。
但要知道服务端的行为:Cherry 服务端在心跳超时后会直接关闭连接(默认 60 秒),并且只按 1 倍间隔判定超时。
自己实现客户端时必看
规范文档说服务端不会因心跳超时断开,但 Cherry 的实现会断。如果你不用官方 SDK 而是自己实现客户端,务必按握手响应给的间隔定期发心跳包(type = 0x03,length = 0,空 body),否则表现就是「连上一分钟左右无故掉线」。
客户端侧通常按 2 倍间隔判定服务端失联。
Go 客户端
框架自带一个 Go 实现的 pomelo 客户端,用于压测机器人或服务端间互连:
import pomeloClient "github.com/cherry-game/cherry/net/parser/pomelo/client"c := pomeloClient.New()
defer c.Disconnect()
// 连接(三选一)
err := c.ConnectToTCP("127.0.0.1:10011")
err := c.ConnectToWS("127.0.0.1:10010", "/")
// 请求
msg, err := c.Request("gate.user.login", &pb.LoginRequest{
ServerId: 10001,
Token: token,
})
// 通知
err = c.Notify("game.player.enter", req)
// 订阅推送
c.On("onMessage", func(msg *pomeloMessage.Message) {
// msg.Route / msg.Data
})OnMessageFn 的签名是 func(msg *pomeloMessage.Message),route 和 body 从 msg 上取。
默认序列化是 protobuf,可以用 WithSerializer 改成 JSON:
c := pomeloClient.New(
pomeloClient.WithSerializer(cserializer.NewJSON()),
pomeloClient.WithHeartbeat(30*time.Second),
pomeloClient.WithRequestTimeout(10*time.Second),
)内部会自动完成握手、建立字典、维护心跳。参考用法见 examples/demo_cluster/robot_client。
常见问题
| 现象 | 原因 |
|---|---|
Protocol is not defined | protocol.js 没在 pomelo-client-*.js 之前加载 |
| 连上后约 1 分钟掉线 | 心跳没发,或间隔超过服务端判定值 |
| 消息发出但无人处理 | 路由第 2 段写错,必须等于目标 Actor 的 AliasID() |
| 响应解析报错 | 客户端 SDK 与服务端序列化方式不匹配,检查 sys.serializer |
收到 code = 10 | 消息在登录前发出,session 还没绑定 UID |
| 握手后立刻断开 | 服务端 isFrontend=false(纯逻辑节点不能接收客户端连接) |
示例参考
| 示例 | 方案 | 位置 |
|---|---|---|
| 单节点聊天室 | WebSocket + JSON | examples/demo_chat/static/index.html |
| 多节点集群 | WebSocket + protobuf | examples/demo_cluster/nodes/web/view/index.html |
| 压测机器人 | TCP + protobuf(Go) | examples/demo_cluster/robot_client |