RPC:对象怎么在网络中传输

为什么需要序列化

网络传输的数据必须是二进制数据,但调用方请求的出入参数都是对象。对象是不能直接在网络中传输的,所以我们需要提前把它转换成可传输的二进制,并且要求转换算法是可逆的,这个过程我们叫做“序列化”。这时,服务提供方就可以正确的从二进制数据中分隔出不同的请求,同时根据请求类型和序列化类型,把二进制的消息体逆向还原成请求对象,这个过程叫做“反序列化”

总结来说,序列化就是将对象转换成二进制数据的过程,而反序列就是反过来将二进制转换为对象的过程。

在这里插入图片描述
那么 RPC 框架为什么需要序列化呢? RPC 的通信流程如下:
在这里插入图片描述

举个例子:发快递,我们要发一个需要自行组装的物件。发件人发之前,会把物件拆开装箱,这就好比序列化;这时候快递员来了,不能磕碰呀,那就要打包,这就好比将序列化后的数据进行编码,封装成一个固定格式的协议;过了两天,收件人收到包裹了,就会拆箱将物件拼接好,这就好比是协议解码和反序列化。

也就是说,因为网络传输的数据必须是二进制数据,所以在RPC调用中,对入参对象和返回值对象进行序列化和反序列化是一个必须的过程

常用的序列化

在不同的场景下合理地选择序列化方式,对提升 RPC 框架整体的稳定性和性能是至关重要的。

JDK 原生序列化

public class Student implements Serializable {

    private int number;

    private String name;

    public int getNumber() {
        return number;
    }

    public void setNumber(int number) {
        this.number = number;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Student{" +
                "number=" + number +
                ", name='" + name + '\'' +
                '}';
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {

        String home=System.getProperty("user.home");
        String basePath=home+"/Desktop";
        FileOutputStream fos=new FileOutputStream(basePath+"student.text");
        Student student=new Student();
        student.setName("wangkai");
        student.setNumber(1);
        ObjectOutputStream oos=new ObjectOutputStream(fos);
        oos.writeObject(student);
        oos.flush();;
        oos.flush();

        FileInputStream fis=new FileInputStream(basePath+"student.text");

        ObjectInputStream ois=new ObjectInputStream(fis);

        Student student1=(Student) ois.readObject();
        ois.close();

        System.out.println(student1);

    }
}


序列化具体的实现是由 ObjectOutputStream 完成的,而反序列化的具体实现是由 ObjectInputStream 完成的。

那么 JDK 的序列化过程是怎样完成的呢?我们看下下面这张图:
在这里插入图片描述
序列化过程就是在读取对象数据的时候,不断加入一些特殊分隔符,这些特殊分隔符用于在反序列化过程中阶段用

  • 头部数据用来声明序列化协议、序列化版本、用于高低版本向后兼容
  • 对象数据主要包括类名、签名、属性名、属性类型以及属性值,当然还有开头结尾等数据,除了属性值属于真正的对象值,其他都是为了反序列化用的元数据
  • 存在对象引用、继承的情况下,就是递归遍历“写对象”的逻辑

实际上任何一种序列化框架,核心思想就是设计一种序列化协议,将对象的类型、属性类型、属性值一一按照固定的格式写到二进制字节流中完成序列化,再按照固定的格式意义读出对象的类型、属性类型、属性值,通过这些信息重新创建出一个新的对象,来完成反序列化

想到了Redis使用的RESP,在做序列化时也是会增加很多冗余的字符,但它胜在实现简单、可读性强易于理解

JSON

JSON是典型的key-value方式,没有数据类型,是一种文本型序列化框架,在应用上非常广泛,无论是web还是磁盘存储文本类型的数据,还是基于HTTP协议的RPC框架,都会选择JSON格式。

但是注意两个问题:

  • json进行序列化的额外开销比较大,对于大数据量服务这意味着巨大的内存和磁盘开销
  • json没有连续,但像json这种强类型语言,需要通过反射统一解决,所以性能不会太好

所以,如果RPC框架选用json序列化,服务提供者和服务调用者之间传输的数据量相对要小,否则将严重影响性能

hessian

hessian是动态类型、二进制、紧凑的,并且可跨语言移植的一种序列化框架。hessian协议要比jdk、json更加紧凑、性能更高效、生成的字节数更小,有非常好的兼容性和稳定性,所以hessian更合适作为RPC框架通信的序列化协议

protobuf

protobuf是google公司内部使用的混合语言数据标准,是一种轻便、高效的结构化数据存储格式,可以用于结构化数据序列号,支持java、python、c++、go等语言。

protobuf使用的时候需要定义 IDL(Interface description language),然后使用不同语言的 IDL编译器,生成序列化工具类,它的优点是:

  • 序列化后体积比json、hessian小很多
  • IDL能清晰的描述语义,所以足以帮助并保证应用程序之间的类型不会丢失,无需类似XML解析器
  • 序列化反序列化速度很快,不需要通过反射获取类型;
  • 消息格式升级和兼容性不错,可以做到向后兼容。

Protobuf 非常高效,但是对于具有反射和动态能力的语言来说,这样用起来很费劲,这一点就不如 Hessian,比如用 Java 的话,这个预编译过程不是必须的,可以考虑使用Protostuff。

Protostuff 不需要依赖 IDL 文件,可以直接对Java 领域对象进行反 / 序列化操作,在效上跟 Protobuf 差不多,生成的二进制格式和Protobuf 是完全相同的,可以说是一个 Java版本的 Protobuf 序列化框架

总结

总结下序列化协议可以分为两类
1.文本类序列化方式,如 xml,json。优点就是可读性好,构造方便,调试也简单。不过缺点也明显,传输体积大,性能差。
2.二进制类学序列化方式,如 Hessian,Protobuf,优点性能好

RPC 框架中如何选择序列化?

必须考虑下面几点
(1)序列化协议的安全性、通用性和兼容性:这直接关系到服务调用的稳定性和可用率的

(2)性能和效率,序列化与反序列化过程是 RPC 调用的一个必须过程,那么序列化与反序列化的性能和效率势必将直接关系到 RPC 框架整体的性能和效率。

(3)空间开销,也就是序列化之后的二进制数据的体积大小。序列化后的字节数据体积越小,网络传输的数据量越小,传输数据的速度也就越快,由于RPC是远程调用,那么网络传输的速度将直接关系到请求响应的耗时。

在这里插入图片描述
综上:首选的还是 Hessian 与 Protobuf,因为他们在性能、时间开销、空间开销、通用性、兼容性和安全性上,都满足了我们的要求。其中 Hessian 在使用上更加方便,在对象的兼容性上更好;Protobuf 则更加高效,通用性上更有优势

RPC 框架在使用时要注意哪些问题?

  • 对象构造得过于复杂:属性很多,并且存在多层的嵌套,比如 A 对象关联 B 对象,B 对象又聚合 C 对象,C 对象又关联聚合很多其他对象,对象依赖关系过于复杂。序列化框架在序列化与反序列化对象时,对象越复杂就越浪费性能,消耗 CPU,这会严重影响 RPC 框架整体的性能;另外,对象越复杂,在序列化与反序列化的过程中,出现问题的概率就越高。
  • 对象过于庞大:这种情况同样会严重地浪费了性能、CPU,并且序列化一个如此大的对象是很耗费时间的,这肯定会直接影响到请求的耗时。
  • 对象有复杂的继承关系:大多数序列化框架在序列化对象时都会将对象的属性一一进行序列化,当有继承关系时,会不停地寻找父类,遍历属性。就像问题 1 一样,对象关系越复杂,就越浪费性能,同时又很容易出现序列化上的问题。

也就是说,在使用 RPC 框架的过程中,我们构造入参、返回值对象,主要记住以下几点:

  1. 对象要尽量简单,没有太多的依赖关系,属性不要太多,尽量高内聚;
  2. 入参对象与返回值对象体积不要太大,更不要传太大的集合;
  3. 尽量使用简单的、常用的、开发语言原生的对象,尤其是集合类;
  4. 对象不要有复杂的继承关系,最好不要有父子类的情况。

实际上,虽然RPC框架可以让我们发起远程调用就像本地调用一样,但是在RPC框架的传输过程中,入参和返回值的根本作用是用来传递信息的,为了提高RPC调用整体的性能和稳定性,我们的入参和返回值对象要构造得尽量简单,这很重要

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值