Skip to content

客户端对接 ​

这页写给客户端开发者:怎么接入 Cherry 服务端、握手和心跳怎么走、路由怎么写、消息怎么收发。

协议本身的字节格式见 Pomelo 协议规范,这页只讲怎么用。

先决定用哪种序列化 ​

服务端的序列化方式决定了你该用哪个客户端 SDK。两者不能混用。

服务端客户端 SDKbody 格式
SetSerializer(NewJSON())pomelo-client-json.jsJSON 字符串的 UTF-8 字节
默认(protobuf)pomelo-client-protobuf.js + pb.jsprotobuf 二进制

不确定服务端用哪种?握手响应里的 sys.serializer 字段会告诉你,值是 "json" 或 "protobuf"。

Web 端引入顺序 ​

引入顺序有依赖关系,写错会报 Protocol is not defined。

JSON 方案

html
<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 方案

html
<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 同源:

bash
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服务器地址与端口
pathWebSocket 路径,通常 '/'
user自定义握手数据,服务端可从握手请求里读到
maxReconnectAttempts最大重连次数

基本用法 ​

js
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 名订阅:

js
pomelo.on("onNewUser", function (data) { /* 新用户加入 */ });
pomelo.on("onMessage", function (data) { /* 新消息 */ });

route 名是业务自己定的,服务端推什么名字,客户端就订阅什么名字。

错误响应 ​

服务端的 ResponseCode 会返回一个带状态码的响应:

js
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:

js
pomelo.request("gate.user.login", loginData, function (data) {
    if (data.isError) { /* 处理错误 */ }
    var rsp = proto.pb.LoginResponse.deserializeBinary(data.body).toObject();
});

被踢下线 ​

服务端主动断开时会先发一个 Kick 包,客户端可以据此区分「被踢」和「网络断了」:

js
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"
go
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:

go
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 definedprotocol.js 没在 pomelo-client-*.js 之前加载
连上后约 1 分钟掉线心跳没发,或间隔超过服务端判定值
消息发出但无人处理路由第 2 段写错,必须等于目标 Actor 的 AliasID()
响应解析报错客户端 SDK 与服务端序列化方式不匹配,检查 sys.serializer
收到 code = 10消息在登录前发出,session 还没绑定 UID
握手后立刻断开服务端 isFrontend=false(纯逻辑节点不能接收客户端连接)

示例参考 ​

示例方案位置
单节点聊天室WebSocket + JSONexamples/demo_chat/static/index.html
多节点集群WebSocket + protobufexamples/demo_cluster/nodes/web/view/index.html
压测机器人TCP + protobuf(Go)examples/demo_cluster/robot_client