使用netty自定义协议,手撸一个RPC框架

RPC概述

  RPC(Remote Procedure Call)— 远程过程调用,是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外地为这个交互作用编程。两个或多个应用程序分布在不同的服务器上,它们之间的调用都像是本地方法调用一样,常见的 RPC 框架有: 比较知名的如阿里的Dubbo、google的gRPC、Go语言的rpcx、Apache的thrift, Spring 旗下的 Spring Cloud。今天我们就利用netty框架,手撸一款简易的RPC框架(不考虑服务注册中心)。

RPC调用流程说明

  1.服务消费方(client)以本地调用方式调用服务

  2.client stub (客户端根程序)接收到调用后负责将方法、参数等封装成能够进行网络传输的消息体

  3.client stub 将消息进行编码并发送到服务端

  4.server stub (服务端根程序) 收到消息后进行解码

  5.server stub 根据解码结果调用本地的服务

  6.本地服务执行并将结果返回给 server stub

  7server stub 将返回导入结果进行编码并发送至消费方

  8.client stub 接收到消息并进行解码

  9.服务消费方(client)得到结果


  RPC 的目标就是将 2-8 步骤都封装起来,用户无需关心这些细节,可以像调用本地方法一样完成远程服务调用。其中编码和解码需要我们留意一下:在TCP进行数据传输的过程中,出站(消息发出)前需要进行编码操作,入站(消息接收)前需要进行解码操作,编码解码器则成为了必不可少的内容,否则会引发粘包和拆包问题,关于粘包和拆包,不明白的朋友可以自行百度,在此不做赘述。

  RPC调用流程图如下:
调用流程图

RPC实现思路

  1.创建一个接口,定义抽象方法。用于消费者和提供者之间的约定。

  2.创建一个提供者,该类需要监听消费者的请求,并按照约定返回数据。

  3.创建一个消费者,该类需要透明的调用自己不存在的方法,内部需要使用 Netty 请求提供者返回数据

RPC编码实现

  我们把协议定义的简单一些:只有以 myRPC# 开头的消息发送到server,才会对消息进行处理并返回给client。
  pom.xml引入相关依赖:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>netty</groupId>
    <artifactId>nettyDemo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <!-- https://mvnrepository.com/artifact/io.netty/netty-all -->
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.20.Final</version>
        </dependency>
    </dependencies>


    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>utf-8</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

创建公共接口

  创建公共接口,这里我们也做的简单一些,如下:HelloService.java

package rpc.publicinterface;

//公共接口,服务提供方和服务消费方都需要
public interface HelloService {

    String hello(String mes);
}

服务提供方代码实现

  公共接口实现类(供提供者调用为消费者返回结果):HelloServiceImpl.java

package rpc.provider;

import rpc.publicinterface.HelloService;

public class HelloServiceImpl implements HelloService {

    @Override
    public String hello(String mes) {
        //根据mes 返回不同的结果
        if (mes != null) {
            return "你好客户端, 我已经收到你的消息 [" + mes + "] ";
        } else {
            return "你好客户端, 我已经收到你的消息 ";
        }
    }
}

  服务提供方实现:NettyServer.java

package rpc.netty;


import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;

public class NettyServer {


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

    //编写一个方法,完成对NettyServer的初始化和启动
    private static void startServer0(String hostname, int port) {
		//处理accecpt
        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.java

package rpc.netty;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import rpc.customer.ClientBootstrap;
import rpc.provider.HelloServiceImpl;

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

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

  服务提供方启动类实现:ServerBootstrap.java

package rpc.provider;


import rpc.netty.NettyServer;

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

        NettyServer.startServer("127.0.0.1", 7000);
    }
}

服务消费方代码实现

  服务消费方业务处理类实现:NettyClientHandler.java

package rpc.netty;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

import java.util.concurrent.Callable;

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 {
        context = ctx; //因为我们在其它方法会使用到 ctx
    }

    //收到服务器的数据后,调用方法 (4)
    @Override
    public synchronized void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        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 {
        context.writeAndFlush(para);
        //进行wait
        wait(); //等待channelRead 方法获取到服务器的结果后,唤醒
        return result; //服务方返回的结果

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

  在消费者业务处理类中使用了线程的等待唤醒机制,当向服务端发送请求之后,进入等待状态,直到读取到服务端返回的数据才唤醒睡眠的线程,然后返回服务端响应的结果,此处并没有考虑超时等因素。
  服务提供方实现:NettyClient.java

package rpc.netty;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;

import java.lang.reflect.Proxy;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class NettyClient {

    //创建线程池用于执行请求服务端的任务
    private static ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

    private static NettyClientHandler client;

    //编写方法使用代理模式,获取一个代理对象
    public Object getBean(final Class<?> serivceClass, final String providerName) {

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

                    //客户端每调用一次 hello, 就会进入到该代码
                    if (client == null) {
                        initClient();
                    }

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

					//执行任务,调用call方法,携带数据向服务端发送请求
                    return executor.submit(client).get();

                });
    }

    //初始化客户端
    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();
        }
    }
}

  服务提供方的getBean方法使用了jdk的动态代理,通过调用getBean可以获得其代理对象,发送请求给服务端。
  服务端启动类:ClientBootstrap.java

package rpc.customer;

import rpc.netty.NettyClient;
import rpc.publicinterface.HelloService;

public class ClientBootstrap {


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

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

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

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

        //通过代理对象调用服务提供者的方法(服务)
        String res = service.hello("你好 ~");
        System.out.println("调用的结果 res= " + res);
    }
}

测试

  启动服务端,运行ServerBootstrap:
服务提供者
  启动消费端,运行ClientBootstrap:
调用结果  服务端同样也受到消费端发送的消息:
服务端
  测试成功,一个简易RPC框架创建完毕。

小结

  此篇博客讲解了RPC的基本概念,设计思路以及编码实现,其中主要用到了线程的等待唤醒机制,线程池异步提交任务获取结果和JDK动态代理,这都需要我们完全理解并熟练掌握。同时我们也要感谢netty的开发者们,没有他们,NIO编程将变得极其繁琐复杂。
  实际的RPC框架将远比我们案例要复杂的多,但是通过此案例想必也会让你有一定的收获和感悟,干这一行就是一个不断学习的过程,学的越多,懂的越少,学海无涯,与君共勉,2020,加油!

发布了28 篇原创文章 · 获赞 100 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/m0_37719874/article/details/104049130