Skip to content

Pomelo 协议规范 ​

Cherry 的默认网络协议是 pomelo 的二进制协议。它分两层编码,理解这两层是排查「客户端连不上」「消息发出去没人处理」这类问题的前提。

两层结构 ​

应用数据
   ↓
[message 层]  route 压缩 / 消息类型封装
   ↓
[package 层]  type + length + body 封装
   ↓
tcp / websocket 字节流
层职责是否必需
package 层握手、心跳、断线通知、数据封装必需
message 层route 压缩、(规范中还有)protobuf 压缩规范里可选

package 层把数据当作任意二进制处理,不做解析。这正是 message 层可以被替换的原因——Cherry 就是用一个可插拔的 序列化器 替代了规范里的 message 层 protobuf 压缩。

package 层 ​

包格式 ​

每包 = header + body:

字段长度说明
type1 字节包类型
length3 字节body 长度,大端整数
bodylength 字节内容

长度字段只有 3 字节,因此单包上限 2^24 = 16MB。

包类型 ​

值常量含义
0x00None空
0x01Handshake客户端握手请求 / 服务端握手响应
0x02HandshakeAck客户端握手确认
0x03Heartbeat心跳
0x04Data数据包
0x05Kick服务端主动断开通知

前三个加 Kick 属控制包,Data 属数据包。

握手 ​

握手让双方在连接建立后交换初始化数据,内容是 UTF-8 的 JSON 字符串(不压缩),放在 body 里。

握手请求(客户端 → 服务端):

json
{
  "sys": {
    "version": "1.1.1",
    "type": "js-websocket"
  },
  "user": {
    "desc": "any customized request data"
  }
}
  • sys.version — 客户端 SDK 版本号
  • sys.type — 客户端类型

握手响应(服务端 → 客户端):

json
{
  "code": 200,
  "sys": {
    "heartbeat": 60,
    "dict": {},
    "serializer": "json"
  },
  "user": {}
}
字段说明
code状态码,200 成功
sys.heartbeat心跳间隔(秒)
sys.dictroute 压缩字典
sys.serializer序列化方式名,json 或 protobuf
user用户自定义握手数据

握手时序(三步):

连接建立
  → 客户端发 Handshake(请求)
  → 服务端回 Handshake(响应)
  → 客户端发 HandshakeAck
握手完成

心跳 ​

  • 心跳包 length 为 0,body 为空
  • 握手完成后由客户端发起第一个心跳
  • 双方收到心跳后,延迟一个间隔再发下一个
  • 超时判定:客户端按 2 倍间隔判超时

数据与断开 ​

Data 包的 body 是上层传来的二进制数据,package 层不处理。

服务端主动断开(踢下线)时,先发一个 Kick 包(0x05)再关闭连接,客户端据此区分「被踢」和「网络断了」。

message 层 ​

消息头 ​

flag(1 字节,必需) + message id(0~5 字节,可选) + route(0~255 字节,可选)
  • message id 用 varint 变长编码,长度随数值大小变化
  • route 长度取决于是否压缩

flag 标志位 ​

flag 只用到了低 4 位:

 bit:  7 6 5 4   3 2 1   0
      ────────   ─────   ─────
      保留       消息类型  route 压缩标志
  • 消息类型:占第 2–4 位,现有 4 类
  • route 压缩标志:最低位,0 = 未压缩,1 = 压缩
  • 两部分相互独立

消息类型与结构 ​

类型flag后随字段
request----000-<message id> <route>
notify----001-<route>
response----010-<message id>
push----011-<route>

只有 request 和 response 带 message id。notify 和 push 都是单向的,不等回复。

route 编码 ​

flag 最低位编码方式
0(未压缩)1 字节长度 + UTF-8 route 字符串,最大 255 字节
1(压缩)2 字节字典编号(uInt16),查字典还原

字典由服务端在握手响应里下发(sys.dict),客户端收到后建立映射表。

Cherry 对 flag 的扩展 ​

除了规范定义的位,Cherry 额外使用了两个标志位:

掩码含义
0x10data 段 gzip 压缩,由 SetDataCompression 开启
0x20response 是错误响应

与规范实现的差异 ​

Cherry 的实现遵循了 pomelo 规范的主体,但在几处做了简化或调整。对接时以这些实际行为为准:

项规范Cherry 实现
握手响应 sys.protos可选,承载 protobuf 定义不存在,改用 sys.serializer
握手响应 user可选,用户自定义服务端不产出,只有 code + sys
握手响应 code200 / 500 / 501硬编码 200,不解析请求体、不校验版本
心跳超时后服务端不主动断开连接服务端会关闭连接
心跳超时倍数2 倍间隔服务端按 1 倍判定(客户端仍按 2 倍)
message 层 protobuf 压缩有未实现,用序列化器整体替代
route 最大长度文档写 256B实际 255B(1 字节长度字段)
最大包长2^241<<24,可通过 SetReadMaxPacketSize / SetWriteMaxPacketSize 覆盖

心跳超时这点最容易被坑

规范说服务端不会因心跳超时断开,但 Cherry 的实现会断。默认心跳 60 秒,服务端超过 60 秒没收到心跳就关连接。

客户端必须保证按握手响应里的 sys.heartbeat 定期发心跳。如果客户端自己写的而没发心跳,表现就是「连上一分钟就掉线」。

服务端配置入口 ​

go
agentActor := pomelo.NewActor("user")

agentActor.SetHeartbeat(30 * time.Second)      // 心跳间隔,默认 60s
agentActor.SetDictionary(myDict)                // route 压缩字典
agentActor.SetDataCompression(true)             // 开启 data 段 gzip
agentActor.SetSysData("key", "value")           // 追加自定义 sys 字段
agentActor.SetReadMaxPacketSize(1 << 20)        // 读包上限
agentActor.SetWriteMaxPacketSize(1 << 20)       // 写包上限

心跳间隔小于 1 秒会被兜底

SetHeartbeat 对小于 1 秒的值会回落到默认的 60 秒。

参考实现位置 ​

内容文件
包编解码net/parser/pomelo/packet/
消息编解码与路由字典net/parser/pomelo/message/
握手 / 心跳 / 踢人处理net/parser/pomelo/command.go
连接管理net/parser/pomelo/agent.go
Go 客户端实现net/parser/pomelo/client/

下一个环节:客户端怎么接这套协议 → 客户端对接。