RPC框架介绍以及手动实现RPC

4 篇文章 0 订阅
1 篇文章 0 订阅

前言:

首先提出一个问题:为什么需要使用RPC,而不是简单的http接口?

http接口在接口不多、系统与系统交互较少的情况下,解决信息孤岛初期常使用的一种通信手段,优点就是简单、直接、开发方便。但是如果是一个大型的系统,内部子系统较多、接口非常多的情况下,RPC框架的好处就显现出来了。如下:

  • 首先是长链接。不必每次通信都要像http一样去进行3次握手和4次挥手,减少了网络开销。

  • 其次就是RPC框架一般都有注册中心,有丰富的监控管理。发布、下线接口、动态扩展等,对调用方来说是无感知、统一化的操作。

  • 再次来说就是安全性。服务架构、服务化治理,RPC框架是一个强力的支撑。

分布式架构作为大型系统架构,高性能分布式调用离不开RPC框架,我们常用的有Dubbo、gRPC、RestTemplate、webservice等。RPC框架看上去高深莫测,其实就三个核心点:

  1. 动态代理技术,像调用本地服务一样调用远程服务,需要对使用者透明
  2. 网络通信框架,用以开发高性能、高可靠的网路服务器和客户端程序。例如BIO(ServerSocket),NIO(ServerSocketChannel/selector)
  3. 序列化,也就是数据包传输格式,将数据转为可传输对象,如Dubbo、json、xml、protobuf、avro、hessian

今天就先说下接口动态代理调用以及网络通信框架,然后再实现一个RPC框架实例。

一 接口调用动态代理

基于JDK的动态代理是基于接口的动态代理。在JDK动态代理机制中,有两个重要的类和接口,一个是InvocationHandler、另一个则是Proxy类,这个类和接口是实现动态代理必须用到的。InvocationHandler接口是给动态代理类实现的,负责处理被代理对象的操作。而Proxy则用来创建动态代理类实例对象,因为只有得到了这个对象我们才能调用那些需要代理的方法。

下面直接上代码

1、InvocationHandler接口实现类

在这里插入图片描述
当接口代理类调用接口方法时,实际上就是调用InterfaceInvokeHandler.invoke方法。所以在这个方法中就可写自己想做的一些事情,比如建立远程连接、发起远程调用等。

2、Proxy创建接口代理对象

使用工厂模式,传入要代理的 接口Class对象,就会创建出接口代理对象。
在这里插入图片描述
大家能看到代理实际处理类以及方法使我们上面定义的InterfaceInvokeHandler.invoke()。

3、 测试接口代理调用

接口类:
在这里插入图片描述
测试代码

在这里插入图片描述 用接口代理工程类产品接口代理对象,再调用接口方法。运行后从输出接口看出我们已经代理了这个接口的调用。

在这里插入图片描述

二、网络通信框架Netty

netty是jboss提供的一个java开源框架,netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可用性的网络服务器和客户端程序。也就是说netty是一个基于nio的编程框架,使用netty可以快速的开发出一个网络应用。

相比JDK原生NIO,Netty提供了相对简单易用的API,非常适合网络编程。Netty是完全基于NIO实现的,所以Netty是异步的。无疑是NIO的老大,它的健壮性、功能、性能、可定制性和可扩展性在同类框架都是首屈一指的。它已经得到成百上千的商业以及商用项目的验证,如Hadoop的RPC框架Avro、RocketMQ以及主流的分布式调用框架Dubbo等。

1、JDK 的 NIO 类库缺点

  • NIO的类库和API繁杂,使用麻烦,你需要熟练掌握Selector、
    ServerSocketChannel、SocketChannel、ByteBuffer等。
  • 需要具备其他的额外技能做铺垫,例如熟悉Java多线程编程。这是因为
    NIO编程涉及到Reactor模式,你必须对多线程和网路编程非常熟悉,才能 编写出高质量的NIO程序。
  • 可靠性靠能力补齐,工作量和难度都非常大。例如客户端面临断连重连、网 络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等问题,NIO
    编程的特点是功能开发相对容易,但是可靠性能力补齐的工作量和难度都 非常大。
  • JDK NIO的BUG,例如臭名昭著的epoll bug,它会导致Selector空轮询, 最终导致CPU100%。官方声称在JDK1.6版本的update18修复了该问题,但
    是直到JDK1.7版本该问题仍旧存在,只不过该BUG发生概率降低了一些而 已,它并没有被根本解决。

2、Netty的优点

  • API使用简单,开发门槛低;
  • 功能强大,预置了多种编解码功能,支持多种主流协议;
  • 定制能力强,可以通过ChannelHandler对通信框架进行灵活地扩展;
  • 性能高,通过与其他业界主流的NIO框架对比,Netty的综合性能最优; 成熟、稳定,Netty修复了已经发现的所有JDK NIO BUG,业务开发人员不需要再为NIO的BUG而烦恼;
  • 社区活跃,版本迭代周期短,发现的BUG可以被及时修复,同时,更多的新功能会加入;
  • 经历了大规模的商业应用考验,质量得到验证。在互联网、大数据、网络游戏、企业应用、电信软件等众多行业得到成功商用,证明了它已经完全能够满足不同行业的商业应用了。

3、netty的核心组件

  • ServerBootstrap是服务器端的辅助启动类,Bootstrap是客户端的辅助启动类。一个Netty应用通常由一个Bootstrap开始,主要作用是配置整个Netty程序,串联各个组件,Netty中Bootstrap类是客户端程序的启动引导类,ServerBootstrap是服务端启动引导类,启动Netty程序,配置连接参数等作用。
  • eventLoop:eventLoop维护了一个线程和任务队列,支持异步提交执行任务。

  • eventLoopGroup:eventLoopGroup
    主要是管理eventLoop的生命周期,可以将其看作是一个线程池,其内部维护了一组eventLoop,每个eventLoop对应处理多个Channel,而一个Channel只能对应一个eventLoop。

  • channelPipeLine:是一个包含channelHandler的list,用来设置channelHandler的执行顺序。
  • Channel:Channel代表一个实体(如一个硬件设备、一个文件、一个网络套接字或者一个能够执行一个或者多个不同的IO操作的程序组件)的开放链接,如读操作和写操作。

  • Futrue、ChannelFuture:Future提供了另一种在操作完成时通知应用程序的方式。这个对象可以看作是一个异步操作结果的占位符;它将在未来的某个时刻完成,并提供对其结果的访问。netty的每一个出站操作都会返回一个ChannelFuture。future上面可以注册一个监听器,当对应的事件发生后会出发该监听器。

  • ChannelInitializer:它是一个特殊的ChannelInboundHandler,当channel注册到eventLoop上面时,对channel进行初始化

  • ChannelHandler:用来处理业务逻辑的代码,ChannelHandler是一个父接口,ChannelnboundHandler和ChannelOutboundHandler都继承了该接口,它们分别用来处理入站和出站。

  • ChannelHandlerContext:允许与其关联的ChannelHandler与它相关联的ChannlePipeline和其它ChannelHandler来进行交互。它可以通知相同ChannelPipeline中的下一个ChannelHandler,也可以对其所属的ChannelPipeline进行动态修改。

三、序列化报文

当通过Netty发送或者接收一个消息的时候,就将会发生一次数据转换。入站消息会被解码:从字节转换为另一种格式,通常是一个Java对象。如果是出站消息,则会发生编码:将从它的当前格式被编码为字节。

Netty默认已经提供了一堆的编/解码器,一般需求已经可以满足,如果不满足可以通过集成ByteToMessageDecoder或MessageToByteEncoder来实现自己的编/解码器。通过查看类的继承结构可以看出Netty提供的编/解码器适配器类都实现了ChannelOutboundHandler或者ChannellnboundHandler接口。

对于解码器Handler而言,其重写了channelRead方法,对于每个从入站Channel读取的消息,这个方法都将会被调用。随后,它将调用由解码器所提取的decode()方法,并将已经解码的字节转发给ChannelPipeline中的下一个ChannellnboundHandler。

OutboundHandler采用相反的处理方式。

四、实现RPC框架

1、整体工程目录如下:

在这里插入图片描述
整体结构说明:

  • rpc-demo-api:定义一个服务接口。用于服务消费者和提供者之间的服务接口调用约定。
  • rpc-demo-server:服务接口具体实现,并创建一个Rpc服务端,监听消费者的Rpc调用请求,并返回数据。
  • rpc-demo-client:创建一个Rpc客户端,调动Rpc服务接口,通过代理调用完成远程Rpc调用。

API工程定义一个服务接口:
在这里插入图片描述

服务端工程实现这个接口:

在这里插入图片描述

2、RpcServer实现

启动Netty Server代码:

在这里插入图片描述
上面的代码中添加了String类型的编码解码器,也就是发送数据时编码成字节码,接收数据时解码成字符串。

自定义NettyServerHandler如下:

**
 * @Author 18224
 * @Date 2020/11/23 23:13
 * @Version 1.0
 */
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
    /**
     * 类路径以及名称
     */
    private static final String CLASSNAME = "interface rpc.demo.server.api.public interface MyServiceTest";
    /**
     * 方法名
     */
    private static final String SEND_MSG = "sendMsg";

    /**
     * 方法名
     */
    private static final String HELLO_WORD = "helloWorld";


    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        String str = msg.toString();
        String[] strArr = str.split("#");
        String interfaceClass = strArr[0];
        String method = strArr[1];
        String params = strArr[2];
        System.out.println("服务端接收到的接口:" + interfaceClass);
        System.out.println("服务端接收到的方法名:" + method);
        System.out.println("服务端接收到的参数:" + params);

        //如果符合约定,调用本地方法返回数据
        if (CLASSNAME.equals(interfaceClass)) {
            MyService myService = new MyServiceImpl();
            if (SEND_MSG.equals(method)) {
                ctx.writeAndFlush(myService.sendMsg(params));
            }
        }


    }
}

因解码器是StringDecoder,所以收到数据对象类型可以转为String,对String报文进行解析,得到要调用的接口、方法、以及入参。然后调用服务端相应的接口实现类方法,结果返回给客户端。

下面再写一个服务启动类:

在这里插入图片描述
现在服务端的代码就写完了,总要步骤是,创建一个Netty服务端,实现一个自定义的handler,自定义handler对收到的报文按协议解码,这里是对字符串拆解得到要调用的接口、方法以及入参(算是一种协议),按照需要调用的接口创建一个接口的实现类,并调用他的方法返回字符串给调用端。

3、RpcClient端实现

RPC客户端有个需要注意的地方,就是对接口的透明调用,也就是框架使用者不用关心底层的网络调用实现。这里我们可以使用上面所讲的JDK动态代理来实现这个目的。

用JDK动态代理来创建接口代理队形,在代理处理类中我们就可以初始化Netty客户端,向服务端按照协议发送请求数据,并返回数据。

(1)自定义客户端

public class NettyClientHandler extends ChannelInboundHandlerAdapter implements Callable {

    private String para;

    /**
     * 保存 Channel 相关的所有上下文信息,同时关联一个 ChannelHandler 对象
     */
    private ChannelHandlerContext context;

    private String result;


    public void setPara(String para) {
        this.para = para;
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        context = ctx;
    }

    /**
     * 收到服务端的数据,唤醒等待线程
     *
     * @param ctx
     * @param msg
     * @throws Exception
     */
    @Override
    public synchronized void channelRead(ChannelHandlerContext ctx, Object msg) {
        result = msg.toString();
        notify();

    }

    /**
     * 写出数据开始等待唤醒
     *
     * @return
     * @throws Exception
     */
    @Override
    public synchronized Object call() throws Exception {
        context.writeAndFlush(para);
        wait();
        return result;
    }



}

(2) 接口代理类

/**
 * @Author 18224
 * @Date 2020/11/23 23:54
 * @Version 1.0
 */
public class MyInvokeHandler implements InvocationHandler {

    /**
     * 执行线程池
     */
    private static ThreadPoolExecutor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(),
            Runtime.getRuntime().availableProcessors(), Runtime.getRuntime().availableProcessors(),
            TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(3),
            new ThreadPoolExecutor.DiscardOldestPolicy());
    private static NettyClientHandler client;

    /**
     * 执行方法
     * @param proxy
     * @param method
     * @param args
     * @return
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("客户端接收到的接口:" + method.getDeclaringClass());
        System.out.println("客户端端接收到的方法名:" + method.getName());
        System.out.println("客户端接收到的参数:" + Arrays.asList(args));
        if (client == null) {
            initClient();
        }

        StringBuilder sb = new StringBuilder(method.getDeclaringClass()
                + "#" + method.getName() + "#" + Arrays.asList(args));

        client.setPara(sb.toString());
        return THREAD_POOL_EXECUTOR.submit(client).get();
    }

    /**
     * 初始化netty客户端
     */
    private static void initClient() {
        client = new NettyClientHandler();
        Bootstrap bootstrap = new Bootstrap();
        NioEventLoopGroup enentLoopGroup = new NioEventLoopGroup();
        bootstrap.group(enentLoopGroup)
                .channel(NioSocketChannel.class)
                .option(ChannelOption.TCP_NODELAY, true)
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        ChannelPipeline p = socketChannel.pipeline();
                        p.addLast(new StringDecoder(CharsetUtil.UTF_8));
                        p.addLast(new StringEncoder(CharsetUtil.UTF_8));
                        p.addLast(client);
                    }
                });
        try {
            bootstrap.connect("localhost", 8088).sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }


    }

}

接口方法代理调用了invoke()方法,在该方法内初始化Netty客户端,并按照协议组装请求报文题,然后向服务端按照协议发送请求数据,并返回服务端数据。

(3)接口工厂类

用工厂模式创建接口代理类

在这里插入图片描述(4)RPC接口调用
通过接口代理类,像调用本地方法一样调用远程方法。
在这里插入图片描述

4、运行演示

服务端显示被调用的日志信息:
在这里插入图片描述客户端显示发起调用以及返回结果的日志信息:

在这里插入图片描述

五、主流RPC框架对比

1、主要RPC框架简介

Dubbo:阿里巴巴出品的国内较为开源的服务治理的Java rpc框架,在国内得到了广泛的应用,并且dubbo3.0也开始发展。

gRPC:grpc是一个多语言、高性能、开源的通用远程过程调用(RPC)框架。 来自于Google的开源项目,2016年8月19日发布了成熟的1.0.0版本 基于HTTP/2等技术。grpc支持Java, C,C++, Python等多种常用的编程语言,并且客户端和服务端可以采用不同的编程语言来实现。另外数据序列化使用Protocol Buffers
Motan:微博出品的内部使用的rpc框架,底层支持Java,生态圈往service mesh发展以支持多语言。

Thrift:是由Facebook为“大规模跨语言服务开发”而开发的。它通过一个代码生成引擎联合了一个软件栈,来创建不同程度的、无缝的跨平台高效服务,可以使用C#、C++(基于POSIX兼容系统)、Cappuccino、Cocoa、Delphi、Erlang、Go、Haskell、Java、Node.js、OCaml、Perl、PHP、Python、Ruby和Smalltalk。

2、rpc框架异同点对比

RPC框架分为两类,一类是服务治理类,这类框架能够提供包括服务注册、管理在内的整套的服务技术架构支持,典型的包括Dubbo、Dubbox、Montan. 另外一类RPC框架是无法提供服务治理功能,更多的是关注跨语言服务调用,典型代表是gRPC、Thrift
在这里插入图片描述

六、RPC总结

RPC(Remote Procedure Call)一种进程间通信方式,允许向调用本地服务一样调用远程服务。

RPC框架的主要目标就是让远程服务调用更简单、透明。RPC框架负责屏蔽底层的传输方式(TCP & UDP)、序列化方式(XML/JSON/二进制)和通信细节。开发人员在使用的时候只需要了解谁在什么位置提供什么样的远程服务接口即可,并不关心底层通信细节和调用过程。

调动图解如下:
在这里插入图片描述

最后附彩蛋:点击即可跳转到源码地址

评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值