从零开始手写RPC框架


前言

学习了netty和zookeeper后一直想写一个简单的RPC框架,网上的代码大多不符合我的要求,又或者技术太复杂。在此记录一下从头手写RPC框架的全过程


一、项目实现功能

实现多个服务之间的远程过程调用,使用起来非常简单。

二、使用步骤

1.引入库

Maven依赖如下(示例,最新版本请到Maven仓库获取):

<dependency>
            <groupId>io.github.baijianruoli</groupId>
            <artifactId>lidou</artifactId>
            <version>1.3.0</version>
</dependency>

2.配置文件

spring boot配置文件(示例):

lidou.port=9997   //netty端口
lidou.servicePackage=com.zut.lpf.service   //扫描@LidouService的包路径
lidou.zookeeper.url=192.168.247.129:2181   //zookeeper的url

3.代码如何使用

package com.zut.lpf.service.impl;

import com.zut.lpf.service.HelloService;

import io.github.baijianruoli.lidou.annotation.LidouService;
//使用@LidouService 标记要加入远程过程调用的Service
@LidouService  
public class HelloServiceImpl implements HelloService {

    @Override
    public int hello(int sum) {
            return sum*3;
    }
}
@RestController
public class HelloController {
    
    //使用@Reference注入Service接口的动态代理
    @Reference(loadBalance = "random")
    private HelloService helloService;
    @RequestMapping("/hello")
    public int hello(int sum)
    {
        long time1 = System.currentTimeMillis();
        int hello = helloService.hello(sum);
        long time2 = System.currentTimeMillis();
        System.out.println(time2-time1);
        return hello;
    }
}

三.框架实现

1.服务的信息保存

首先为了保存多个服务的信息,这里采用zookeeper。服务启动后会扫描lidou.servicePackage下所有@LidouService的类,加入到zookeeper中。
zookeeper目录结构如下:
在这里插入图片描述如何扫描自定义的@LidouService注解标记的类呢?

这里主要实现的方法是实现spring的后置工厂(BeanFactoryPostProcessor)以及继承类扫描器(ClassPathBeanDefinitionScanner)

在BeanFactoryPostProcessor中
postProcessBeanFactory可以拿到未初始化的bean工厂,这里我们重新扫描自定义包路径下的类,加入到ConfigurableListableBeanFactory 里

 @Override
    public  void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
        AnnotationScanner scanner = AnnotationScanner.getScanner((BeanDefinitionRegistry) configurableListableBeanFactory, LidouService.class);
        String property = applicationContext.getEnvironment().getProperty("lidou.servicePackage");
        if(property==null)
            property="io.github.baijianruoli";
        scanner.setResourceLoader(applicationContext);
        int count=scanner.scan(property);
    }

在ClassPathBeanDefinitionScanner中
doScan方法为我们自定义的扫描路径,registerDefaultFilters方法为过滤含有特定注解的类,由此可以把所有@LidouService标记的类加入到spring工厂里。(很多框架都是用这种方式扫描路径的,例如mybatis)

@Override
    public void registerDefaultFilters() {
        // 添加需扫描的Annotation Class
        this.addIncludeFilter(new AnnotationTypeFilter(staticTempAnnotationClazz));
    }

    // 以下为初始化后调用的方法
    @Override
    public Set<BeanDefinitionHolder> doScan(String... basePackages) {
        return super.doScan(basePackages);
    }

    @Override
    public boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
        return super.isCandidateComponent(beanDefinition)
                && beanDefinition.getMetadata().hasAnnotation(this.selfAnnotationClazz.getName());
    }

在这里插入图片描述

2.服务之间的通信

zookeeper有了各个服务的service信息,怎么相互之间通信呢?
这里在spring boot启动时使用高性能的网络框架netty并且采用protobuf传输数据,提高了传输速度。

NioEventLoopGroup bossGroup=new NioEventLoopGroup(1);
NioEventLoopGroup groupGroup=new NioEventLoopGroup(4);
ServerBootstrap serverBootstrap=new ServerBootstrap();
serverBootstrap.group(bossGroup,groupGroup).channel(NioServerSocketChannel.class)
        .childHandler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel socketChannel) throws Exception {
                ChannelPipeline pipeline = socketChannel.pipeline();
                pipeline.addLast(new ServerDecode());
                pipeline.addLast(new ServerEncode());
                pipeline.addLast(new IdleStateHandler(100,100,100, TimeUnit.SECONDS));
                pipeline.addLast(new ServerHandler());
            }
        });
InetAddress address = InetAddress.getLocalHost();
String hostAddress = address.getHostAddress();
try{
    ChannelFuture future= serverBootstrap.bind(hostAddress, port).sync();
    log.info("服务端启动");
    future.channel().closeFuture();

客户端请求服务端的参数

@Data
@NoArgsConstructor
public class BaseRequest {
    private String className;  //类全路径
    private String methodName;  //执行的方法名
    private Object[] parameters;  //参数
    private Class<?>[] parameTypes;  //参数类型
    public BaseRequest(String className) {
        this.className = className;
    }

    public BaseRequest(String className, String methodName, Object[] parameters, Class<?>[] parameTypes) {
        this.className = className;
        this.methodName = methodName;
        this.parameters = parameters;
        this.parameTypes = parameTypes;
    }
}

netty服务端接收执行,返回结果到客户端

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    BaseRequest request = (BaseRequest) msg;
    if("heart".equals(request.getClassName()))
        return ;
    String className = request.getClassName();
    String methodName = request.getMethodName();
    Class<?>[] parameTypes = request.getParameTypes();
    Object[] parameters = request.getParameters();
    Object o = InitRpcConfig.rpcServiceMap.get(className);
    Object message=null;
    try{
        Method declaredMethod = o.getClass().getDeclaredMethod(methodName, parameTypes);
        Object invoke = declaredMethod.invoke(o, parameters);//执行实现类
        message=invoke;
    }catch (NoSuchMethodException e)
    {
        log.info("bean实例化未找到");
        ctx.writeAndFlush(new BaseResponse<String>(StatusCode.Fail,e.getMessage()));
    }
    ctx.writeAndFlush(new BaseResponse(StatusCode.Success,message));//把结果写回客户端
}

3.动态代理

服务信息已经保存,服务通信也可以使用,最后还缺少如何在客户端调用接口执行的是服务端的实现类。对每个被@Reference标记的属性,使用jdk动态代理注入代理对象,完成最后一步。

public Object getBean(final Class<?> serviceClass, final Object o,String mode) {
    return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{serviceClass}, (proxy, method, args) -> {
        BaseRequest baseRequest = new BaseRequest((String)o, method.getName(), args, method.getParameterTypes());
        //负载均衡
        //获得zookeeper路径
        String url;
        String port;
        String path= PathUtils.addZkPath(serviceClass.getName());
        List<String> children = zkClient.getChildren(path);
        //负载均衡
        String tmp = loadBalanceService.loadBalance(path, children, mode);
        String[] split = tmp.split(":");
        url=split[0];
        port=split[1];
        NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
        ClientHandler clientHandler = new ClientHandler();
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(bossGroup).channel(NioSocketChannel.class).option(ChannelOption.TCP_NODELAY, true).handler(new ChannelInitializer<SocketChannel>() {
            protected void initChannel(SocketChannel socketChannel) throws Exception {
                ChannelPipeline pipeline = socketChannel.pipeline();
                pipeline.addLast(new ClientEncode());
                pipeline.addLast(new ClientDecode());
                pipeline.addLast(new IdleStateHandler(80L, 80L, 80L, TimeUnit.SECONDS));
                pipeline.addLast(clientHandler);
            }
        });
        ChannelFuture future1 = bootstrap.connect(url, Integer.valueOf(port)).sync();
        clientHandler.setPars(baseRequest);
        Object result = executor.submit(clientHandler).get();
        future1.channel().closeFuture();
        bossGroup.shutdownGracefully();
        return result;
    });
}

最后返回服务端的结果,就像调用本地方法一样,因为又采用了负载均衡,大大提高了并发量。

4.最后

Github地址(如果还没有理解可以去github查看源码,也欢迎提交pr)
联系QQ:523892377

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值