Skip to content

序列化与通信协议

1. 这是什么

很多人学 RPC 时,前面几章都还能跟上:

  • 有 Provider
  • 有 Consumer
  • 有注册中心
  • 有负载均衡

但一到“序列化”和“通信协议”,就开始觉得抽象。

其实先别把它想复杂,你可以这样理解:

  • 序列化:把 Java 对象变成可以在网络上传输的字节数据
  • 反序列化:把收到的字节数据再还原成对象
  • 通信协议:规定这些字节怎么组织、怎么识别、怎么解析

如果只记一句话:

序列化解决“对象怎么传”,协议解决“这段数据该怎么读”。

例如一次 Dubbo 调用里,Consumer 想传一个请求对象:

java
new UserQuery(1001L)

这个对象本身不能直接在网络里“原样飞过去”。

必须先做两件事:

  1. 把对象编码成字节流
  2. 规定接收方怎么从字节流中正确拆出一条完整消息

这就是这一层存在的意义。


2. 为什么重要

这一层看似底层,实际上直接影响:

  • 性能
  • 稳定性
  • 兼容性
  • 调试难度

它决定了这些现实问题:

  • 为什么有些 RPC 框架更快
  • 为什么字段一改,调用就可能炸掉
  • 为什么有时日志只看到“反序列化失败”
  • 为什么协议一旦定下来,不能随便乱改

换句话说:

你看到的是一次方法调用,框架看到的是一段二进制消息的编码与解码。

如果这一层设计或使用不当,哪怕你的业务逻辑完全没问题,调用也照样会失败。


3. 先建立最核心的直觉

可以把一次 RPC 调用想成寄快递。

序列化像什么

像把物品装箱、贴标签:

  • 原始对象不能直接发
  • 要先打包成标准格式
  • 对方收到后才能拆开还原

协议像什么

像快递单和包装规范:

  • 这是谁寄的
  • 发到哪里
  • 包裹有多长
  • 里面是什么类型
  • 用什么方式解码

如果没有协议规范,会发生什么?

  • 收件方根本不知道从哪里开始读
  • 不知道一条消息什么时候结束
  • 不知道字段如何解释

所以你可以先记住:

  • 序列化更像“对象打包”
  • 协议更像“打包规则 + 读取规则”

4. 为什么对象不能直接传

因为网络底层传输的不是 Java 对象,而是字节。

你在代码里写的是:

java
userService.getById(1001L)

但到网络层,必须变成类似这样的信息:

  • 调用的是哪个接口
  • 调用的是哪个方法
  • 参数类型是什么
  • 参数值是什么
  • 请求 ID 是多少
  • 是否需要返回值

然后再编码成一段字节流发出去。

Provider 收到后,再按同样规则解码,还原出:

  • 方法名
  • 参数列表
  • 调用上下文

然后才能真正执行本地方法。

所以序列化从来不只是“对象转字节”这么简单,它还关系到:

  • 字段结构
  • 类型信息
  • 版本兼容
  • 安全边界

5. 一条 RPC 消息通常包含什么

你可以先用最粗粒度理解一条消息结构:

text
| 协议头 | 消息体 |

再展开一点看:

5.1 协议头可能包含

  • 魔数(判断是不是这个协议)
  • 协议版本
  • 消息类型(请求/响应)
  • 序列化方式
  • 请求 ID
  • 消息长度

5.2 消息体可能包含

  • 接口名
  • 方法名
  • 参数类型
  • 参数值
  • 返回值
  • 异常信息

这样设计的好处是:

  • 接收方知道怎么解析
  • 能区分请求和响应
  • 能根据请求 ID 做请求响应匹配
  • 能根据序列化类型选择对应解码器

6. 什么是协议头里的“魔数”

这是协议里一个很常见的概念。

魔数可以理解成:

  • 一段固定标记
  • 用来快速识别“这是不是我这个协议的数据”

通俗理解:

  • 像文件格式里的签名
  • 或像快递包裹上的特定标识

它的作用是:

  • 避免把错误数据按错误协议去解
  • 帮助快速判断数据是否合法

7. 常见序列化关注点

很多人一说序列化,只想到“快不快”。

其实至少要同时看 4 件事:

  1. 编码速度
  2. 解码速度
  3. 体积大小
  4. 兼容性

7.1 速度

序列化慢,会拖累 CPU。

7.2 体积

编码后数据太大,会浪费带宽,也会拉高延迟。

7.3 兼容性

这是实战里最容易被忽略的点。

比如你今天发的是:

java
class UserDTO {
    Long id;
    String name;
}

明天改成:

java
class UserDTO {
    Long id;
    String name;
    Integer age;
}

如果 Provider 和 Consumer 版本不一致,就可能出现:

  • 新字段丢失
  • 反序列化失败
  • 类型不匹配

所以序列化方案不是只看“跑分”,还要看“演进时稳不稳”。


8. Dubbo 为什么不能随意改协议

因为协议一旦两端约定好,就是双方沟通的共同语言。

如果 Consumer 认为:

  • 前 2 个字节是魔数
  • 后 4 个字节是长度
  • 接下来是请求 ID

而 Provider 却按另一套规则解释,那结果一定是:

  • 解码失败
  • 消息错位
  • 请求根本读不出来

所以协议不是“想怎么改就怎么改的内部实现细节”,而是:

通信双方必须长期共同遵守的一套规则。

这也是为什么协议演进一定要考虑兼容性。


9. 一个最小例子:请求是怎么被编码理解的

假设业务代码是:

java
userService.getById(1001L)

框架层可能需要抽象出类似这样的请求:

json
{
  "interface": "com.demo.UserService",
  "method": "getById",
  "parameterTypes": ["java.lang.Long"],
  "args": [1001],
  "requestId": 123456
}

然后再把这段结构编码成字节。

接收方收到后,再还原回来,才能定位到:

  • 调哪个服务
  • 调哪个方法
  • 参数是什么

所以你看到的是:

  • getById(1001L)

底层实际要处理的是:

  • 一份结构化请求消息的编码与解码

10. 动手验证:从“能调通”到“能看懂”

如果你本地有 Dubbo 项目,可以做下面几步实验。

10.1 正常跑通一次调用

先保证 Consumer 和 Provider 能正常调用。

10.2 打印请求对象和返回对象

观察:

  • 方法调用前的 Java 对象长什么样
  • 调用返回后的对象长什么样

思考中间发生了什么:

  • 对象在网络里不可能原样存在
  • 中间一定经历过编码和解码

10.3 故意修改 DTO 字段

例如新增一个字段:

java
private Integer age;

然后让 Consumer 和 Provider 保持不同版本,观察:

  • 调用是否还能成功
  • 新字段是否丢失
  • 是否出现兼容性问题

这一步能帮你真正理解:

  • 协议兼容不是理论问题,而是升级时马上会碰到的问题

11. 为什么字段变更很危险

很多人以为 DTO 改个字段只是“小事”。

但在 RPC 里,字段变更可能影响:

  • 老版本 Consumer
  • 老版本 Provider
  • 序列化兼容
  • 反序列化结果

常见危险操作包括:

  • 删除字段
  • 修改字段类型
  • 调整字段语义
  • 不做版本隔离直接上线

相对更稳妥的思路通常是:

  • 优先新增字段,而不是粗暴改旧字段语义
  • 做灰度验证
  • 确保两端兼容窗口存在

12. 最容易踩的坑

12.1 只关心速度,不关心兼容性

这是最典型的错误。

序列化跑得再快,如果版本一升级就炸,线上成本会非常高。

12.2 DTO 字段随意改

尤其是共享接口模型,一旦随便改字段类型或删除字段,就容易让老调用方出问题。

12.3 看到“反序列化失败”只怀疑网络

很多时候网络没问题,真正的问题是:

  • DTO 结构不一致
  • 协议版本不匹配
  • 序列化方式不一致

12.4 不理解协议头字段的作用

如果连:

  • 长度字段是干嘛的
  • 请求 ID 是干嘛的
  • 消息类型是干嘛的

都不理解,排查编解码问题会很痛苦。


13. 一套排查思路

以后你遇到“Dubbo 调用报编解码/序列化错误”,可以按这个顺序排:

  1. Consumer 和 Provider 的接口版本是否一致
  2. DTO 字段结构是否兼容
  3. 序列化方式是否一致
  4. 协议版本是否兼容
  5. 是否有字段类型变更或删除
  6. 是否能从日志里定位到具体哪个类/字段反序列化失败

这个顺序比一上来就怀疑“网络不稳”更有效。


14. 练习建议

练习 1:画一张消息结构图

自己画出:

  • 协议头里有哪些字段
  • 消息体里有哪些内容

练习 2:设计一个最小请求模型

例如给 getUserById 设计一份最小 RPC 请求结构,包含:

  • 接口名
  • 方法名
  • 参数类型
  • 参数值
  • 请求 ID

练习 3:模拟兼容性问题

故意让 Consumer 和 Provider 的 DTO 不一致,然后观察:

  • 是否还能调通
  • 哪些字段丢了
  • 是否直接异常

15. 自测问题

  • 序列化和通信协议分别解决什么问题?
  • 为什么网络里传的不是 Java 对象本身?
  • 协议头里为什么常常要有长度、请求 ID、消息类型这些字段?
  • 为什么协议和 DTO 结构不能随意变动?
  • 为什么说兼容性常常比“极限性能”更重要?

16. 这一章你至少要带走什么

如果你看完只记住 4 件事,就记这 4 件:

  1. 序列化解决的是“对象怎么变成字节”
  2. 协议解决的是“这些字节怎么组织、怎么识别、怎么解析”
  3. RPC 稳定性不仅取决于网络,还取决于编解码和兼容性
  4. 字段演进和协议升级必须考虑兼容,否则很容易线上炸调用

把这一层理解透了,你再看 Dubbo 的协议、序列化和链路排障,就不会觉得它只是“底层黑魔法”了。