Netty实现自定义RPC框架

193 阅读7分钟

Netty实现自定义RPC框架

前言

Netty框架作为“java进阶之梯”,是程序员绕不开的一个框架。目前市面上很多开源框架, 基本上都是使用Netty作为网络通信的底层框架,包括Dubbo、RocketMQ、Canal、otter、HBase等等。 本文根据一个简单的业务需求,使用Netty框架搭建一个简单的RPC框架,包括服务端,客户端,注册中心等,暂时不考虑监控,主要目的是带大家熟悉一下Netty的一些常用API,以及熟悉下服务间的远程调用过程。Netty其实就是对网络通信的封装框架,说到底底层还是IO那一套。建议大家先熟悉一下NIO的三件套,即buffer,selector,channel。

什么是RPC?

RPC是一种远程调用过程,是一种通过网络远程调用其他服务的协议。通俗的说就是,A通过打电话的方式让B帮忙办一件事,B办完事后将结果告知A。

RPC的远程调用流程:

  1. client客户端发起请求到client stub(客户端存根:存放服务端地址信息,将客户端的请求参数编组成网络 消息,再通过网络发送给服务方),client stub接收到调用后负责将方法、参数等编组成能够进行网络传输的 消息体;在java中就是序列化的过程。
  2. client stub找到服务地址,并将消息通过网络发送到服务端(server),server stub(服务端存根:接受客户 端发送过来的消息并解组,再调用本地服务,再将本地服务执行结果发送给客户端)收到消息后进行解组。
  3. server stub根据解组结果调用本地的服务;
  4. 本地服务执行处理逻辑,将结果返回给server stub;
  5. server stub将返回结果编组成网络消息,通过网络并发送客户端;
  6. client stub接收到消息,并进行解组;
  7. 服务调用方client得到最终结果。

rpc.webp

需求

现在我们模拟一个需求,订单服务(客户端)通过id调用商品服务(服务端),商品服务经过一系列业务逻辑,最终返回客户端商品的信息。

导入依赖

首先我们需要建一个maven项目,然后在pom文件中导入依赖。因为我们的实现只用到了Netty原生API,所以我们只需要导入两个依赖,分别是lombok的依赖和netty相关的依赖。

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

服务提供端

服务端首先需要有5个类:

实体类:商品信息类

商品服务接口类:提供商品服务的相关功能接口

商品服务实现类: 具体实现商品服务的相关功能

服务业务处理类: 接收到客户端的请求后,会先经过处理类处理相关业务,如果请求很多的情况下,可以在 这个类里面做请求分发处理。

启动类: 启动商品,监听一个端口。

代码如下:

实体类

package cn.itsource.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author : pankun
 * @Version : 1.0
 **/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ShopInfo {
    /**
     * 商品名称
     */
    private String name;
    /**
     * 商品价格
     */
    private String price;
    /**
     * 商品产地
     */
    private String address;

}

服务接口

package cn.itsource.provider;

/**
 * @author : pankun
 * @Version : 1.0
 **/
public interface ShopService {

    String getShopInfoById(int a);

}

服务实现类

package cn.itsource.provider;

import cn.itsource.domain.ShopInfo;

import java.util.HashMap;

/**
 * @author : pankun
 * @Version : 1.0
 *
 **/
public class ShopServiceImpl implements ShopService {
    private static HashMap<Integer, ShopInfo> map = new HashMap<Integer, ShopInfo>();

    static {
        map.put(1, new ShopInfo("苹果", "5","四川"));
        map.put(2, new ShopInfo("香蕉", "6","海南"));
        map.put(3, new ShopInfo("芒果", "8","云南"));
    }

    @Override
    public String getShopInfoById(int a) {
        return map.get(a).toString();
    }
}

服务业务处理类

package cn.itsource.provider;

import cn.itsource.protocol.MyInvokerProtocol;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author : pankun
 * @Version : 1.0
 **/

public class RpcServerHandler extends ChannelInboundHandlerAdapter {
 
    // 注册中心 (容器)
    public static ConcurrentHashMap<String,Object> context = new ConcurrentHashMap<String,Object>();
    // 类信息集合
    private List<String> classNames = new ArrayList<String>();


    // 构造器初始化
    public RpcServerHandler(){
        // 包扫描信息
        scannerClass("cn.itsource.provider");
        // 注册容器
        doRegistry();
    }
 
 
    // 递归包扫描
    private void scannerClass(String packageName) {
        // 获取类加载器
        URL url = this.getClass().getClassLoader().getResource(packageName.replaceAll("\\.", "/"));
        File dir = new File(url.getFile());
        for (File file : dir.listFiles()) {
            //如果是一个文件夹,继续递归
            if(file.isDirectory()){
                scannerClass(packageName + "." + file.getName());
            }else{
                classNames.add(packageName + "." + file.getName().replace(".class", "").trim());
            }
        }
    }
 
    // 注册
    private void doRegistry() {
        if(classNames.isEmpty()){return;}
        for (String className : classNames) {
            try {
                Class<?> clazz = Class.forName(className);
                Class<?> anInterface = clazz.getInterfaces()[0]; // 因为此demo默认实现一个接口  所以此处写死获取当前第一个接口信息
                context.put(anInterface.getName(),clazz.newInstance());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
 
    // 请求到达  执行执行的业务逻辑
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        Object result = new Object();
        MyInvokerProtocol request = (MyInvokerProtocol) msg; // netty 会按照我们自定义的策略进行转换
        // 判断当前调用服务在容器中是否真正存在
        if(context.containsKey(request.getClassName())){
            // 确实存在执行对应的业务逻辑
            Object clazz = context.get(request.getClassName());
            // 获取真正执行的
            Method method = clazz.getClass().getMethod(request.getMethodName(), request.getParames());
            result = method.invoke(clazz, request.getValues());
        }
        if(result != null){
            ctx.write(result);
        }
        ctx.flush();
        ctx.close();
    }
 
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
 
 
}

启动类

package cn.itsource.provider;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
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.NioServerSocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.LengthFieldPrepender;
import io.netty.handler.codec.serialization.ClassResolvers;
import io.netty.handler.codec.serialization.ObjectDecoder;
import io.netty.handler.codec.serialization.ObjectEncoder;


/**
 * @author : pankun
 * @Version : 1.0
 **/
public class RpcServer {
 
    // 暴露端口
    private  int port;
 
    public RpcServer(int port){
        this.port = port;
    }
 
    /**
     * 开启服务接口监听
     * 调用 netty 相关API实现接口监听
     */
    public void listen(){
        // Boss线程 (Selector核心)
        NioEventLoopGroup boss = new NioEventLoopGroup();
        // Work线程 (工作线程)
        NioEventLoopGroup work = new NioEventLoopGroup();
 
        // 1. 建立服务
        ServerBootstrap server = new ServerBootstrap();
        // 2. 注入 Boos/Worker
        server.group(boss,work)
                .channel(NioServerSocketChannel.class) // 3. 管道执行 keys 轮询的核心
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    protected void initChannel(SocketChannel channel) throws Exception {
                        // 5. 对流数据进行解析
                        ChannelPipeline pipeline = channel.pipeline();
                        // 6. 自定义协议解码器 (取决于自己定义的规则对象)
                        /** 入参有5个,分别解释如下
                         *  maxFrameLength:框架的最大长度。如果帧的长度大于此值,则将抛出TooLongFrameException。
                         *  lengthFieldOffset:长度字段的偏移量:即对应的长度字段在整个消息数据中得位置
                         *  lengthFieldLength:长度字段的长度。如:长度字段是int型表示,那么这个值就是4(long型就是8)
                         *  lengthAdjustment:要添加到长度字段值的补偿值
                         *  initialBytesToStrip:从解码帧中去除的第一个字节数
                         */
                        pipeline.addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4));
                        //自定义协议编码器
                        pipeline.addLast(new LengthFieldPrepender(4));
                        // 7. 参数解析
                        //对象参数类型编码器
                        pipeline.addLast("encoder",new ObjectEncoder());
                        //对象参数类型解码器
                        pipeline.addLast("decoder",new ObjectDecoder(Integer.MAX_VALUE, ClassResolvers.cacheDisabled(null)));
                        // 8. 执行业务逻辑
                        pipeline.addLast(new RpcServerHandler());
                    }
                }) // 4. 子线程 执行对应的业务逻辑
                .option(ChannelOption.SO_BACKLOG,128) // 主线程最大连接数
                .childOption(ChannelOption.SO_KEEPALIVE,true); // 子线程持续
 
        try {
            // 服务绑定端口
            ChannelFuture future = server.bind(port).sync();
            System.out.println("RPC start success, listen port is :" +  port + " !!");
            future.channel().closeFuture().sync(); // 回调
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            boss.shutdownGracefully();
            work.shutdownGracefully();
        }
    }
 
    
    public static void main(String[] args) {
        // 监听启动
        new RpcServer(8082).listen();
    }
 
 
}

服务消费端

客户端我们需要4个类:

客户端启动类:客户端发起请求,开启远程调用,显示调用结果

RPC动态代理类:创建代理对象,封装request请求对象,创建RpcClient对象,

客户端业务处理类:主要是发送消息到服务端,接收服务端返回的消息

Netty启动类:连接netty服务端,提供消息发送等方法,并且可以自定义传输协议。

代码如下:

Netty启动类

package cn.itsource.consumer;

import cn.itsource.protocol.MyInvokerProtocol;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
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.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.LengthFieldPrepender;
import io.netty.handler.codec.serialization.ClassResolvers;
import io.netty.handler.codec.serialization.ObjectDecoder;
import io.netty.handler.codec.serialization.ObjectEncoder;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;


/**
 * @author : pankun
 * @Version : 1.0
 **/
public class RpcClient implements InvocationHandler {
 
    private Class<?> clazz;
    public RpcClient(Class<?> clazz){
        this.clazz = clazz;
    }
 
    // 动态代理执行相应业务逻辑
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if(Object.class.equals(method.getDeclaringClass())){
            // 如果当前就是一个实现类
            return method.invoke(proxy,args);
        }else{
            return rpcInvoke(method,args);
        }
    }
 
    private Object rpcInvoke(Method method, Object[] args) {
        MyInvokerProtocol request = new MyInvokerProtocol();
        request.setClassName(this.clazz.getName()); // 类名称
        request.setMethodName(method.getName()); // 方法名称
        request.setParames(method.getParameterTypes()); // 入参列表
        request.setValues(args); // 实参列表
 
        // TCP 远程调用
        final RpcClientHandler consumerHandler = new RpcClientHandler();
 
        NioEventLoopGroup work = new NioEventLoopGroup();
 
        Bootstrap server = new Bootstrap();
        server.group(work)
                .channel(NioSocketChannel.class)// 客户端管道
                .option(ChannelOption.TCP_NODELAY, true) // 开启
                .handler(new ChannelInitializer<SocketChannel>() {
                    protected 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 = null;
        try {
            future = server.connect("localhost", 8082).sync();
            future.channel().writeAndFlush(request).sync();
            future.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            work.shutdownGracefully();
        }
        return consumerHandler.getResponse();
    }
}

客户端业务处理类

package cn.itsource.consumer;

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

/**
 * @author : pankun
 * @Version : 1.0
 **/
public class RpcClientHandler extends ChannelInboundHandlerAdapter {
 
    private Object response;
 
    public Object getResponse() {
        return response;
    }
 
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        response = msg;
    }
 
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        super.exceptionCaught(ctx, cause);
    }
}

RPC动态代理类

package cn.itsource.consumer;

import java.lang.reflect.Proxy;

/**
 * @author : pankun
 * @Version : 1.0
 **/
public class RpcProxy {
 
    public static <T> T invoke(Class<T> clazz){
        Class<?> [] interfaces = clazz.isInterface() ?
                new Class[]{clazz} :
                clazz.getInterfaces();
       T result = (T) Proxy.newProxyInstance(clazz.getClassLoader(),interfaces,new RpcClient(clazz));
       return result;
    }
 
}

客户端启动类

package cn.itsource.consumer;

import cn.itsource.provider.ShopService;

/**
 * @author : pankun
 * @Version : 1.0
 **/
public class IRpcConsumer {
 
    public static void main(String[] args) {
        ShopService invoke1 = RpcProxy.invoke(ShopService.class);
        System.out.println(invoke1.getShopInfoById(1));
        System.out.println(invoke1.getShopInfoById(2));
        System.out.println(invoke1.getShopInfoById(3));
    }
}

自定义传输协议

使用Netty框架可以自定义传输协议,如果定义了自定义传输协议,Netty会按照我们自定义的策略进行转换,所以我们这里有一个类来自定义传输协议。代码如下:

package cn.itsource.protocol;

import lombok.Data;

import java.io.Serializable;

/**
 * 自定义传输协议
 */
@Data
public class MyInvokerProtocol implements Serializable {
 
    private String className;//类名
    private String methodName;//函数名称 
    private Class<?>[] parames;//形参列表
    private Object[] values;//实参列表
 
}

测试

测试远程调用,我们首先启动服务端启动类RpcServer。控制台出现如下信息表示启动成功。

微信图片_20221113104940.jpg 然后我们启动客户端启动类IRpcConsumer,开启远程调用,如果客户端的控制台能够获取到服务端返回的相关商品信息,那么表示远程调用成功。如下图:

微信图片_20221113104949.jpg