Skip to content

消息序列化 ​

序列化决定消息在网络上长什么样。Cherry 把它抽象成一个可替换的接口,框架的多个部分共用同一份实现。

接口 ​

定义在 facade/serializer.go:

go
type ISerializer interface {
	Marshal(v interface{}) ([]byte, error)
	Unmarshal(data []byte, v interface{}) error
	Name() string   // 标识当前 codec,如 "json" / "protobuf"
}

只有三个方法。集群、Actor 系统、网络解析器都通过它做编解码,所以换一个实现就整体换掉了。

两种内置实现 ​

JSONProtobuf
构造cserializer.NewJSON()cserializer.NewProtobuf()
底层库json-iterator/gogoogle.golang.org/protobuf/proto
Name()jsonprotobuf
可读性好,便于调试差,二进制
体积 / 速度一般优
go
import cserializer "github.com/cherry-game/cherry/net/serializer"

app.SetSerializer(cserializer.NewJSON())

两者都有 []byte 直通

如果 Marshal 的入参已经是 []byte,两种实现都会原样返回,不再编码。这让上层可以自行控制某些消息的编码方式。

默认是 Protobuf ​

cherry.Configure(...) 创建的 Application 默认使用 protobuf:

go
serializer: cserializer.NewProtobuf()

所以要用 JSON,必须显式调用 SetSerializer。

设置时机 ​

必须在 app.Startup() 之前设置。

原因是握手包在启动时就预编码并缓存了。Startup() 里加载网络解析器时会构造握手响应,把 app.Serializer().Name() 写进 sys.serializer 字段:

json
{ "code": 200, "sys": { "heartbeat": 60, "dict": {}, "serializer": "json" } }

启动之后再改虽然运行期的 Marshal / Unmarshal 会跟着变,但握手包里缓存的 codec 名已经发出去了,客户端会按错误的格式解析 body。

go
app := cherry.Configure(...)

app.SetSerializer(cserializer.NewJSON()) // 在 Startup 之前

app.Startup()

两种格式的写法 ​

JSON ​

业务 struct 加 json:"..." tag 即可,无需额外步骤:

go
type LoginRequest struct {
	Nickname string `json:"nickname"`
}

type SyncMessage struct {
	Name       string `json:"name"`
	Content    string `json:"content"`
	PacketTime int64  `json:"packetTime"`
}

tag 里的名字就是客户端看到的名字。改这里等于改协议,客户端必须同步。

Protobuf ​

protobuf 的实现会对入参做 proto.Message 断言,断言失败直接返回 ProtobufWrongValueType。

所以 struct tag 方式不适用于 protobuf,必须写 .proto 并生成 pb.go:

protobuf
syntax = "proto3";
package pb;
option go_package = "my-game/pb";

message LoginRequest {
  int32 serverId = 1;
  string token = 2;
  map<int32, string> params = 5;
}

生成的类型直接作为 Actor handler 的参数,框架会自动反序列化:

go
func (p *ActorAgent) login(session *cproto.Session, req *pb.LoginRequest) {
	// req 已经反序列化好了
}

生成命令:

bash
protoc --go_out=. --go_opt=paths=source_relative protocol/*.proto

不需要注册

protobuf 消息不用像 pomelo 原版那样在握手时下发 proto 定义,也不需要注册表。生成的 pb.go 直接参与编译,双方各自持有同一份定义即可。

序列化发生在哪里 ​

理解调用点有助于排查「反序列化失败」这类问题。

环节位置
客户端 body保持原始字节,不在这里反序列化
Actor 调用 handler 前app.Serializer().Unmarshal(argBytes, argValue)
Actor 返回值app.Serializer().Marshal(retValue)
发送响应 / 推送Response / Push / Kick 内部做 Marshal
跨节点调用参数System.marshalArg → Marshal(arg)
跨节点响应Unmarshal(rspData, reply)

客户端消息是延迟反序列化的

从客户端收到的包,body 一路以原始 []byte 传到 Actor 的投递阶段(message.Args = msg.Data),直到真正要调用 handler 时才反序列化。所以「反序列化失败」的报错发生在 Actor 调用处,而不是收包处——排查时不要只盯着网络层。

跨节点消息用的是另一套 ​

别混淆

节点之间传的 ClusterPacket 不使用 ISerializer,而是固定用 protobuf 序列化的 proto 消息。ISerializer 只管 ClusterPacket 内部的业务参数。

所以即使你把序列化器改成 JSON,节点间通信的报文格式也不变。

客户端必须对齐 ​

服务端用哪种 codec,客户端就要用对应的 SDK:

服务端客户端
NewJSON()pomelo-client-json.js
NewProtobuf()pomelo-client-protobuf.js + 预编译的 pb.js

客户端可以通过握手响应的 sys.serializer 字段得知服务端用的是哪种。

protobuf 客户端不是用 pomelo 原版的 proto 下发机制,而是直接用 protoc --js_out 生成的 pb.js:

js
var loginRequest = new proto.pb.LoginRequest();
loginRequest.setServerid(serverId);
loginRequest.setToken(token);
var loginData = loginRequest.serializeBinary(); // Uint8Array

pomelo.request("gate.user.login", loginData, function (data) {
    var rsp = proto.pb.LoginResponse.deserializeBinary(data.body).toObject();
});

细节见 客户端对接。

选型建议 ​

场景建议
网页调试、快速验证JSON,浏览器里能直接看到消息内容
正式环境Protobuf,省带宽、省 CPU
内网低流量服务JSON 也可以,可维护性更好

可以混用吗

不可以。一个节点的序列化器是全局唯一的,同一节点上的所有连接共用一套 codec。要按连接区分需要自己扩展解析器。