RPC原理(1)之深入RPC原理简介

一、RPC调用原理图

下面这张图是我们微服务一次Http调用请求图:

首先在请求的过程中我们知道是有三次握手,四次挥手的流程,具体流程如下:

1.浏览器请求服务器(订单服务),请求建立连接,首先客户端向服务器端发送一段 TCP 报文
2.服务器(订单服务)相应浏览器,可以建立连接,并且询问浏览器是否马上建立连接。
3.浏览器相应服务器(订单服务),可以建立连接。



-----开始传输数据------------

###下面要断开连接就进入四次挥手环节
1.浏览器向服务器(订单服务)发起请求,需要断开连接(分手)
2.服务器(订单服务)相应浏览器,说我收到断开请求了,但是需要在想一下(一般是数据传输)
3.服务器(订单服务)接受完数据之后,向浏览器发送请求,说可以断开连接了(想好了,可以分手了)
4.浏览器接受到服务器(订单服务),回复服务器,我断开连接了。

1.1RPC概述

RPC(Remote Procedure Call Protocol)远程过程调用协议。RPC的主要功能是让构建分布式计算更加容易,再提供强大的远程调用能力的同时而不损失本地调用的语义简洁性。为实现该目标,RPC框架需要提供一套透明的调用机制,使使用者不用显示的区分本地调用还是分布式调用。

RPC的优点:

  • 分布式设计
  • 部署灵活
  • 解耦服务
  • 扩展性强

RPC框架的优势:

  • RPC框架一般使用长连接,不必要每次通信都三次握手、四次挥手。
  • RPC框架一般有注册中心,有丰富的监控管理、发布、下线接口、动态扩展等。对调用方来说是无感知的、统一化的操作、协议私密,安全性高。
  • RPC更简单内容更小、效率更高、服务化架构、服务化治理、RPC框架是一个强力的支撑。
  • RPC框架基于TCP实现,也可以基于Http2实现。

1.2RPC框架

        主流的RPC框架:

  • Dubbo:国内最早开源的RPC框架,由阿里巴巴公司开发并与2011年对外发布,不过仅支持Java语言。
  • Motan:新浪微博内部使用的RPC框架,与2016年对外开源,仅支持Java语言。
  • Tars:腾讯内部使用的RPC框架,与2017年对外开源,仅支持C++语言。
  • Spring Cloud:国外 Pivotal 公司 2014 年对外开源的 RPC 框架,提供了丰富的生态组件。
  • gRPC:Goolge与2015年对外开源的跨语言rpc框架,2007年贡献给了Apache基金,成为了Apache开源项目之一,支持多种语言。

1.3应用场景

        应用举例:

  • 分布式操作系统的进程间通讯:进程间通讯是操作系统必须提供的基本设施之一,分布式操作系统必须提供分布于异构的结点机上进 程间的通讯机制,RPC是实现消息传送模式的分布式进程间通讯方式之一。
  • 构造分布式设计的软件环境:由于分布式软件设计,服务与环境的分布性, 它的各个组成成份之间存在大量的交互和通讯, RPC是 其基本的实现方法之一。Dubbo分布式服务框架基于RPC实现,Hadoop也采用了RPC方式实现客 户端与服务端的交互。
  • 远程数据库服务:在分布式数据库系统中,数据库一般驻存在服务器上,客户机通过远程数据库服务功能访问数据库 服务器,现有的远程数据库服务是使用RPC模式的。例如,Sybase和Oracle都提供了存储过程机 制,系统与用户定义的存储过程存储在数据库服务器上,用户在客户端使用RPC模式调用存储过 程。
  • 分布式应用程序设计:RPC机制与RPC工具为分布式应用程序设计提供了手段和方便, 用户可以无需知道网络结构和协议细 节而直接使用RPC工具设计分布式应用程序。
  • 分布式程序调试:RPC可用于分布式程序的调试。使用反向RPC使服务器成为客户并向它的客户进程发出RPC,可以 调试分布式程序。例如,在服务器上运行一个远端调试程序,它不断接收客户端的RPC,当遇到一 个调试程序断点时,它向客户机发回一个RPC,通知断点已经到达,这也是RPC用于进程通讯的例 子。

二、深入RPC原理

        2.1设计与调用原理

 

        具体调用过程:

  1. 服务消费者(client客户端)通过本地调用的方式调用服务。
  2. 客户端存根(client stub)接收到请求后负责将方法、入参等信息序列化(组装)成能够进行网络传输的消息体。
  3. 客户端存根(client stub)找到远程的服务地址,并且将消息通过网络发送给服务端。
  4. 服务端存根(server stub)收到消息后进行解码(反序列化操作)。
  5. 服务端存根(server stub)根据解码结果调用本地的服务进行相关处理。
  6. 本地服务执行具体的业务逻辑并将处理结果返回给服务端存根(server stub)。
  7. 服务端存根(server stub)将返回的结果重新打包成消息(序列化),并通过网络发送到消费方。
  8. 客户端存根(client stub)接收到消息,并进行解码(反序列化)。
  9. 服务消费方得到最终的结果。

所涉及到的技术:

  1. 动态代理:生成Client stub(客户端存根)和Server Stub(服务端存根)的时候需要用到java动态代码技术。
  2. 序列化:在网络中,所有的数据都将会转化为字节进行传送,需要对这些参数进行序列化和反序列化操作。目前主流的开源序列化框架有Kryo、fastJson、Hessian、Protobuf等。
  3. NIO通信:Java提供了NIO的解决方案,Java7也提供了更优秀的NIO.2支持。可以采用Netty或者mina框架来解决数据传输的问题。开源的RPC框架Dubbo就是采用NIO通信,集成netty、mina、grizzly。
  4. 服务注册中心:通过注册中心,让客户端连接调用服务端所发布的服务。主流的注册中心组件:redis、Nacos、Zookeeper、Consul、Etcd。Dubbo采用的是ZooKeeper提供服务注册与发现功能。
  5. 负载均衡:在高并发的场景下,需要多个节点或者集群来提升整体的吞吐能力。
  6. 健康检查:健康检查包括,客户端心跳和服务主动探测两种方式。

2.2 RPC深入解析 

2.2.1序列化技术

  •  序列化的作用:

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

  • 序列化原理:

        自定义二进制协议来实现序列化:

         

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

        1.JDK原生序列化,代码如下:

public class JDKSerialization {

    public static void serialize() throws Exception {
        //将序列化后的数据存入到D:/TestCode/tradeUser.clazz中
        String basePath =  "D:/TestCode/";
        FileOutputStream fos = new FileOutputStream(basePath + "tradeUser.clazz");
        //创建tradeUser对象
        TradeUser tradeUser = new TradeUser();
        tradeUser.setName("Mirson");
        //将tradeUser写入到tradeUser.clazz中
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(tradeUser);
        oos.flush();
        oos.close();

        //读取D:/TestCode/tradeUser.clazz
        FileInputStream fis = new FileInputStream(basePath + "tradeUser.clazz");
        ObjectInputStream ois = new ObjectInputStream(fis);
        //将读取的数据反序列化到对象中
        TradeUser deTradeUser = (TradeUser) ois.readObject();
        ois.close();
        System.out.println("=== 反序列化结果 ==== ");
        System.out.println(deTradeUser);
    }

    public static void main(String[] args) throws Exception {
        serialize();
    }
}


@Data
public class TradeUser implements Serializable {

    /**
     * 用户编号
     */
    private String userNo;

    /**
     * 用户名称
     */
    private String name;

    /**
     * 用户密码
     */
    private String userPwd;

    /**
     * 电话号码
     */
    private String phone;

    /**
     * 公司ID
     */
    private Long companyId;

    /**
     * 邮箱
     */
    private String email;

    /**
     * 地址
     */
    private String address;

    /**
     * 最近一次用户登陆IP
     */
    private String lastLoginIp;

    /**
     * 最近一次登陆时间
     */
    private Date lastLoginTime;

    /**
     * 状态(0:有效, 1:锁定, 2:禁用)
     */
    private int status;

    /**
     * 创建时间
     */
    private Date createTime;


}
  1. 在Java中,序列化必须要实现java.io.Serializable接口。
  2. 通过ObjectOutputStream和ObjectInputStream对象进行序列化以及反序列化操作。
  3. 虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的点事两个类的序列化ID是否一致(也就是在代码中定于的序列ID private static final long serialVersionUID);
  4. 序列化并不会保存静态变量。
  5. 想要父类对象也序列化,就需要父类也实现Serializable接口;
  6. Transient关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient变量的值被设为初始值,如基本类型int为0,封装对象型则为null;
  7. 服务器给客户端发哦送你个序列化对象数据并非加密的,如果对象中有一些敏感数据比如密码等,那么在对密码字段序列化之前,最好做加密处理,这样子可以一定程度保证序列化对象的数据安全。

        2.json序列化

        一般在HTTP协议的RPC框架通信中,会选择JSON方式,因为JSON有较好的扩展性、可读性和通用性。

        缺陷:JSON序列化占用空间开销较大,没有JAVA的强类型区分,需要通过反射解决,解析效率和压缩率都较差。

        如果对并发和性能要求较高,或者是传输数据量较大的场景,不建议采用JSON序列化方式。

        3.Hessian2序列化

        Hessian是一个动态类型,二进制序列化,并且支持跨语言特性的序列化框架。Hessian性能上要比JDK、JSON序列化高效很多,并且生成的字节数更小,有非常好的兼容性和稳定性,所以Hessian更加适合作为RPC框架远程通信的序列化协议。

        代码示例:

public class Hessian2Serialization {

    public static void main(String[] args) throws Exception {
        serialize();
    }

    public static void serialize() throws Exception {
        TradeUser tradeUser = new TradeUser();
        tradeUser.setName("Mirson");
        tradeUser.setUserNo("100001");

        //tradeUser对象序列化处理
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        Hessian2Output output = new Hessian2Output(bos);
        output.writeObject(tradeUser);
        output.flushBuffer();
        byte[] data = bos.toByteArray();
        System.out.println("=== 序列化结果 ==== ");
        System.out.println(data);
        bos.close();

        //tradeUser对象反序列化处理
        ByteArrayInputStream bis = new ByteArrayInputStream(data);
        Hessian2Input input = new Hessian2Input(bis);
        TradeUser deTradeUser = (TradeUser) input.readObject();
        input.close();
        System.out.println("=== 反序列化结果 ==== ");
        System.out.println(deTradeUser);

    }
}

        Dobbo Hessian Lite序列化流程:

Dubbo Hessian Lite反序列化流程:

 

        Hessian自身也存在一些缺陷,大家在使用过程中要注意:

  • 对Linked系列对象不支持,比如LinkedHashMap、LinkedHashSet等,但可以通过CollectionSerializer类修复。
  • Local类不支持,可以通过拓展ContextSerizlizerFactory类修复。
  • Byte/Short在反序列化的时候会转成Integer。

Dubbo2.7.3通讯序列化源码实现分析:

        

 

        ExchangeCodec的 encode 方法:
    public void encode(Channel channel, ChannelBuffer buffer, Object msg) throws IOException {
        if (msg instanceof Request) {
            this.encodeRequest(channel, buffer, (Request)msg);
        } else if (msg instanceof Response) {
            this.encodeResponse(channel, buffer, (Response)msg);
        } else {
            super.encode(channel, buffer, msg);
        }

    }

        反序列化流程:

        源码如下:ExchangeCodec的decode方法

    public Object decode(Channel channel, ChannelBuffer buffer) throws IOException {
        int readable = buffer.readableBytes();
        byte[] header = new byte[Math.min(readable, 16)];
        buffer.readBytes(header);
        return this.decode(channel, buffer, readable, header);
    }
    

protected Object decodeBody(Channel channel, InputStream is, byte[] header) throws IOException {
        byte flag = header[2];
        byte proto = (byte)(flag & 31);
        long id = Bytes.bytes2long(header, 4);
        if ((flag & -128) == 0) {
            Response res = new Response(id);
            if ((flag & 32) != 0) {
                res.setEvent(true);
            }

            byte status = header[3];
            res.setStatus(status);

            try {
                ObjectInput in = CodecSupport.deserialize(channel.getUrl(), is, proto);
                if (status == 20) {
                    Object data;
                    if (res.isHeartbeat()) {
                        data = this.decodeHeartbeatData(channel, in);
                    } else if (res.isEvent()) {
                        data = this.decodeEventData(channel, in);
                    } else {
                        data = this.decodeResponseData(channel, in, this.getRequestData(id));
                    }

                    res.setResult(data);
                } else {
                    res.setErrorMessage(in.readUTF());
                }
            } catch (Throwable var12) {
                res.setStatus((byte)90);
                res.setErrorMessage(StringUtils.toString(var12));
            }

            return res;
        } else {
            Request req = new Request(id);
            req.setVersion(Version.getProtocolVersion());
            req.setTwoWay((flag & 64) != 0);
            if ((flag & 32) != 0) {
                req.setEvent(true);
            }

            try {
                ObjectInput in = CodecSupport.deserialize(channel.getUrl(), is, proto);
                Object data;
                if (req.isHeartbeat()) {
                    data = this.decodeHeartbeatData(channel, in);
                } else if (req.isEvent()) {
                    data = this.decodeEventData(channel, in);
                } else {
                    data = this.decodeRequestData(channel, in);
                }

                req.setData(data);
            } catch (Throwable var13) {
                req.setBroken(true);
                req.setData(var13);
            }

            return req;
        }
    }

        4.Protobuf序列化

        Protobuf是Google推出的开源的序列库,它是一种轻便、高效的结构化数据存储格式,可以用于结构化数据序列化,支持Java、Python、C++、Go等多种语言,

        Protobuf使用的时候需要定义IDL(inteface description language),然后使用不同语言的IDL编译器,生成序列化工具类,它具备一下有点:

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

        脚本示例:

// 定义Proto版本
syntax = "proto3";
// 是否允许生成多个JAVA文件
option java_multiple_files = false;
// 生成的包路径
option java_package = "com.itcast.rpc.samples.serial.proto";
// 生成的JAVA类名
option java_outer_classname = "TradeUserProto";


// 预警通知消息体
message TradeUser {

    /**
     * 用户ID
     */
    int64 userId = 1 ;

    /**
     * 用户名称
     */
    string userName = 2 ;
}

        代码操作:

public class ProtoSerialization {

    public static void serialize() throws Exception{

        // 创建TradeUser的Protobuf对象
        TradeUserProto.TradeUser.Builder builder = TradeUserProto.TradeUser.newBuilder();
        builder.setUserId(100001);
        builder.setUserName("Mirson");

        //将TradeUser做序列化处理
        TradeUserProto.TradeUser msg = builder.build();
        byte[] data = msg.toByteArray();
        System.out.println("=== 序列化结果 ==== ");
        System.out.println(data);

        //反序列化处理, 将刚才序列化的byte数组转化为TradeUser对象
        TradeUserProto.TradeUser deTradeUser = TradeUserProto.TradeUser.parseFrom(data);
        System.out.println("=== 反序列化结果 ==== ");
        System.out.println(deTradeUser);

    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值