从零实现一个轻量级 RPC 框架-系列文章
Github: https://github.com/DongZhouGu/XRpc
前言
RPC 需要将对象序列化成二进制数据,写入本地 Socket 中,然后被网卡发送到网络设备中进行网络传输,序列化的速度以及序列化后的数据大小非常影响网络通信的效率,这里,我们实现了多中序列化的方法,并通过 SPI 实现自定义拓展。
对象是不能直接在网络中传输,我们需要提前把它转成可传输的二进制,并要求转换算法是可逆的,这个过程我们一般叫做“序列化”。
服务提供方就可以正确的从二进制数据中分割出不同的请求,同时根据请求类型和序列化类型,把二进制消息逆向还原成请求对象,称之为”反序列化“。
序列化要素
- 解析效率:序列化协议应该首要考虑的因素,像 xml/json 解析起来比较耗时,需要解析 doom 树,二进制自定义协议解析起来效率要快很多。
- 压缩率:同样一个对象,xml/json 传输起来有大量的标签冗余信息,信息有效性低,二进制自定义协议占用的空间相对来说会小很多。
- 扩展性与兼容性:是否能够利于信息的扩展,并且增加字段后旧版客户端是否需要强制升级,这都是需要考虑的问题,在自定义二进制协议时候,要做好充分考虑设计。
- 可读性与可调试性:xml/json 的可读性会比二进制协议好很多,并且通过网络抓包是可以直接读取,二进制则需要反序列化才能查看其内容。
- 跨语言:有些序列化协议是与开发语言紧密相关的,例如 dubbo 的 Hessian 序列化协议就只能支持 Java 的 RPC 调用。
- 通用性:xml/json 非常通用,都有很好的第三方解析库,各个语言解析起来都十分方便,二进制数据的处理方面也有 Protobuf 和 Hessian 等插件,在做设计的时候尽量做到较好的通用性。
序列化算法
最简单的一种就是直接实现 JDK 自带的序列化接口 Serializable 就可以了,但是这种方式不支持跨语言调用,而且性能比较低。现在常用的序列化协议有 hessian,kyro,protostuff。另外 JSON 和 XML 这种文本类序列化方式,可读性比较好,但是性能也比较差。
JDK 序列化
1 | public class RpcRequest implements Serializable{ |
这里的 serialVersionUID 是我们指定的序列化数据的版本,当对这个类的对象进行序列化操作的时候,serialVersionUID 会被写入到二进制序列中,当反序列化的时候会检查这个二进制序列的 serialVersionUID 是否和当前类的 serialVersionUID 相同,如果相同才会正常进行,否则就会抛出 InvalidClassException 异常。一般我们会手动指定 serialVersionUID,如果没有手动指定,编译器会自动生成默认的 serialVersionUID。如果想把一个 Java 对象变为 byte[]数组,需要使用 ObjectOutputStream。它负责把一个 Java 对象写入一个字节流
缺点:
- 不支持跨语言调用,其他语言无法使用
- 相比其他序列化框架封装的序列化功能性能较低,主要原因是序列化后的字节数组体积较大,传输成本高。
Kryo
Kryo 是一个高性能的序列化/反序列化工具,由于其变长存储的特性,并且使用了字节码生成机制(底部使用了 ASM 库),拥有较高的运行速度和较小的字节码体积。Kryo 作为一个成熟的序列化工具,在 Twitter,Groupon,Yahoo 等多个著名开源项目中都有广泛使用. 号称 Java 中最快的序列化框架
优点:接口易用、解析快、体积小
缺点:只支持 Java、增删字段会异常
Hessian2
Hessian 是动态类型、二进制、紧凑的,并且可跨语言移植的一种序列化框架。Hessian 协议要比 JDK、JSON 更加紧凑,性能上要币 JDK、JSON 序列化高很多,而且序列化的字节数也要更小。有非常好的兼容性和稳定性,所以 Hessian 更加适合作为 RPC 框架远程通信的序列化协议。
但是 Hessian 本身也有问题,比如:
- Linked 系列,LinkedHashMap、LinkedHashSet 等,但是可以通过扩展 CollectionDeserializer 类修复
- Locale 类,可以通过扩展 ContextSerializerFactory 类修复
- Byte/Short 反序列化的时候编程 Integer
Protobuf
Protobuf 是 Google 内部的混合语言数据标准,是一种轻便、高效的结构化数据存储格式,可以用于结构化数据序列化,支持 Java、Python、C ++、Go 等语言。 Protobuf 使用时需要定义 IDL,使用不同语言的 IDL 编译器,生成序列化工具类
优点:
- 序列化后体积相比 JSON 、Hessian 之类的小很多
- IDL 能清晰地描述语义,保证应用程序之间的类型不会丢失,无需类似 XML 解析器
- 序列化反序列化速度很快,不需要通过反射获取类型
- 消息格式升级和兼容性不错,可以做到向后兼容
但是使用 Protobuf 对于具有反射和动态能力的语言来说使用起来很费劲,可以考虑使用 Protostuff。
Protostuff 不需要依赖 IDL 文件,可以直接对 Java 领域对象进行反/序列化操作,在效率上根 Protobuf 差不多,生成的二进制格式和 Protobuf 是完全相同的,可以说是一个 Java 版本的 Protobuf 序列化框架。
缺点:
- 不支持 null
- Protostuff 不支持单纯的 Map、List 集合对象,需要包在对象里面
JSON
JSON 是典型的 key-value 方式,没有数据类型,是一种文本型序列化框架。
JSON 序列化的两大问题:
- JSON 进行序列化的额外空间开销比较大,对于数据量大的服务这意味着需要巨大的内存和磁盘开销。
- JSON 没有类型,但像 Java 这种强类型语言,需要通过反射同一解决,性能不太好。
所以如果 RPC 框架选用 JSON 序列化,服务提供者与服务调用者之间传输的数据量要相对较小,否则将严重影响性能。一般来说 JSON 用在 HTTP 中多一些,因为具有较好的可读性。
- FastJson 是阿里开源的 JSON 解析库。正如其名,“快”是其主要卖点。从官方的测试结果来看,FastJson 确实是最快的,比 Jackson 快 20% 左右,但是近几年 FastJson 的安全漏洞比较多,而且版本升级可能会存在较大的兼容问题,所以在选择的时候,还是需要谨慎一些。
- Jackson 相对 FastJson 的功能比较多,安全漏洞也比较少,社区活跃。虽然性能相对于 Jackson 稍差,但是用着安心。但是其序列化结果的体积比较大,对 RPC 框架来说,还是不大适合的。
性能对比
摘自美团技术团队文章https://tech.meituan.com/2015/02/26/serialization-vs-deserialization.html
解析时间
解析空间
代码实现
定义序列化接口
1 |
|
Kyro 序列化
Kryo 不是线程安全的。每个线程都应该有自己的 Kryo 对象、输入和输出实例。
因此在多线程环境中,可以考虑使用 ThreadLocal 或者对象池来保证线程安全性
1 | public class KryoSerializer implements Serializer { |
Hessian 序列化
1 | public class HessianSerializer implements Serializer { |
Protostuff 序列化
1 | public class ProtostuffSerializer implements Serializer { |