RPC序列化流程
序列化的作用 在网络传输中,数据必须采用二进制形式, 所以在RPC调用过程中, 需要采用序列化技术,对入参对象和返回值对象进行序列化与反序列化。
序列化原理
自定义的二进制协议来实现序列化:
一个对象是如何进行序列化? 下面以User对象例举讲解:
User对象:
package com.itcast; public class User { /** * 用户编号 */ private String userNo = "0001"; /** * 用户名称 */ private String name = "zhangsan"; }
包体的数据组成:
业务指令为0x00000001占1个字节,类的包名com.itcast占10个字节, 类名User占4个字节;
属性UserNo名称占6个字节,属性类型string占2个字节表示,属性值为0001占4个字节;
属性name名称占4个字节,属性类型string占2个字节表示,属性值为zhangsan占8个字节;
包体共计占有1+10+4+6+2+4+4+2+8 = 41字节。
包头的数据组成:
版本号v1.0占4个字节,消息包体实际长度为41占4个字节表示,序列号0001占4个字节,校验码32位表示占4个字节。
包头共计占有4+4+4+4 = 16字节。
包尾的数据组成:
通过回车符标记结束\r\n,占用1个字节。
整个包的序列化二进制字节流共41+16+1 = 58字节。这里讲解的是整个序列化的处理思路, 在实际的序列化处理中还要考虑更多细节,比如说方法和属性的区分,方法权限的标记,嵌套类型的处理等等。
序列化的处理要素
- 解析效率:序列化协议应该首要考虑的因素,像xml/json解析起来比较耗时,需要解析doom树,二进制自定义协议解析起来效率要快很多。
- 压缩率:同样一个对象,xml/json传输起来有大量的标签冗余信息,信息有效性低,二进制自定义协议占用的空间相对来说会小很多。
- 扩展性与兼容性:是否能够利于信息的扩展,并且增加字段后旧版客户端是否需要强制升级,这都是需要考虑的问题,在自定义二进制协议时候,要做好充分考虑设计。
- 可读性与可调试性:xml/json的可读性会比二进制协议好很多,并且通过网络抓包是可以直接读取,二进制则需要反序列化才能查看其内容。
- 跨语言:有些序列化协议是与开发语言紧密相关的,例如dubbo的Hessian序列化协议就只能支持Java的RPC调用。
- 通用性:xml/json非常通用,都有很好的第三方解析库,各个语言解析起来都十分方便,二进制数据的处理方面也有Protobuf和Hessian等插件,在做设计的时候尽量做到较好的通用性。
常用的序列化技术
1). JDK原生序列化
代码: ```java ... public static void main(String[] args) throws IOException, ClassNotFoundException { String basePath = "D:/TestCode"; FileOutputStream fos = new FileOutputStream(basePath + "tradeUser.clazz"); TradeUser tradeUser = new TradeUser(); tradeUser.setName("Mirson"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(tradeUser); oos.flush(); oos.close(); FileInputStream fis = new FileInputStream(basePath + "tradeUser.clazz"); ObjectInputStream ois = new ObjectInputStream(fis); TradeUser deStudent = (TradeUser) ois.readObject(); ois.close(); System.out.println(deStudent); } ... ``` (1) 在Java中,序列化必须要实现java.io.Serializable接口。 (2) 通过ObjectOutputStream和ObjectInputStream对象进行序列化及反序列化操作。 (3) 虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致 (也就是在代码中定义的序列ID private static final long serialVersionUID) (4) 序列化并不会保存静态变量。 (5) 要想将父类对象也序列化,就需要让父类也实现Serializable 接口。 (6) Transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如基本类型 int为 0,封装对象型Integer则为null。 (7) 服务器端给客户端发送序列化对象数据并非加密的,如果对象中有一些敏感数据比如密码等,那么在对密码字段序列化之前,最好做加密处理, 这样可以一定程度保证序列化对象的数据安全。
2). JSON序列化
一般在HTTP协议的RPC框架通信中,会选择JSON方式。 优势:JSON具有较好的扩展性、可读性和通用性。 缺陷:JSON序列化占用空间开销较大,没有JAVA的强类型区分,需要通过反射解决,解析效率和压缩率都较差。 如果对并发和性能要求较高,或者是传输数据量较大的场景,不建议采用JSON序列化方式。
3). Hessian2序列化
Hessian 是一个动态类型,二进制序列化,并且支持跨语言特性的序列化框架。 Hessian 性能上要比 JDK、JSON 序列化高效很多,并且生成的字节数也更小。有非常好的兼容性和稳定性,所以 Hessian 更加适合作为 RPC 框架远程通信的序列化协议。 代码示例: ```java ... TradeUser tradeUser = new TradeUser(); tradeUser.setName("Mirson"); //tradeUser对象序列化处理 ByteArrayOutputStream bos = new ByteArrayOutputStream(); Hessian2Output output = new Hessian2Output(bos); output.writeObject(tradeUser); output.flushBuffer(); byte[] data = bos.toByteArray(); bos.close(); //tradeUser对象反序列化处理 ByteArrayInputStream bis = new ByteArrayInputStream(data); Hessian2Input input = new Hessian2Input(bis); TradeUser deTradeUser = (TradeUser) input.readObject(); input.close(); System.out.println(deTradeUser); ... ``` Dubbo Hessian Lite序列化流程: ![file](https://oscimg.oschina.net/oscnet/up-eae7d3d7dedb4eccf9c7d9dce3a123f9d2f.png) Dubbo Hessian Lite反序列化流程: ![file](https://oscimg.oschina.net/oscnet/up-859ad77b660f726b8fc7a465d51734763e0.png) Hessian自身也存在一些缺陷,大家在使用过程中要注意: + 对Linked系列对象不支持,比如LinkedHashMap、LinkedHashSet 等,但可以通过CollectionSerializer类修复。 + Locale 类不支持,可以通过扩展 ContextSerializerFactory 类修复。 + Byte/Short 在反序列化的时候会转成 Integer。
4). Protobuf序列化
Protobuf 是 Google 推出的开源序列库,它是一种轻便、高效的结构化数据存储格式,可以用于结构化数据序列化,支持 Java、Python、C++、Go 等多种语言。 Protobuf 使用的时候需要定义 IDL(Interface description language),然后使用不同语言的 IDL 编译器,生成序列化工具类,它具备以下优点: + 压缩比高,体积小,序列化后体积相比 JSON、Hessian 小很多; + IDL 能清晰地描述语义,可以帮助并保证应用程序之间的类型不会丢失,无需类似 XML 解析器; + 序列化反序列化速度很快,不需要通过反射获取类型; + 消息格式的扩展、升级和兼容性都不错,可以做到向后兼容。 代码示例: Protobuf脚本定义: ```Protobuf // 定义Proto版本 syntax = "proto3"; // 是否允许生成多个JAVA文件 option java_multiple_files = false; // 生成的包路径 option java_package = "com.itcast.bulls.stock.struct.netty.trade"; // 生成的JAVA类名 option java_outer_classname = "TradeUserProto"; // 预警通知消息体 message TradeUser { /** * 用户ID */ int64 userId = 1 ; /** * 用户名称 */ string userName = 2 ; } ``` 代码操作: ```java // 创建TradeUser的Protobuf对象 TradeUserProto.TradeUser.Builder builder = TradeUserProto.TradeUser.newBuilder(); builder.setUserId(101); builder.setUserName("Mirson"); //将TradeUser做序列化处理 TradeUserProto.TradeUser msg = builder.build(); byte[] data = msg.toByteArray(); //反序列化处理, 将刚才序列化的byte数组转化为TradeUser对象 TradeUserProto.TradeUser deTradeUser = TradeUserProto.TradeUser.parseFrom(data); System.out.println(deTradeUser); ```
本文由mirson创作, 希望对大家有所帮助, 谢谢!