您当前的位置:首页 > 电脑百科 > 程序开发 > 架构

真的够可以的,基于Netty实现了RPC框架

时间:2021-04-09 10:31:09  来源:掘金  作者:zchanglin

RPC全称Remote Procedure Call,即远程过程调用,对于调用者无感知这是一个远程调用功能。目前流行的开源RPC 框架有阿里的Dubbo、google 的 gRPC、Twitter 的Finagle 等。本次RPC框架的设计主要参考的是阿里的Dubbo,这里Netty 基本上是作为架构的技术底层而存在的,主要完成高性能的网络通信,从而实现高效的远程调用。

Dubbo的架构与Spring

其实在之前的文章中《谈谈京东的服务框架》,探讨过Dubbo的组成和架构。

真的够可以的,基于Netty实现了RPC框架

 


真的够可以的,基于Netty实现了RPC框架

 

另外使用Dubbo最方便的地方在于它可以和Spring非常方便的集成,Dubbo对于配置的优化也是随着Spring一脉相承的,从最早的XML形式到后来的注解方式以及自动装配,都是在不断地简化开发过程来提高开发效率。

Dubbo在Spring框架中的工作流程:

1、Spring的IOC容器启动

2、把服务注册到注册中心(zookeeper软件)中

3、消费者启动时会把它需要用到的服务从注册中心拉取下来

4、提供者的地址发生改变时,注册中心会马上通知消费者

5、根据注册中心中的服务地址直接就可以调用提供者了,如果调用了提供者,就会把提供者的地址主动缓存起来

6、监控消费者调用提供者的次数

RPC实现的关键

1、序列化与反序列化

在远程过程调用时,客户端跟服务端是不同的进程,甚至有时候客户端用JAVA,服务端用C++。这时候就需要客户端把参数先转成一个字节流,传给服务端后,再把字节流转成自己能读取的格式,这个过程叫序列化和反序列化,同理,从服务端返回的值也需要序列化反序列化的过程。在序列化的时候,我们选择Netty自身的对象序列化器。

真的够可以的,基于Netty实现了RPC框架

 

2、数据网络传输

解决了序列化的问题,那么剩下的就是如何把数据参数传到生产者,网络传输层需要把序列化后的参数字节流传给服务端,然后再把序列化后的调用结果传回客户端,虽然大部分RPC框架都采用了TCP作为传输协议,其实UDP也可以作为传输协议的,基于TCP和UDP我们可以自定义任意规则的协议,加之我们要使用NIO通信方式作为高性能网络服务的前提,于是Netty似乎更符合我们Java程序员的口味,Netty真香!

3、告诉注册中心我要调谁

现在调用参数的序列化和网络传输都已经具备,但是还有个问题,那就是消费者要调用谁的问题,一个函数或者方法,我们可以理解为一个服务,这些服务注册在注册中心上面,只有当消费者告诉注册中心要调用谁,才可以进行远程调用。所以不但要把将要调用的服务的参数传过去,也要把要调用的服务信息传过去。

简易RPC框架的架构

真的够可以的,基于Netty实现了RPC框架

 

Dubbo 核心模块主要有四个:Registry 注册中心、Provider 服务提供者、Consumer 服务消费者、Monitor监控,为了方便直接砍掉了监控模块,同时把服务提供者模块与注册中心模块写在一起,通过实现自己的简易IOC容器,完成对服务提供者的实例化。

关于使用Netty进行Socket编程的部分可以参考Netty的官网 或者我之前的博客《Netty编码实战与Channel生命周期》,在这里Netty的编码技巧和方式不作为本文的重点。

RPC框架编码实现

首先需要引入的依赖如下(Netty + Lombok):

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.6.Final</version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.16.8</version>
</dependency>

1、Registry与Provider

目录结构如下:

───src
    └─main
        ├─java
        │  └─edu
        │      └─xpu
        │          └─rpc
        │              ├─api
        │              │      IRpcCalc.java
        │              │      IRpcHello.java
        │              │
        │              ├─core
        │              │      InvokerMessage.java
        │              │
        │              ├─provider
        │              │      RpcCalcProvider.java
        │              │      RpcHelloProvider.java
        │              │
        │              └─registry
        │                      MyRegistryHandler.java
        │                      RpcRegistry.java
        │
        └─resources
───pom.xml

IRpcCalc.java与IRpcHello.java是两个Service接口。IRpcCalc.java内容如下,完成模拟业务加、减、乘、除运算

public interface IRpcCalc {
    // 加
    int add(int a, int b);

    // 减
    int sub(int a, int b);

    // 乘
    int mul(int a, int b);

    // 除
    int div(int a, int b);
}

IRpcHello.java,测试服务是否可用:

public interface IRpcHello {
    String hello(String name);
}

至此API 模块就定义完成了,非常简单的两个接口。接下来,我们要确定传输规则,也就是传输协议,协议内容当然要自定义,才能体现出Netty 的优势。

设计一个InvokerMessage类,里面包含了服务名称、调用方法、参数列表、参数值,这就是我们自定义协议的协议包:

@Data
public class InvokerMessage implements Serializable {
    private String className; // 服务名称
    private String methodName; // 调用哪个方法
    private Class<?>[] params; // 参数列表
    private Object[] values; // 参数值
}

通过定义这样的协议类,就能知道我们需要调用哪个服务,服务中的哪个方法,方法需要传递的参数列表(参数类型+参数值),这些信息正确传递过去了才能拿到正确的调用返回值。

接下来创建这两个服务的具体实现类,IRpcHello的实现类如下:

public class RpcHelloProvider implements IRpcHello {
    public String hello(String name) {
        return "Hello, " + name + "!";
    }
}

IRpcCalc的实现类如下:

public class RpcCalcProvider implements IRpcCalc {
    @Override
    public int add(int a, int b) {
        return a + b;
    }

    @Override
    public int sub(int a, int b) {
        return a - b;
    }

    @Override
    public int mul(int a, int b) {
        return a * b;
    }

    @Override
    public int div(int a, int b) {
        return a / b;
    }
}

Registry 注册中心主要功能就是负责将所有Provider的服务名称和服务引用地址注册到一个容器中(这里为了方便直接使用接口类名作为服务名称,前提是假定我们每个服务只有一个实现类),并对外发布。Registry 应该要启动一个对外的服务,很显然应该作为服务端,并提供一个对外可以访问的端口。先启动一个Netty服务,创建RpcRegistry 类,RpcRegistry.java的具体代码如下:

public class RpcRegistry {
    private final int port;
    public RpcRegistry(int port){
        this.port = port;
    }

    public void start(){
        NioEventLoopGroup bossGroup = new NioEventLoopGroup();
        NioEventLoopGroup workGroup = new NioEventLoopGroup();
        try{
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup, workGroup)
                    .channel(NIOServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            ChannelPipeline pipeline = socketChannel.pipeline();
                            // 处理拆包、粘包的编解码器
                            pipeline.addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4));
                            pipeline.addLast(new LengthFieldPrepender(4));
                            // 处理序列化的编解码器
                            pipeline.addLast("encoder", new ObjectEncoder());
                            pipeline.addLast("decoder", new ObjectDecoder(Integer.MAX_VALUE, ClassResolvers.cacheDisabled(null)));
                            // 自己的业务逻辑
                            pipeline.addLast(new MyRegistryHandler());
                        }
                    })
                    .option(ChannelOption.SO_BACKLOG, 128)
                    .childOption(ChannelOption.SO_KEEPALIVE, true); // 设置长连接

            ChannelFuture channelFuture = serverBootstrap.bind(this.port).sync();
            System.out.println("RPC Registry start listen at " + this.port);
            channelFuture.channel().closeFuture().sync();
        } catch (Exception e){
            e.printStackTrace();
        } finally {
            bossGroup.shutdownGracefully();
            workGroup.shutdownGracefully();
        }
    }
    public static void main(String[] args) {
        new RpcRegistry(8080).start();
    }
}

接下来只需要实现我们自己的Handler即可,创建MyRegistryHandler.java,内容如下:

public class MyRegistryHandler extends ChannelInboundHandlerAdapter {
    // 在注册中心注册服务需要有容器存放
    public static ConcurrentHashMap<String, Object> registryMap = new ConcurrentHashMap<>();

    // 类名的缓存位置
    private static final List<String> classCache = new ArrayList<>();

    // 约定,只要是写在provider下所有的类都认为是一个可以对完提供服务的实现类
    // edu.xpu.rpc.provider

    public MyRegistryHandler(){
        scanClass("edu.xpu.rpc.provider");
        doRegister();
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        Object result = new Object();
        // 客户端传过来的调用信息
        InvokerMessage request = (InvokerMessage)msg;
        // 先判断有没有这个服务
        String serverClassName = request.getClassName();
        if(registryMap.containsKey(serverClassName)){
            // 获取服务对象
            Object clazz = registryMap.get(serverClassName);
            Method method = clazz.getClass().getMethod(request.getMethodName(), request.getParams());
            result = method.invoke(clazz, request.getValues());
            System.out.println("request=" + request);
            System.out.println("result=" + result);
        }
        ctx.writeAndFlush(result);
        ctx.close();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }


    // 实现简易IOC容器
    // 扫描出包里面所有的Class
    private void scanClass(String packageName){
        ClassLoader classLoader = this.getClass().getClassLoader();
        URL url = classLoader.getResource(packageName.replaceAll("\.", "/"));
        File dir = new File(url.getFile());
        File[] files = dir.listFiles();
        for (File file: files){
            if(file.isDirectory()){
                scanClass(packageName + "." + file.getName());
            }else{
                // 拿出类名
                String className = packageName + "." + file.getName().replace(".class", "").trim();
                classCache.add(className);
            }
        }
    }

    // 把扫描到的Class实例化,放到Map中
    // 注册的服务名称就叫做接口的名字 [约定优于配置]
    private void doRegister(){
        if(classCache.size() == 0) return;
        for (String className: classCache){
            try {
                Class<?> clazz = Class.forName(className);
                // 服务名称
                Class<?> anInterface = clazz.getInterfaces()[0];
                registryMap.put(anInterface.getName(), clazz.newInstance());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

在这里还通过反射实现了简易的IOC容器,先递归扫描provider包底下的类,把这些类的对象作为服务对象放到IOC容器中进行管理,由于IOC是一个Map实现的,所以将类名作为服务名称,也就是Key,服务对象作为Value。根据消费者传过来的服务名称,就可以找到对应的服务,到此,Registry和Provider已经全部写完了。

2、consumer

目录结构如下:

└─src
    ├─main
    │  ├─java
    │  │  └─edu
    │  │      └─xpu
    │  │          └─rpc
    │  │              ├─api
    │  │              │      IRpcCalc.java
    │  │              │      IRpcHello.java
    │  │              │
    │  │              ├─consumer
    │  │              │  │  RpcConsumer.java
    │  │              │  │
    │  │              │  └─proxy
    │  │              │          RpcProxy.java
    │  │              │          RpcProxyHandler.java
    │  │              │
    │  │              └─core
    │  │                      InvokerMessage.java
    │  │
    │  └─resources
    └─test
        └─java
└─ pom.xml

在看客户端的实现之前,先梳理一下RPC流程。API 模块中的接口只在服务端实现了。因此,客户端调用API 中定义的某一个接口方法时,实际上是要发起一次网络请求去调用服务端的某一个服务。而这个网络请求首先被注册中心接收,由注册中心先确定需要调用的服务的位置,再将请求转发至真实的服务实现,最终调用服务端代码,将返回值通过网络传输给客户端。整个过程对于客户端而言是完全无感知的,就像调用本地方法一样,所以必定要对客户端的API接口做代理,隐藏网络请求的细节。

真的够可以的,基于Netty实现了RPC框架

 

由上图的流程图可知,要让用户调用无感知,必须创建出代理类来完成网络请求的操作。

RpcProxy.java如下:

public class RpcProxy {
    public static <T> T create(Class<?> clazz) {
        //clazz传进来本身就是interface
        MethodProxy proxy = new MethodProxy(clazz);
        T result = (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz} , proxy);
        return result;
    }

    private static class MethodProxy implements InvocationHandler {
        private Class<?> clazz;

        public MethodProxy(Class<?> clazz) {
            this.clazz = clazz;
        }
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            // 如果传进来是一个已实现的具体类
            if (Object.class.equals(method.getDeclaringClass())) {
                try {
                    return method.invoke(this, args);
                } catch (Throwable t) {
                    t.printStackTrace();
                }
                // 如果传进来的是一个接口(核心)
            } else {
                return rpcInvoke(method, args);
            }
            return null;
        }

        // 实现接口的核心方法
        public Object rpcInvoke(Method method, Object[] args) {
            // 传输协议封装
            InvokerMessage invokerMessage = new InvokerMessage();
            invokerMessage.setClassName(this.clazz.getName());
            invokerMessage.setMethodName(method.getName());
            invokerMessage.setValues(args);
            invokerMessage.setParams(method.getParameterTypes());

            final RpcProxyHandler consumerHandler = new RpcProxyHandler();
            EventLoopGroup group = new NioEventLoopGroup();

            try {
                Bootstrap bootstrap = new Bootstrap();
                bootstrap.group(group)
                        .channel(NioSocketChannel.class)
                        .option(ChannelOption.TCP_NODELAY, true)
                        .handler(new ChannelInitializer<SocketChannel>() {
                            @Override
                            public void initChannel(SocketChannel ch) throws Exception {
                                ChannelPipeline pipeline = ch.pipeline();
                                pipeline.addLast("frameDecoder", new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4));
                                //自定义协议编码器
                                pipeline.addLast("frameEncoder", new LengthFieldPrepender(4));
                                //对象参数类型编码器
                                pipeline.addLast("encoder", new ObjectEncoder());
                                //对象参数类型解码器
                                pipeline.addLast("decoder", new ObjectDecoder(Integer.MAX_VALUE, ClassResolvers.cacheDisabled(null)));
                                pipeline.addLast("handler", consumerHandler);
                            }
                        });
                ChannelFuture future = bootstrap.connect("localhost", 8080).sync();
                future.channel().writeAndFlush(invokerMessage).sync();
                future.channel().closeFuture().sync();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                group.shutdownGracefully();
            }
            return consumerHandler.getResponse();
        }
    }
}

我们通过传进来的接口对象,获得了要调用的服务名,服务方法名,参数类型列表,参数列表,这样就把自定义的RPC协议包封装好了,只需要把协议包发出去等待结果返回即可,所以为了接收返回值数据还需要自定义一个接收用的Handler,RpcProxyHandlerdiamante如下:

public class RpcProxyHandler extends ChannelInboundHandlerAdapter {
    private Object result;

    public Object getResponse() {
        return result;
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        result = msg;
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        System.out.println("client exception is general");
    }
}

这样就算是完成了整个流程,下面开始测试一下吧,测试的RpcConsumer.java代码如下:

public class RpcConsumer {
    public static void main(String[] args) {
        // 本机之间的正常调用
        // IRpcHello iRpcHello = new RpcHelloProvider();
        // iRpcHello.hello("Tom");

        // 肯定是用动态代理来实现的
        // 传给它接口,返回一个接口的实例,伪代理
        IRpcHello rpcHello = RpcProxy.create(IRpcHello.class);
        System.out.println(rpcHello.hello("ZouChangLin"));

        int a = 10;
        int b = 5;
        IRpcCalc iRpcCalc = RpcProxy.create(IRpcCalc.class);

        System.out.println(String.format("%d + %d = %d", a, b, iRpcCalc.add(a, b)));
        System.out.println(String.format("%d - %d = %d ", a, b, iRpcCalc.sub(a, b)));
        System.out.println(String.format("%d * %d = %d", a, b, iRpcCalc.mul(a, b)));
        System.out.println(String.format("%d / %d = %d", a, b, iRpcCalc.div(a, b)));
    }
}

3、效果测试

先开启Registry,运行端口是8080:

真的够可以的,基于Netty实现了RPC框架

 

开启consumer开始调用

真的够可以的,基于Netty实现了RPC框架

 

调用完成后可以看到调用结果正确,并且在Registry这边也看到了日志:

真的够可以的,基于Netty实现了RPC框架

 

可以发现,简易RPC框架顺利完工!

作者:zchanglin
链接:
https://juejin.cn/post/6948351262668636174
来源:掘金



Tags:RPC框架   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
一、概述手动实现一款轻量,高效的RPC框架,基于TCP的二进制协议实现github源码: https://github.com/wosn00/srpc二、特征 基于netty的主从Reactor模型,NIO通信 支持同步,异步,携带...【详细内容】
2021-10-26  Tags: RPC框架  点击:(38)  评论:(0)  加入收藏
RPC全称Remote Procedure Call,即远程过程调用,对于调用者无感知这是一个远程调用功能。目前流行的开源RPC 框架有阿里的Dubbo、Google 的 gRPC、Twitter 的Finagle 等。本次R...【详细内容】
2021-04-09  Tags: RPC框架  点击:(332)  评论:(0)  加入收藏
主流RPC框架通讯协议实现原理与源码解析。互联网+的大环境下,用户量、数据量的急剧增长,使得单机系统不能承载更多的数据处理能力,从而催生了分布式技术的快速发展。分布式RPC...【详细内容】
2020-08-10  Tags: RPC框架  点击:(80)  评论:(0)  加入收藏
RPC(Remote Procedure Call Protocol)远程过程调用协议。一个通俗的描述是:客户端在不知道调用细节的情况下,调用存在于远程计算机上的某个对象,就像调用本地应用程序中的对象一...【详细内容】
2020-03-16  Tags: RPC框架  点击:(68)  评论:(0)  加入收藏
说起RPC,就不能不提到分布式,这个促使RPC诞生的领域。假设你有一个计算器接口,Calculator,以及它的实现类CalculatorImpl,那么在系统还是单体应用时,你要调用Calculator的add方法...【详细内容】
2019-11-12  Tags: RPC框架  点击:(150)  评论:(0)  加入收藏
一类是跟某种特定语言平台绑定的,另一类是与语言无关即跨语言平台的。跟语言平台绑定的开源 RPC 框架主要有下面几种。 Dubbo:国内最早开源的 RPC 框架,由阿里巴巴公司开发并...【详细内容】
2019-11-06  Tags: RPC框架  点击:(64)  评论:(0)  加入收藏
一、前 言RPC 是远程过程调用(Remote Procedure Call)的缩写,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。举例来说,部署在 A 节点上的应用调...【详细内容】
2019-08-27  Tags: RPC框架  点击:(205)  评论:(0)  加入收藏
开源 RPC 框架有哪些呢?一类是跟某种特定语言平台绑定的,另一类是与语言无关即跨语言平台的。跟语言平台绑定的开源 RPC 框架主要有下面几种。 Dubbo:国内最早开源的 RPC 框架,...【详细内容】
2019-08-21  Tags: RPC框架  点击:(257)  评论:(0)  加入收藏
RPC(Remote Procedure Call):远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的思想。 RPC 是一种技术思想而非一种规范或协议,常见 RPC...【详细内容】
2019-06-17  Tags: RPC框架  点击:(369)  评论:(0)  加入收藏
▌简易百科推荐
为了构建高并发、高可用的系统架构,压测、容量预估必不可少,在发现系统瓶颈后,需要有针对性地扩容、优化。结合楼主的经验和知识,本文做一个简单的总结,欢迎探讨。1、QPS保障目标...【详细内容】
2021-12-27  大数据架构师    Tags:架构   点击:(3)  评论:(0)  加入收藏
前言 单片机开发中,我们往往首先接触裸机系统,然后到RTOS,那么它们的软件架构是什么?这是我们开发人员必须认真考虑的问题。在实际项目中,首先选择软件架构是非常重要的,接下来我...【详细内容】
2021-12-23  正点原子原子哥    Tags:架构   点击:(7)  评论:(0)  加入收藏
现有数据架构难以支撑现代化应用的实现。 随着云计算产业的快速崛起,带动着各行各业开始自己的基于云的业务创新和信息架构现代化,云计算的可靠性、灵活性、按需计费的高性价...【详细内容】
2021-12-22    CSDN  Tags:数据架构   点击:(10)  评论:(0)  加入收藏
▶ 企业级项目结构封装释义 如果你刚毕业,作为Java新手程序员进入一家企业,拿到代码之后,你有什么感觉呢?如果你没有听过多模块、分布式这类的概念,那么多半会傻眼。为什么一个项...【详细内容】
2021-12-20  蜗牛学苑    Tags:微服务   点击:(8)  评论:(0)  加入收藏
我是一名程序员关注我们吧,我们会多多分享技术和资源。进来的朋友,可以多了解下青锋的产品,已开源多个产品的架构版本。Thymeleaf版(开源)1、采用技术: springboot、layui、Thymel...【详细内容】
2021-12-14  青锋爱编程    Tags:后台架构   点击:(20)  评论:(0)  加入收藏
在了解连接池之前,我们需要对长、短链接建立初步认识。我们都知道,网络通信大部分都是基于TCP/IP协议,数据传输之前,双方通过“三次握手”建立连接,当数据传输完成之后,又通过“四次挥手”释放连接,以下是“三次握手”与“四...【详细内容】
2021-12-14  架构即人生    Tags:连接池   点击:(16)  评论:(0)  加入收藏
随着移动互联网技术的快速发展,在新业务、新领域、新场景的驱动下,基于传统大型机的服务部署方式,不仅难以适应快速增长的业务需求,而且持续耗费高昂的成本,从而使得各大生产厂商...【详细内容】
2021-12-08  架构驿站    Tags:分布式系统   点击:(23)  评论:(0)  加入收藏
本系列为 Netty 学习笔记,本篇介绍总结Java NIO 网络编程。Netty 作为一个异步的、事件驱动的网络应用程序框架,也是基于NIO的客户、服务器端的编程框架。其对 Java NIO 底层...【详细内容】
2021-12-07  大数据架构师    Tags:Netty   点击:(16)  评论:(0)  加入收藏
前面谈过很多关于数字化转型,云原生,微服务方面的文章。虽然自己一直做大集团的SOA集成平台咨询规划和建设项目,但是当前传统企业数字化转型,国产化和自主可控,云原生,微服务是不...【详细内容】
2021-12-06  人月聊IT    Tags:架构   点击:(23)  评论:(0)  加入收藏
微服务看似是完美的解决方案。从理论上来说,微服务提高了开发速度,而且还可以单独扩展应用的某个部分。但实际上,微服务带有一定的隐形成本。我认为,没有亲自动手构建微服务的经历,就无法真正了解其复杂性。...【详细内容】
2021-11-26  GreekDataGuy  CSDN  Tags:单体应用   点击:(35)  评论:(0)  加入收藏
最新更新
栏目热门
栏目头条