使用Netty实现一套分布式RPC服务

写在前面

Netty作为一个异步事件驱动的网络应用框架,可以用于快速开发可维护的高性能服务器和客户端。国内著名的RPC框架Dubbo底层使用的是Netty作为网络通信的。本篇文章我们来探索一下RPC框架的本质以及使用Netty来实现一个简单地RPC框架。

什么是RPC

概述

RPC(Remote Procedure Call)— 远程过程调用,是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外地为这个交互作用编程

两个或多个应用程序都分布在不同的服务器上,它们之间的调用都像是本地方法调用一样(如图)

在这里插入图片描述

RPC与Socket的关系

通过上面的描述,好像RPC与Socket非常像,都是调用远程的方法,都是client/server模式。但是值得注意的是,RPC并不等同于Socket。Socket是RPC经常采用的通信手段之一,RPC是在Socket的基础上实现的,它比socket需要更多的网络和系统资源。除了Socket,RPC还有其他的通信方法,比如:http、操作系统自带的管道等技术来实现对于远程程序的调用。微软的Windows系统中,RPC就是采用命名管道进行通信。需要了解Socket相关概念的,可以参考之前的这篇文章golang socket编程。

调用方法的三种姿势

本地方法调用

本地方法调用使我们开发中最常见的,如下定义一个方法:

public String sayHello(String name) {
    return "hello, " + name;
}

只需要传入一个参数,调用sayHello方法就可以得到一个输出,入参、出参以及方法体都在同一个进程空间中,这就是本地方法调用

Socket通信

那有没有办法实现不同进程之间通信呢?调用方在进程A,需要调用方法B,但是方法B在进程B中。

在这里插入图片描述

最容易想到的实现方式

就是使用Socket通信,使用Socket可以完成跨进程调用,我们需要约定一个进程通信协议,来进行传参,调用函数,出参。写过Socket应该都知道,Socket是比较原始的方式,我们需要更多的去关注一些细节问题,比如参数和函数需要转换成字节流进行网络传输,也就是序列化操作,然后出参时需要反序列化。

假如RPC就是让我们在客户端直接使用Socket远程调用,那无疑是个灾难。所以有没有什么简单方法,让我们的调用方不需要关注细节问题,让调用方像调用本地函数一样,只要传入参数,调用方法,然后坐等返回结果就可以了呢?而这个诉求的解决方案就是RPC框架——为使用方屏蔽底层网络通信的细节。

RPC框架

RPC框架就是用来解决上面的问题的,它能够让调用方像调用本地函数一样调用远程服务底层通讯细节对调用方是透明的,将各种复杂性都给屏蔽掉,给予调用方极致体验。

在这里插入图片描述

当server需要对方法内实现修改时,client完全感知不到,不用做任何变更。这种方式在跨部门,跨公司合作的时候是非常方便的。

常见的RPC框架

常见的 RPC 框架有: 比较知名的如阿里的Dubbo、google的gRPC、Go语言的rpcx、Apache的thrift, Spring 旗下的 Spring Cloud。

在这里插入图片描述

RPC基本调用流程图

在这里插入图片描述

RPC调用流程说明

  1. 服务消费方(client)以本地调用方式调用服务
  2. client stub 接收到调用后负责将方法、参数等封装成能够进行网络传输的消息体
  3. client stub 将消息进行编码并发送到服务端
  4. server stub 收到消息后进行解码
  5. server stub 根据解码结果调用本地的服务
  6. 本地服务执行并将结果返回给 server stub
  7. server stub 将返回导入结果进行编码并发送至消费方
  8. client stub 接收到消息并进行解码
  9. 服务消费方(client)得到结果

小结:RPC 的目标就是将 2-8 这些步骤都封装起来,用户无需关心这些细节,可以像调用本地方法一样即可完成远程服务调用。

RPC调用过程存中需要关注的技术细节

前面就已经说到RPC框架,可以让调用像调用本地函数一样调用远程服务。原理就是RPC框架屏蔽Socket通信的相关细节,使调用方可以向调用本地方法一样调用远程方法。

在使用的时候,调用方是直接调用本地函数,传入相应参数,其他细节它不用管,至于通讯细节交给RPC框架来实现。实际上RPC框架采用代理类的方式,具体来说是动态代理的方式,在运行时动态创建新的类,也就是代理类,在该类中实现通讯的细节问题,比如与服务端的连接、参数序列化、结果反序列化等。

除了上述动态代理,还需要约定一个双方通信的协议格式,规定好协议格式,比如请求方法的类名、请求的方法名、请求参数的数据类型,请求的参数等,这样根据格式进行序列化后进行网络传输,然后服务端收到请求对象后按照指定格式进行解码,这样服务端才知道具体该调用哪个方法,传入什么样的参数。

刚才又提到网络传输,RPC框架重要的一环也就是网络传输,服务是部署在不同主机上的,如何高效的进行网络传输,尽量不丢包,保证数据完整无误的快速传递出去?实际上,就是利用我们今天的主角——Netty,Netty是一个高性能的网络通讯框架,它足以胜任我们的任务。

前面说了这么多,再次总结下一个RPC框架需要重点关注哪几个点:

  • 动态代理
  • 通信协议
  • 序列化
  • 网络传输

当然一个优秀的RPC框架需要关注的不止上面几点,只不过本篇文章旨在做一个简易的RPC框架,理解了上面关键的几点就够了

在这里插入图片描述

实操自己实现一个简单版的RPC

需求说明

dubbo 底层使用了 Netty 作为网络通讯框架,要求用 Netty 实现一个简单的 RPC 框架 模仿 dubbo,消费者和提供者约定接口和协议,消费者远程调用提供者的服务,提供者返回一个字符串,消费者打印提供者返回的数据。底层网络通信使用 Netty 4.1.20

涉及说明

创建一个接口,定义抽象方法。用于消费者和提供者之间的约定。 创建一个提供者,该类需要监听消费者的请求,并按照约定返回数据。 创建一个消费者,该类需要透明的调用自己不存在的方法,内部需要使用 Netty 请求提供者返回数据

服务调用流程说明

  1. 服务消费方(client)以本地调用方式调用服务
  2. client stub 接收到调用后负责将方法、参数等封装成能够进行网络传输的消息体
  3. client stub 将消息进行编码并发送到服务端
  4. server stub 收到消息后进行解码
  5. server stub 根据解码结果调用本地的服务
  6. 本地服务执行并将结果返回给
  7. server stub server stub 将返回导入结果进行编码并发送至消费方
  8. client stub 接收到消息并进行解码
  9. 服务消费方(client)得到结果

RPC 的目标就是将 2-8 这些步骤都封装起来,用户无需关心这些细节,可以像调用本地方法一样即可完成远程服务调用。

代码实现

公共接口实现

//这个是接口,是服务提供方和 服务消费方都需要
public interface HelloService {

    String hello(String mes);
}

服务提供方实现接口

public class HelloServiceImpl implements HelloService {

    private static int count = 0;
    //当有消费方调用该方法时, 就返回一个结果
    @Override
    public String hello(String mes) {
        System.out.println("收到客户端消息=" + mes);
        //根据mes 返回不同的结果
        if(mes != null) {
            return "你好客户端, 我已经收到你的消息 [" + mes + "] 第" + (++count) + " 次";
        } else {
            return "你好客户端, 我已经收到你的消息 ";
        }
    }
}

服务提供方的相关代码

NettyServer
public class NettyServer {


    public static void startServer(String hostName, int port) {
        startServer0(hostName,port);
    }

    //编写一个方法,完成对NettyServer的初始化和启动

    private static void startServer0(String hostname, int port) {

        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {

            ServerBootstrap serverBootstrap = new ServerBootstrap();

            serverBootstrap.group(bossGroup,workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                                      @Override
                                      protected void initChannel(SocketChannel ch) throws Exception {
                                          ChannelPipeline pipeline = ch.pipeline();
                                          pipeline.addLast(new StringDecoder());
                                          pipeline.addLast(new StringEncoder());
                                          pipeline.addLast(new NettyServerHandler()); //业务处理器

                                      }
                                  }

                    );

            ChannelFuture channelFuture = serverBootstrap.bind(hostname, port).sync();
            System.out.println("服务提供方开始提供服务~~");
            channelFuture.channel().closeFuture().sync();

        }catch (Exception e) {
            e.printStackTrace();
        }
        finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }

    }
}

NettyServerHandler
//服务器这边handler比较简单
public class NettyServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        //获取客户端发送的消息,并调用服务
        System.out.println("msg=" + msg);
        //客户端在调用服务器的api 时,我们需要定义一个协议
        //比如我们要求 每次发消息是都必须以某个字符串开头 "HelloService#hello#你好"
        if(msg.toString().startsWith(ClientBootstrap.providerName)) {

            String result = new HelloServiceImpl().hello(msg.toString().substring(msg.toString().lastIndexOf("#") + 1));
            ctx.writeAndFlush(result);
        }
    }

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

ServerBootstrp:启动服务
//ServerBootstrap 会启动一个服务提供者,就是 NettyServer
public class ServerBootstrap {
    public static void main(String[] args) {

        //代码代填..
        NettyServer.startServer("127.0.0.1", 7000);
    }
}

服务消费方代码

NettyClient
public class NettyClient {

    //创建线程池
    private static ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

    private static NettyClientHandler client;
    private int count = 0;

    //编写方法使用代理模式,获取一个代理对象

    public Object getBean(final Class<?> serivceClass, final String providerName) {

        return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
                new Class<?>[]{serivceClass}, (proxy, method, args) -> {

                    System.out.println("(proxy, method, args) 进入...." + (++count) + " 次");
                    //{}  部分的代码,客户端每调用一次 hello, 就会进入到该代码
                    if (client == null) {
                        System.out.println("Handler为空");
                        initClient();
                    }
                    System.out.println("Handler不为空");

                    //设置要发给服务器端的信息
                    //providerName 协议头 args[0] 就是客户端调用api hello(???), 参数
                    client.setPara(providerName + args[0]);

                    //
                    return executor.submit(client).get();

                });
    }

    public NettyClient() {
        initClient();
    }

    //初始化客户端
    private static void initClient() {
        client = new NettyClientHandler();
        //创建EventLoopGroup
        NioEventLoopGroup group = new NioEventLoopGroup();
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(group)
                .channel(NioSocketChannel.class)
                .option(ChannelOption.TCP_NODELAY, true)
                .handler(
                        new ChannelInitializer<SocketChannel>() {
                            @Override
                            protected void initChannel(SocketChannel ch) throws Exception {
                                ChannelPipeline pipeline = ch.pipeline();
                                pipeline.addLast(new StringDecoder());
                                pipeline.addLast(new StringEncoder());
                                pipeline.addLast(client);
                            }
                        }
                );

        try {
            bootstrap.connect("127.0.0.1", 7000).sync();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

NettyClientHandler
public class NettyClientHandler extends ChannelInboundHandlerAdapter implements Callable {

    private ChannelHandlerContext context;//上下文
    private String result; //返回的结果
    private String para; //客户端调用方法时,传入的参数


    //与服务器的连接创建后,就会被调用, 这个方法是第一个被调用(1)
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println(" channelActive 被调用  ");
        context = ctx; //因为我们在其它方法会使用到 ctx
        System.out.println("初始化ctx....");
        System.out.println(ctx);
    }

    //收到服务器的数据后,调用方法 (4)
    //
    @Override
    public synchronized void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println(" channelRead 被调用  ");
        result = msg.toString();
        notify(); //唤醒等待的线程
    }

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

    //被代理对象调用, 发送数据给服务器,-> wait -> 等待被唤醒(channelRead) -> 返回结果 (3)-》5
    @Override
    public synchronized Object call() throws Exception {
        System.out.println(" call1 被调用  ");
        context.writeAndFlush(para);
        //进行wait
        wait(); //等待channelRead 方法获取到服务器的结果后,唤醒
        System.out.println(" call2 被调用  ");
        return  result; //服务方返回的结果

    }
    //(2)
    void setPara(String para) {
        System.out.println(" setPara  ");
        this.para = para;
    }
}

ClientBootStrap
public class ClientBootstrap {


    //这里定义协议头
    public static final String providerName = "HelloService#hello#";

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

        //创建一个消费者
        NettyClient customer = new NettyClient();

        //创建代理对象
        HelloService service = (HelloService) customer.getBean(HelloService.class, providerName);

        for (int i = 0; i < 3; i++) {
            Thread.sleep(2 * 1000);
            //通过代理对象调用服务提供者的方法(服务)
            String res = service.hello("你好 dubbo~");
            System.out.println("调用的结果 res= " + res);
        }
    }
}

测试

F:\java1.8\bin\java.exe "-javaagent:F:\IDEA2020.2\IntelliJ IDEA 2020.2\lib\idea_rt.jar=53999:F:\IDEA2020.2\IntelliJ IDEA 2020.2\bin" -Dfile.encoding=UTF-8 -classpath F:\java1.8\jre\lib\charsets.jar;F:\java1.8\jre\lib\deploy.jar;F:\java1.8\jre\lib\ext\access-bridge-64.jar;F:\java1.8\jre\lib\ext\cldrdata.jar;F:\java1.8\jre\lib\ext\dnsns.jar;F:\java1.8\jre\lib\ext\jaccess.jar;F:\java1.8\jre\lib\ext\jfxrt.jar;F:\java1.8\jre\lib\ext\localedata.jar;F:\java1.8\jre\lib\ext\nashorn.jar;F:\java1.8\jre\lib\ext\sunec.jar;F:\java1.8\jre\lib\ext\sunjce_provider.jar;F:\java1.8\jre\lib\ext\sunmscapi.jar;F:\java1.8\jre\lib\ext\sunpkcs11.jar;F:\java1.8\jre\lib\ext\zipfs.jar;F:\java1.8\jre\lib\javaws.jar;F:\java1.8\jre\lib\jce.jar;F:\java1.8\jre\lib\jfr.jar;F:\java1.8\jre\lib\jfxswt.jar;F:\java1.8\jre\lib\jsse.jar;F:\java1.8\jre\lib\management-agent.jar;F:\java1.8\jre\lib\plugin.jar;F:\java1.8\jre\lib\resources.jar;F:\java1.8\jre\lib\rt.jar;D:\IDEAWorkSpace\Netty\Spring\target\classes;E:\Maven\warehouse\maven_repository\org\springframework\spring-core\5.3.1\spring-core-5.3.1.jar;E:\Maven\warehouse\maven_repository\org\springframework\spring-jcl\5.3.1\spring-jcl-5.3.1.jar;E:\Maven\warehouse\maven_repository\org\springframework\spring-beans\5.3.1\spring-beans-5.3.1.jar;E:\Maven\warehouse\maven_repository\org\springframework\spring-context\5.3.1\spring-context-5.3.1.jar;E:\Maven\warehouse\maven_repository\org\springframework\spring-aop\5.3.1\spring-aop-5.3.1.jar;E:\Maven\warehouse\maven_repository\org\springframework\spring-expression\5.3.1\spring-expression-5.3.1.jar;E:\Maven\warehouse\maven_repository\org\junit\jupiter\junit-jupiter\5.8.0-M1\junit-jupiter-5.8.0-M1.jar;E:\Maven\warehouse\maven_repository\org\junit\jupiter\junit-jupiter-api\5.8.0-M1\junit-jupiter-api-5.8.0-M1.jar;E:\Maven\warehouse\maven_repository\org\apiguardian\apiguardian-api\1.1.1\apiguardian-api-1.1.1.jar;E:\Maven\warehouse\maven_repository\org\opentest4j\opentest4j\1.2.0\opentest4j-1.2.0.jar;E:\Maven\warehouse\maven_repository\org\junit\platform\junit-platform-commons\1.8.0-M1\junit-platform-commons-1.8.0-M1.jar;E:\Maven\warehouse\maven_repository\org\junit\jupiter\junit-jupiter-params\5.8.0-M1\junit-jupiter-params-5.8.0-M1.jar;E:\Maven\warehouse\maven_repository\org\junit\jupiter\junit-jupiter-engine\5.8.0-M1\junit-jupiter-engine-5.8.0-M1.jar;E:\Maven\warehouse\maven_repository\org\junit\platform\junit-platform-engine\1.8.0-M1\junit-platform-engine-1.8.0-M1.jar;E:\Maven\warehouse\maven_repository\org\jetbrains\kotlin\kotlin-stdlib-jdk8\1.4.20\kotlin-stdlib-jdk8-1.4.20.jar;E:\Maven\warehouse\maven_repository\org\jetbrains\kotlin\kotlin-stdlib\1.4.20\kotlin-stdlib-1.4.20.jar;E:\Maven\warehouse\maven_repository\org\jetbrains\kotlin\kotlin-stdlib-common\1.4.20\kotlin-stdlib-common-1.4.20.jar;E:\Maven\warehouse\maven_repository\org\jetbrains\annotations\13.0\annotations-13.0.jar;E:\Maven\warehouse\maven_repository\org\jetbrains\kotlin\kotlin-stdlib-jdk7\1.4.20\kotlin-stdlib-jdk7-1.4.20.jar;E:\Maven\warehouse\maven_repository\io\netty\netty-all\4.1.18.Final\netty-all-4.1.18.Final.jar;E:\Maven\warehouse\maven_repository\com\alibaba\fastjson\1.2.28\fastjson-1.2.28.jar com.pjh.Example.dubborpc.customer.ClientBootstrap
channelActive 被调用  
初始化ctx....
ChannelHandlerContext(NettyClientHandler#0, [id: 0x7a836196, L:/127.0.0.1:58448 - R:/127.0.0.1:7000])
(proxy, method, args) 进入....1Handler不为空
 setPara  
 call1 被调用  
 channelRead 被调用  
 call2 被调用  
调用的结果 res= 你好客户端, 我已经收到你的消息 [你好 dubbo~]1(proxy, method, args) 进入....2Handler不为空
 setPara  
 call1 被调用  
 channelRead 被调用  
 call2 被调用  
调用的结果 res= 你好客户端, 我已经收到你的消息 [你好 dubbo~]2(proxy, method, args) 进入....3Handler不为空
 setPara  
 call1 被调用  
 channelRead 被调用  
 call2 被调用  
调用的结果 res= 你好客户端, 我已经收到你的消息 [你好 dubbo~]3
msg=HelloService#hello#你好 dubbo~
收到客户端消息=你好 dubbo~
msg=HelloService#hello#你好 dubbo~
收到客户端消息=你好 dubbo~
msg=HelloService#hello#你好 dubbo~
收到客户端消息=你好 dubbo~

实操自己实现一个复杂版的RPC框架

后续更新。。。。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值