RPC实现原理之核心技术-序列化

  1. RPC序列化流程

RPC实现原理之核心技术-序列化_第1张图片

  1. 序列化的作用
    在网络传输中,数据必须采用二进制形式, 所以在RPC调用过程中, 需要采用序列化技术,对入参对象和返回值对象进行序列化与反序列化。
  2. 序列化原理

    自定义的二进制协议来实现序列化:
    RPC实现原理之核心技术-序列化_第2张图片

    一个对象是如何进行序列化? 下面以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字节。这里讲解的是整个序列化的处理思路, 在实际的序列化处理中还要考虑更多细节,比如说方法和属性的区分,方法权限的标记,嵌套类型的处理等等。

  3. 序列化的处理要素

    • 解析效率:序列化协议应该首要考虑的因素,像xml/json解析起来比较耗时,需要解析doom树,二进制自定义协议解析起来效率要快很多。
    • 压缩率:同样一个对象,xml/json传输起来有大量的标签冗余信息,信息有效性低,二进制自定义协议占用的空间相对来说会小很多。
    • 扩展性与兼容性:是否能够利于信息的扩展,并且增加字段后旧版客户端是否需要强制升级,这都是需要考虑的问题,在自定义二进制协议时候,要做好充分考虑设计。
    • 可读性与可调试性:xml/json的可读性会比二进制协议好很多,并且通过网络抓包是可以直接读取,二进制则需要反序列化才能查看其内容。
    • 跨语言:有些序列化协议是与开发语言紧密相关的,例如dubbo的Hessian序列化协议就只能支持Java的RPC调用。
    • 通用性:xml/json非常通用,都有很好的第三方解析库,各个语言解析起来都十分方便,二进制数据的处理方面也有Protobuf和Hessian等插件,在做设计的时候尽量做到较好的通用性。
  4. 常用的序列化技术

    1). JDK原生序列化

    代码:

    ...
        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 框架远程通信的序列化协议。

    代码示例:

    ...
    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序列化流程:
    RPC实现原理之核心技术-序列化_第3张图片

    Dubbo Hessian Lite反序列化流程:
    RPC实现原理之核心技术-序列化_第4张图片

    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脚本定义:

    // 定义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 ;
    }     

    代码操作:

    // 创建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创作, 希望对大家有所帮助, 谢谢!

你可能感兴趣的:(java)