四、序列化和反序列化

1 几个关键概念澄清

通常我们习惯将序列化(Serialization)称为编码(Encode),它将对象序列化为字节数组,用于网络传输、数据持久化或其它用途。
反之,反序列化(Deserialization)/解码(Decode)把从网络、磁盘等读取的字节数组还原成原始对象(通常是原始对象的副本)。

1.1 序列化与通信框架的关系

序列化与通信框架不是强耦合的关系,通信框架提供的编解码框架可以非常方便地支持用户通过扩展实现自定义的序列化格式。通信框架的编解码接口作为可选插件,并不强制用户一定要在通信框架内部实现消息的序列化和反序列化。

1.2 序列化与通信协议的关系

序列化与通信协议是解耦的,同一种通信协议可能由多种序列化方式承载,同一种序列化方式也可以用在不同协议里。

以 HTTP 协议为例,承载消息体的可以是 XML、JSON 等文本类的协议,也可以是图片附件等二进制流媒体协议。
在设计分布式服务框架时,序列化和反序列化是一个独立的接口和插件,它可以被多种协议重用、替换和扩展,以实现服务框架序列化方式的多样性。

1.3 是否需要支持多种序列化方式

整体而言,序列化可以分为文本类和二进制类两种,不同的业务场景需求也不同,分布式服务框架面向的领域是多样化的,因此它的序列化/反序列化框架需要具备如下特性:

  1. 默认支持多种常用的序列化/反序列化方式,文本类例如 XML/JSON 等,二进制的如 PB(Protocol Buffer)/Thrift 等。
  2. 序列化框架可扩展,用户可以非常灵活、方便地扩展其它序列化方式。

2 功能设计

从功能、跨语言支持、兼容性、性能等多个角度进行综合考量。

  1. 功能丰富。
  2. 跨语言支持。
  3. 兼容性
  4. 性能。

3 扩展性设计

利用 Netty 提供的编解码框架,可以非常快速的实现序列化/反序列化框架的扩展。

3.1 内置的序列化/反序列化功能类

为了降低用户的开发难度,Netty 对常用的功能和 API 做了装饰,以屏蔽底层的实现细节。Netty 内置的编解码功能包括 base64、Protobuf、JBoss Marshalling、spdy 等。

3.2 反序列化扩展

  1. 业务发布服务的时候,可以指定协议类型和承载数据的序列化方式,例如将购买商品服务发布成 HTTP 服务,序列化格式采用 XML;同时允许用户指定新增的序列化格式发布服务。
  2. 序列化类库能够以插件的格式插入到通信调用链中,实现序列化格式的扩展。在这个过程中,需要考虑 TCP 的黏包和拆包等底层相关的技术细节。

我们看半包的处理,如果不处理半包,Netty 调用 decode 方法传递的 ByteBuf 对象可能就是个半包,我们拿半包做反序列化就会失败,因此在反序列化之前,我们需要保证调用解码方法时传递的是个完整的数据包。

了解 TCP 通信机制的渎职应该都知道 TCP 底层的黏包和拆包,当我们在接收消息的时候,不能认为读取到的报文就是个整包消息,特别是对于采用非阻塞 I/O 和长连接通信的程序。
如何区分一个整包消息,通常有如下四种做法:

  1. 固定长度,例如每 120 个字节代表一个整包消息,不足的前面补位。解码器在处理这类定长消息的时候比较简单,每次读到指定长度的字节后进行解码。
  2. 通过回车换行符区分消息,例如 HTTP 协议。这类区分消息的方式多用于文本协议。
  3. 通过特定的分隔符区分整包消息。
  4. 通过在协议头/消息头中设置长度字段来标识整包消息。

4 最佳实践

4.1 接口的前向兼容性规范

  1. 制定”分布式服务框架接口兼容性规范“,在规范中要明确服务框架支持哪些兼容性,例如新增字段、删除字段还是修改字段。
  2. 引导客户。

4.2 高并发下的稳定性

需要模拟现网高并发场景对序列化框架做压测和稳定性测试,如果序列化框架存在全局锁较激烈的线程竞争等问题,多线程、高并发压力测试就会出现问题。究其原因是因为一些序列化框架为了实现线程安全,使用了全局锁等,这从实用角度看确实简单,但是在高并发场景下就会出现性能下降、耗时不稳定等问题。

解决此类问题的方案很多,例如每个线程聚合一个序列化/反序列化类库,避免多线程竞争。

5 个人总结

序列化/反序列化是 RPC 框架的基础组成部分,从性能、业务考虑,而 Netty 非常适合去做扩展。当然,还是清楚其中的经典黏包、拆包。