看了这篇你就会手写RPC框架了

原文地址:https://leisure.wang/procedural-framework/framework/704.html

一、学习本文你能学到什么?

  • RPC的概念及运作流程
  • RPC协议及RPC框架的概念
  • Netty的基本使用
  • Java序列化及反序列化技术
  • Zookeeper的基本使用(注册中心)
  • 自定义注解实现特殊业务逻辑
  • Java的动态代理
  • 自定义Spring Boot Starter

这里只是列出了你能从RPC框架源码中能学到的东西,本文并不会每个知识点都点到,主要讲述如何手写一个RPC框架,更多细节需要读者阅读源码,文章的下方会提供源码链接哦。

二、RPC基础知识

2.1 RPC是什么?

Remote Procedure Call(RPC):远程过程调用。

过程是什么?
过程就是业务处理、计算任务,更直白理解,就是程序。(像调用本地方法一样调用远程的过程。)

RPC采用Client-Server结构,通过Request-Response消息模式实现。

2.2 RPC的流程

image-20200712085059464

  • 客户端处理过程中调用Client stub(就像调用本地方法一样),传递参数;
  • Client stub将参数编组为消息,然后通过系统调用向服务端发送消息;
  • 客户端本地操作系统将消息从客户端机器发送到服务端机器;
  • 服务端操作系统将接收到的数据包传递给Server stub;
  • Server stub解组消息为参数;
  • Server stub再调用服务端的过程,过程执行结果以反方向的相同步骤响应给客户端。

2.3 RPC流程中需要处理的问题

  • Client stub、Server stub的开发;
  • 参数如何编组为消息,以及解组消息;
  • 消息如何发送;
  • 过程结果如何表示、异常情况如何处理;
  • 如何实现安全的访问控制。

2.4 RPC协议是什么?

RPC调用过程中需要将参数编组为消息进行发送,接受方需要解组消息为参数,过程处理结果同样需要经编组、解组。消息由哪些部分构成及消息的表示形式就构成了消息协议。

RPC调用过程中采用的消息协议称为RPC协议

RPC协议规定请求、响应消息的格式

在TCP(网络传输控制协议)上可选用或自定义消息协议来完成RPC消息交互

我们可以选用通用的标准协议(如:http、https),也也可根据自身的需要定义自己的消息协议。

2.5 RPC框架是什么?

封装好参数编组、消息解组、底层网络通信的RPC程序开发框架,带来的便捷是可以直接在其基础上只需要专注于过程代码编写。

Java领域:

  • 传统的webservice框架:Apache CXF、Apache Axis2、Java自带的JAX-WS等。webservice框架大多基于标准的SOAP协议。
  • 新兴的微服务框架:Dubbo、spring cloud、Apache Thrift等。

三、手写RPC

3.1 目标

我们将会写一个简易的RPC框架,暂且叫它leisure-rpc-spring-boot-starter,通过在项目中引入该starter,并简单的配置一下,项目即拥有提供远程服务的能力。

编写自定义注解@Service,被它注解的类将会提供远程服务。

编写自定义注解@InjectService,使用它可注入远程服务。

3.2 项目整体结构

image-20200722221416777

3.3 客户端编写

3.3.1 客户端需要做什么?

客户端想要调用远程服务,必须具备服务发现的能力;在知道有哪些服务过后,还必须有服务代理来执行服务调用;客户端想要与服务端通信,必须要有相同的消息协议;客户端想要调用远程服务,那么必须具备网络请求的能力,即网络层功能。

当然,这是客户端所需的最基本的能力,其实还可以扩展的能力,例如负载均衡。

3.3.2 具体实现

我们先看看客户端的代码结构:

image-20200722230006033

基于面向接口编程的理念,不同角色都实现了定义了相应规范的接口。这里面我们没有发现消息协议相关内容,那是因为服务端也需要消息协议,因此抽离了出来,放在公共层。

3.3.2.1 服务发现者
/**
 * 服务发现抽象类,定义服务发现规范
 *
 * @author 东方雨倾
 * @since 1.0.0
 */
public interface ServiceDiscoverer {
   
   
    List<Service> getServices(String name);
}

/**
 * Zookeeper服务发现者,定义以Zookeeper为注册中心的服务发现细则
 *
 * @author 东方雨倾
 * @since 1.0.0
 */
public class ZookeeperServiceDiscoverer implements ServiceDiscoverer {
   
   

    private ZkClient zkClient;

    public ZookeeperServiceDiscoverer(String zkAddress) {
   
   
        zkClient = new ZkClient(zkAddress);
        zkClient.setZkSerializer(new ZookeeperSerializer());
    }

    /**
     * 使用Zookeeper客户端,通过服务名获取服务列表
     * 服务名格式:接口全路径
     *
     * @param name 服务名
     * @return 服务列表
     */
    @Override
    public List<Service> getServices(String name) {
   
   
        String servicePath = LeisureConstant.ZK_SERVICE_PATH + LeisureConstant.PATH_DELIMITER + name + "/service";
        List<String> children = zkClient.getChildren(servicePath);
        return Optional.ofNullable(children).orElse(new ArrayList<>()).stream().map(str -> {
   
   
            String deCh = null;
            try {
   
   
                deCh = URLDecoder.decode(str, LeisureConstant.UTF_8);
            } catch (UnsupportedEncodingException e) {
   
   
                e.printStackTrace();
            }
            return JSON.parseObject(deCh, Service.class);
        }).collect(Collectors.toList());
    }
}

服务发现者使用Zookeeper来实现,通过ZkClient我们很容易发现已经注册在ZK上的服务。当然我们也可以使用其他组件作为注册中心,例如Redis。

3.3.2.2 网络客户端
/**
 * 网络请求客户端,定义网络请求规范
 *
 * @author 东方雨倾
 * @since 1.0.0
 */
public interface NetClient {
   
   
    byte[] sendRequest(byte[] data, Service service) throws InterruptedException;
}

/**
 * Netty网络请求客户端,定义通过Netty实现网络请求的细则。
 *
 * @author 东方雨倾
 * @since 1.0.0
 */
public class NettyNetClient implements NetClient {
   
   
    private static Logger logger = LoggerFactory.getLogger(NettyNetClient.class);

    /**
     * 发送请求
     *
     * @param data    请求数据
     * @param service 服务信息
     * @return 响应数据
     * @throws InterruptedException 异常
     */
    @Override
    public byte[] sendRequest(byte[] data, Service service) throws InterruptedException {
   
   
        String[] addInfoArray = service.getAddress().split(":");
        String serverAddress = addInfoArray[0];
        String serverPort = addInfoArray[1];

        SendHandler sendHandler = new SendHandler(data);
        byte[] respData;
        // 配置客户端
        EventLoopGroup group = new NioEventLoopGroup();
        try {
   
   
            Bootstrap b = new Bootstrap();
            b.group(group).channel(NioSocketChannel.class).option(ChannelOption.TCP_NODELAY, true)
                    .handler(new ChannelInitializer<SocketChannel>() {
   
   
                        @Override
                        public void initChannel(SocketChannel ch) {
   
   
                            ChannelPipeline p = ch.pipeline();
                            p.addLast(sendHandler);
                        }
                    });

            // 启动客户端连接
            b.connect(serverAddress, Integer.parseInt(serverPort)).sync();
            respData = (byte[]) sendHandler.rspData();
            logger.info("SendRequest get reply: {}", respData);
        } finally {
   
   
            // 释放线程组资源
            group.shutdownGracefully();
        }

        return respData;
    }
}

/**
 * 发送处理类,定义Netty入站处理细则
 *
 * @author 东方雨倾
 * @since 1.0.0
 */
public class SendHandler extends ChannelInboundHandlerAdapter {
   
   
    private static Logger logger = LoggerFactory.getLogger(SendHandler.class);

    private CountDownLatch cdl;
    private Object readMsg = null;
    private byte[] data;

    public SendHandler(byte[] data) {
   
   
        cdl = new CountDownLatch(1);
        this.data = data;
    }

    /**
     * 当连接服务端成功后,发送请求数据
     *
     * @param ctx 通道上下文
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
   
   
        logger.info("Successful connection to server:{}", ctx);
        ByteBuf reqBuf = Unpooled.buffer(data.length);
        reqBuf.writeBytes(data);
        logger.info("Client sends message:{}", reqBuf);
        ctx.writeAndFlush(reqBuf);
    }

    /**
     * 读取数据,数据读取完毕释放CD锁
     *
     * @param ctx 上下文
     * @param msg ByteBuf
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
   
   
        logger.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值