手写rpc?也不是很难

Nexus-RPC

介绍

Nexus是一个简单的rpc框架,基本实现rpc的主要功能,目前须结合spring使用,其中使用Netty为通信框架,使用kryo作为默认序列化协议,通过spi来选配序列化协议、 负载均衡器、注册中心;

1、为什么要写这个RPC

  • 最近在学习nio的知识,个人学习使用,在netty学习过程中,算是一个学习成果;
  • 学习一些rpc设计上的思想,想仿造一个dubbo;
  • 学会造轮子;

2、为什么叫Nexus

  • because of love!

3、项目架构

  • 【annotation】
    客户端/服务端 服务注解;
  • 【loadbalancer】
    客户端负载均衡
  • 【config】
    配置类(springboot自动装配)
  • 【discovery】
    服务发现
  • 【exception】
    异常类
  • 【interceptor】
    拦截器
  • 【invocation】
    动态代理
  • 【io】
    io通信层
  • 【property】
    配置属性
  • 【registry】
    注册中心
  • 【serialize】
    序列化
  • 【spi】
    spi层
  • 【spring】
    spring监听器和BeaPostProcessor

服务端

服务端端使用netty最为常用的多主从Reactor模型,如下

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

bossGroup(即主Reactor)负责接受请求,workerGroup(即从Reactor)负责io读写,EventLoopGroup为netty对线程池的封装,bossGroup在只监听一个端口号的时候,默认开启一个线程,workerGroup主要做的是io读写,即为io密集型,默认为核心cpu数*2;

业务线程池,如下

// 业务线程池
final Executor executor = new ThreadPoolExecutor(maxWorkThreadCount, maxWorkThreadCount, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), r -> {
   Thread thread = new Thread(r);
   thread.setDaemon(true);
   thread.setName(String.format("nexus-server-executor-%d", thread.getId()));
   return thread;
});

客户端

客户端为netty客户端的标准实现

private final EventLoopGroup group = new NioEventLoopGroup();
private final Bootstrap strap = new Bootstrap();

客户端连接池

new FixedChannelPool(strap.remoteAddress(key), new DefaultChannelPoolHandler(), maxConnection);

nexus使用netty的FixedChannelPool,最大连接数通过配置文件配置,此连接池中每个连接(channel)和大多数db连接池类似,为线程独占;

编解码器

.addLast("encode", new LengthFieldPrepender(8))
.addLast("decode", new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0,8,0,8))

使用LengthFieldPrepender和LengthFieldBasedFrameDecoder解决Tcp传输中粘包和半包问题,这种方式会在数据包加一个8个字节的数据,用来表示数据包的长度;

服务注册发现

nexus默认并仅仅支持使用consul作为服务注册中心,后续会加入zk,nacos;

动态代理

使用jdk动态代理生成代理对象,供客户端使用

public <T> T getProxyInstance(Class<T> clazz) {
           return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, new InvocationHandler() {
               @Override
               public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                   String serviceName = clazz.getName();
                   final List<MateInfo> serviceInfos = serviceDiscovery.listServices(serviceName);
                   logger.info("Rpc server instance list: {}", serviceInfos);
                   if (CollectionUtils.isEmpty(serviceInfos)) {
                       throw new RpcException("No rpc servers found.");
                   }
                   final MateInfo mateInfo = loadBalancer.getService(serviceInfos);
                   final RpcRequest rpcRequest = new RpcRequest();
                   rpcRequest.setServiceName(serviceName);
                   rpcRequest.setMethod(method.getName());
                   rpcRequest.setParameterTypes(method.getParameterTypes());
                   rpcRequest.setParameters(args);
                   // 编码请求消息
                   byte[] data = serializer.serialize(rpcRequest);
                   // 发送消息
                   byte[] byteResponse = rpcClient.sendMessage(data, mateInfo);
                   // 解码响应消息
                   final RpcResponse rpcResponse = serializer.deserialize(byteResponse, RpcResponse.class);
                   if (rpcResponse.getException() != null) {
                       throw rpcResponse.getException();
                   }
                   // 解析返回结果进行处理
                   return rpcResponse.getData();
               }
           });
       }

序列化协议

nexus有两种序列化协议可以选择,一种时jdk自带的,一种是kryo,默认使用kryo,可以通过配置文件修改,由于是使用spi进行加载,也可实现自己的协议,通过实现com.mao.nexus.serialize.Serializer接口,并在 META-INF\nexus\internal\com.mao.nexus.serialize.Serializer文件放入自己的实现类的全限定名和协议名称;

spi

  • spi介绍

    SPI全称为Service Provider Interface,对应中文为服务发现机制。 SPI类似一种可插拔机制,首先需要定义一个接口或一个约定,然后不同的场景可以对其进行实现,调用方在使用的时候无需过多关注具体的实现细节。 在Java中,SPI体现了面向接口编程的思想,满足开闭设计原则。

  • nexus中spi

    nexus的spi使用了和dubbo的相同的实现,拿负载均衡器举例,nexus默认使用负载均衡策略为轮询,在ExtensionLoader通过配置文件clusterProperties拿到实例对象,如下:

ExtensionLoader<LoadBalancer> extensionLoader = ExtensionLoader.getExtensionLoader(LoadBalancer.class);
     return extensionLoader.getExtension(clusterProperties.getLoadBalance());

此时如果想实现随机的策略,只需要实现LoadBalancer接口,重写getService方法,如下

public class RandomLoadBalancer implements LoadBalancer {

    private static final Random counter = new Random();

    @Override
    public MateInfo getService(List<MateInfo> services) {
        return services.get(counter.nextInt(services.size()));
    }
}

并在META-INF\nexus\internal\com.mao.nexus.loadbalancer.LoadBalancer文件中该类的全限定名和协议名称,如下:

random=com.mao.nexus.loadbalancer.RandomLoadBalancer
train=com.mao.nexus.loadbalancer.TrainLoadBalancer

接着在配置文件中指定协议名称,如下:

nexus:
  cluster:
    load-balance: random

即实现随机策略;

spring

  • 在spring.factories中加入配置类全限定名,进行自动装配;
  • 使用ApplicationListener中onApplicationEvent方法启用netty服务端并扫描容器中是否有@NexusService注解的bean,有就注册到注册中心,onApplicationEvent方法会在spring对容器刷新时(refresh)调用;
  • 使用BeanPostProcessor的前置处理方法对客户端所有bean中含有@NexusClient属性,进行注入代理对象;

连接复用

在连接池部分提到过,在nio模型中,channel的writeAndFlush方法是一个异步方法,调用之后立即返回,因此单个连接可以被多个io线程进行读写;

  • 连接复用的实现

    具体的实现思路就是在单个channel中,在数据包中加一个唯一标识字段,客户端每一次发送数据新建一个callBack,然后放在一个全局的map中,以唯一标识为key,callBack对象为value,在接受到服务端响应时,会将唯一标识带过来,拿到标识字段的值,去map中拿到callBack对象,调用callBack对象handle方法去处理响应数据包的数据,这样每一次请求就能和响应对应起来,而对于channel来说,只用关心发数据和接受数据就行了,后续的处理交给callBack处理就行了;

    Show code!!!

    在RpcRequest和RpcResponse继承一类MateData,这个类中有一个long类型的唯一标识,代码如下:

    @Data
    public class MateData implements Serializable {
        private Long uniqueIdentification;
    }
    

    这里 新建一个了处理的回调类,代码如下:

    public class ChannelCallBack implements CallBack {
    
        private static Logger logger = LoggerFactory.getLogger(ChannelCallBack.class);
        private RpcResponse resp = null;
    
        @Override
        public void handle(RpcResponse response) {
            this.resp = response;
            synchronized (this) {
                this.notify();
            }
        }
    
        @Override
        public void close() {
            synchronized (this) {
                this.notify();
            }
        }
    
        @Override
        public RpcResponse getResult(Long uniqueIdentification, int timeoutMillis) {
            try {
                synchronized (this) {
                    this.wait(timeoutMillis);
                }
            } catch (InterruptedException ex) {
                logger.error("result is interrupted,msg:{}", ex.getMessage());
            } finally {
                ChannelManger.CALLBACK_CACHES.remove(uniqueIdentification);
            }
            return resp;
        }
    

    使用ConcurrentHashMap来存放callBack对象:

    public static final Map<Long, CallBack> CALLBACK_CACHES = new ConcurrentHashMap<>(16);
    

    调用getResult方法:

    callback.getResult(request.getUniqueIdentification(), timeoutMillis)
    

    调用该方法,会调用callBack的 wait(long timeoutMillis)方法,这里会阻塞住当前线程,等待响应结果,如果在指定时间内没有响应数据包,则超时(注:超时会接着往下走,而不是抛出InterruptedException,只有线程被打断时,才会抛出此错误),超时会将callBack从map中移除,避免没有收到数据包而导致callBack不能被GC回收,导致内存泄漏,默认超时时间是1s,可通过配置文件配置;

    收到响应数据包会去map中找到callBack,从而调用handle方法,handle方法其实就是赋值和调用notify,唤醒被阻塞的线程,注:这里有可能找不到,因为可能超时被移除,所以要做非空判断,代码如下:

    CallBack callBackService = ChannelManger.CALLBACK_CACHES.get(rpcResponse.getUniqueIdentification());
                if (callBackService != null) {
                    callBackService.handle(rpcResponse);
                } else {
                    logger.info("receive message fail");
                }
    

    处理之后就在进行业务端处理了。

项目源码

GitHub - StephenFaust/nexus: Nexus是一个简单的rpc框架,基本实现rpc的主要功能,目前须结合spring使用,其中使用Netty为通信框架,使用kryo作为默认序列化协议,通过spi来选配序列化协议、 负载均衡器、注册中心;Nexus是一个简单的rpc框架,基本实现rpc的主要功能,目前须结合spring使用,其中使用Netty为通信框架,使用kryo作为默认序列化协议,通过spi来选配序列化协议、 负载均衡器、注册中心; - GitHub - StephenMAOhjm/Nexus: Nexus是一个简单的rpc框架,基本实现rpc的主要功能,目前须结合spring使用,其中使用Netty为通信框架,使用kryo作为默认序列化协议,通过spi来选配序列化协议、 负载均衡器、注册中心;https://github.com/StephenFaust/nexus

 

  • 15
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值