Apollo 技术文档Apollo 技术文档
指南
  • 架构概述
  • BigWorld 架构深度解析
  • BigWorld 进程架构与玩家生命周期
  • AOI九宫格系统详解
  • AOI广播与消息去重
  • Base 模块
  • Core 模块
  • Runtime 模块
  • Data 模块
  • Network 模块
  • /modules/actor.html
  • Game 模块
  • BigWorld 模块
服务器应用
API 参考
QA
GitHub
指南
  • 架构概述
  • BigWorld 架构深度解析
  • BigWorld 进程架构与玩家生命周期
  • AOI九宫格系统详解
  • AOI广播与消息去重
  • Base 模块
  • Core 模块
  • Runtime 模块
  • Data 模块
  • Network 模块
  • /modules/actor.html
  • Game 模块
  • BigWorld 模块
服务器应用
API 参考
QA
GitHub
  • MMORPG 架构 QA

Q14: 如何设计消息协议?Protobuf vs JSON vs 自定义协议?

核心结论

消息协议设计的重点,不是先在 JSON / Protobuf / 自定义二进制 里站队,而是先回答四个问题:

  • 消息边界怎么划分
  • 版本怎么演进
  • 调试怎么做
  • 性能瓶颈到底在哪

大多数 MMO 项目里,更稳妥的做法通常是:

  • 协议层先做统一消息头和版本边界
  • 业务消息体优先用 Protobuf 这类成熟方案
  • JSON 更多用于配置、调试、后台接口
  • 真正极高频、极高性能场景才考虑少量自定义二进制

一、先明确“消息协议”到底在设计什么

很多人一提消息协议,就直接开始对比 JSON 和 Protobuf。

其实消息协议至少有三层含义:

1.1 传输封装层

负责回答:

  • 一条消息从哪里开始、哪里结束
  • 消息长度怎么表示
  • 消息类型怎么区分
  • 序列号、会话号、校验位放在哪里

1.2 消息体结构层

负责回答:

  • 业务字段怎么编码
  • 数组、嵌套、枚举怎么表示
  • 类型系统怎么定义

1.3 演进与兼容层

负责回答:

  • 老版本能否看懂新消息
  • 新版本能否兼容旧客户端
  • 字段删除、替换、保留怎么做

所以协议设计不是“选一个序列化库”就结束了。


二、一个好协议最重要的不是快,而是可控

协议设计真正要优先保证的通常是:

  • 边界清楚
  • 兼容可演进
  • 容易排查
  • 性能足够

顺序不要反过来。

如果一开始只盯着“字节最小、速度最快”,最后很容易得到一套:

  • 开发效率低
  • 调试极痛苦
  • 版本兼容很脆
  • 团队没人敢改

这类协议在长期项目里通常不是好协议。


三、协议设计时先定哪些基础规则

3.1 统一消息头

无论消息体用什么格式,通常都建议先有统一头部,至少包含:

  • 消息 ID
  • 长度
  • 序列号
  • 会话或连接上下文
  • 版本号或协议标记

这样做的好处是:

  • 解包边界清楚
  • 调试更容易
  • 后续切换消息体格式也更方便

3.2 明确请求、响应、通知三种语义

这一步经常被忽略。

通常至少要区分:

  • Request:客户端发起请求
  • Response:服务端返回结果
  • Notify / Push:服务端主动推送

如果这些语义不清楚,后面很容易出现:

  • 谁该带序列号不清楚
  • 谁该等待回包不清楚
  • 超时和重试逻辑不好做

3.3 先设计错误码与可观测性

协议不只是“能传数据”,还要能让线上问题被看见。

至少要考虑:

  • 通用错误码
  • 非法消息处理
  • 日志里如何打印消息摘要
  • 调试工具能否快速查看包内容

四、JSON、Protobuf、自定义协议分别适合什么

4.1 JSON

JSON 的最大价值不是性能,而是:

  • 人类可读
  • 调试方便
  • 与 Web、后台系统天然兼容

它适合:

  • 配置文件
  • GM / 管理后台接口
  • 调试接口
  • 开发阶段临时协议

它不太适合:

  • 高频二进制消息
  • 带宽敏感的实时主链路

原因很简单:

  • 体积大
  • 解析慢
  • 类型约束弱

4.2 Protobuf

Protobuf 的最大价值是:

  • 有类型系统
  • 有成熟代码生成
  • 有明确的字段编号与演进规则
  • 体积和性能都比较平衡

它通常很适合:

  • 客户端与服务端主业务通信
  • 服务端内部 RPC
  • 需要跨语言协作的消息定义

多数项目里,如果没有特别强的理由,Protobuf 往往是比“自研协议”更稳的默认选择。

4.3 自定义二进制协议

自定义协议的优势是:

  • 可以完全按业务裁剪
  • 可以极致压缩
  • 可以减少多余字段和元信息

但代价同样明显:

  • 开发成本高
  • 调试难
  • 兼容性风险高
  • 团队维护门槛高

所以它更适合:

  • 极高频消息
  • 极致性能热点
  • 团队有长期维护能力的场景

而不是“项目一开始就全部自定义”。


五、为什么大多数项目优先选 Protobuf

这是一个很务实的选择。

原因通常不是它绝对最快,而是它在多个维度上比较平衡:

  • 性能足够高
  • 体积足够小
  • 兼容机制清晰
  • 工具链成熟
  • 团队协作成本低

对 MMO 来说,这种“整体最稳”的价值往往高于某个单点性能指标。

如果你还没证明协议序列化是瓶颈,过早自定义协议通常收益不大。


六、自定义协议真正值得投入的场景

6.1 高频状态同步

例如:

  • 位置
  • 朝向
  • 输入状态
  • 高频 AOI 更新

这类消息的特点是:

  • 频率高
  • 字段固定
  • 对体积非常敏感

这时可以考虑专门做更轻量的二进制布局。

6.2 固定结构、极小消息

如果一个消息永远只有几个固定字段,用完整的通用序列化框架反而可能有额外成本。

6.3 但仍然要注意边界

即使做自定义,也不建议把整个系统都拖进去。

更合理的方式通常是:

  • 主业务消息仍用成熟协议
  • 少量热点消息单独自定义

这样收益和维护成本更平衡。


七、协议演进比格式选型更重要

长期项目里,真正把协议做死的,往往不是序列化方式,而是演进策略没设计好。

7.1 常见演进规则

  • 新增字段尽量向后兼容
  • 不随意复用旧字段编号
  • 删除字段前先废弃,再清理
  • 服务端尽量容忍旧客户端缺少新字段

7.2 最怕的不是“字段多”,而是“语义漂移”

例如:

  • 一个字段最开始表示“等级”
  • 后来偷偷改成“段位”

这种问题比格式选择本身危险得多。

7.3 所以协议文档必须稳定

消息协议最好是:

  • 有统一定义源
  • 有版本管理
  • 有字段注释
  • 有生成流程

否则很快会出现客户端和服务端各自理解不一致的问题。


八、调试能力必须被当成协议设计的一部分

如果一个协议快,但线上根本没法快速看懂,那长期成本会很高。

至少要考虑:

  • 是否能打印消息摘要
  • 是否能把二进制消息转成可读文本
  • 是否有抓包和回放工具
  • 是否能快速定位字段错位和版本不匹配

这也是为什么很多项目即便主协议用 Protobuf,仍然会保留:

  • JSON 调试输出
  • 文本日志映射
  • 协议可视化工具

九、一个更实用的选择方式

9.1 默认策略

大多数情况下可以先这样做:

  • 统一消息头:自定义
  • 业务消息体:Protobuf
  • 配置和后台接口:JSON

这是一个很常见、也很稳的组合。

9.2 什么时候再引入自定义二进制

满足以下条件时再考虑:

  • 某些消息是稳定热点
  • 已经确认序列化和带宽是主要瓶颈
  • 团队具备工具链和调试能力

9.3 不要把“追求极致”当成默认路线

很多团队最容易犯的错误是:

  • 一开始就想做最极致的自定义协议
  • 结果还没上线,协议工具链和兼容问题先把自己拖住

更好的方式通常是:

  • 先用成熟协议跑通
  • 再针对真实热点局部优化

十、总结

消息协议设计的核心,不是先选 JSON、Protobuf 还是自定义,而是先把边界、版本、调试和演进方式设计清楚。

对大多数 MMO 项目来说,更稳妥的经验通常是:

  • 主业务通信优先 Protobuf
  • 配置和调试优先 JSON
  • 高频热点消息再考虑局部自定义

如果没有先把协议演进和调试工具做好,再快的协议也很容易在长期维护里变成负担。


参考资料

  • Protobuf
  • FlatBuffers
  • MessagePack
  • KBEngine GitHub - Messages
在 GitHub 上编辑此页
最后更新: 3/20/26, 6:06 AM
贡献者: cuihairu