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
- 高频热点消息再考虑局部自定义
如果没有先把协议演进和调试工具做好,再快的协议也很容易在长期维护里变成负担。
