2、自定义消息协议设计
协议设计这事儿,说简单也简单,说复杂也复杂。我刚开始做消息中间件的时候,总觉得随便定个格式就能用。结果呢?线上出了几次兼容性问题,改得我头皮发麻。后来我学乖了——协议设计,一定要先想清楚原则。
协议设计原则
我个人习惯把协议设计原则归纳为四点。你想想看,一个消息协议要在不同语言、不同系统之间跑来跑去,没点规矩怎么行?
- 明确性:消息的边界必须清晰。不能出现「这个字节到底属于哪条消息」的歧义。
- 可扩展性:版本升级时,老客户端不能崩。我在项目中遇到过,加了个字段,老服务直接解析失败——血的教训。
- 高效性:序列化后的体积要小,解析速度要快。尤其是高频交易场景,多一个字节都是成本。
- 跨语言友好:你的服务可能是Java写的,下游是Go,再下游是Python。协议必须大家都能理解。
核心原则就一句话:向前兼容,向后兼容,解析无歧义。
消息头与消息体结构
消息协议一般分两部分:消息头和消息体。头里放元信息,体里放业务数据。这个思路其实跟HTTP很像——你想想看,HTTP的Header和Body是不是也是这个道理?
我常用的消息头结构长这样:
// 消息头(固定长度,16字节)
+----------------+----------------+----------------+----------------+
| Magic Number | Version | Message Type | Payload Len |
| (2 bytes) | (1 byte) | (1 byte) | (4 bytes) |
+----------------+----------------+----------------+----------------+
| Sequence ID | Timestamp | Reserved | Checksum |
| (4 bytes) | (4 bytes) | (2 bytes) | (2 bytes) |
+----------------+----------------+----------------+----------------+
解释一下每个字段的作用:
| 字段 | 长度 | 说明 |
|---|---|---|
| Magic Number | 2字节 | 魔数,用于快速识别协议。我习惯用 0xABCD |
| Version | 1字节 | 协议版本号。升级时靠这个做兼容 |
| Message Type | 1字节 | 消息类型,比如0x01表示心跳,0x02表示业务消息 |
| Payload Len | 4字节 | 消息体长度。没有这个,你都不知道该读多少字节 |
| Sequence ID | 4字节 | 序列号,用于请求-响应配对 |
| Timestamp | 4字节 | 时间戳,毫秒级 |
| Reserved | 2字节 | 保留字段,给未来扩展用 |
| Checksum | 2字节 | 校验和,防止数据在传输中被篡改 |
小技巧:Reserved字段一定要留。我曾经觉得「反正用不上」,结果后来加了个加密标志位,不得不改协议版本——麻烦得很。
消息体就简单了,放业务数据。可以是JSON字符串,也可以是Protobuf二进制。具体用哪个,看你的场景。
序列化与反序列化:JSON vs Protobuf
这是老生常谈的话题了。我两种都用过,说说我的感受。
JSON
- 优点:人类可读,调试方便。你curl一下就能看到返回内容。
- 缺点:体积大,解析慢。没有强类型约束,容易出幺蛾子。
- 适用场景:对外API、调试阶段、低频通信。
Protobuf
- 优点:体积小(比JSON小3-5倍),解析快,有强类型约束。
- 缺点:二进制不可读,调试需要工具。版本管理要小心。
- 适用场景:内部服务间通信、高频场景、移动端。
举个例子,同样的数据:
// JSON 格式
{
"user_id": 12345,
"name": "张三",
"age": 28,
"tags": ["vip", "active"]
}
// Protobuf 定义
message UserInfo {
int32 user_id = 1;
string name = 2;
int32 age = 3;
repeated string tags = 4;
}
JSON序列化后大概80字节,Protobuf只要30字节左右。差距就在这里。
注意:Protobuf的字段编号(=1, =2, =3...)一旦发布就不能改。我见过有人把字段1改成字段2,结果新旧版本解析全乱套了。
消息类型与路由键设计
消息类型决定了「这条消息是干嘛的」。路由键决定了「这条消息该发给谁」。这两个东西配合好了,你的系统才能灵活扩展。
我一般这样设计消息类型:
// 消息类型枚举
enum MessageType {
HEARTBEAT = 0x01; // 心跳
REQUEST = 0x02; // 请求
RESPONSE = 0x03; // 响应
NOTIFICATION = 0x04; // 通知
ERROR = 0x05; // 错误
}
路由键的设计,我推荐用层级结构。比如:
// 路由键格式:服务名.模块名.操作名
user.profile.get
user.profile.update
order.create
order.pay.callback
这样做的好处是,消费者可以用通配符订阅。比如 user.*.* 就能收到所有用户相关的消息。RabbitMQ和Kafka都支持这种模式。
我曾经在一个项目里,路由键设计成扁平的字符串,比如 getUserProfile、updateUserProfile。后来服务多了,根本没法做批量订阅。改路由键又涉及一堆消费者——嗯,那段时间加班加得挺狠的。
知识体系总览
下面这张图,把本章的核心逻辑串起来了。你可以对照着看,心里有个谱。
说白了,消息协议设计就是一套「约定」。你定好了规则,大家按规矩办事,系统才能跑得稳。我见过太多项目,前期图省事随便搞搞,后期改协议改到崩溃——嗯,别问我怎么知道的。
我的建议:先画好协议结构图,再写代码。哪怕多花半天时间,也比上线后出问题强。
可可精品资料库