手撸RPC----异步、代理模式、pipeline

一、异步

目前消费端已经可以获取一个可用的连接地址,接下来要做的就是建立消费端和服务提供方的之间的长连接,并且进行通信。但因为netty在整个通信的过程中是异步的,所以我们会使用CompletableFuture来获取异步的结果。

关于CompletableFuture的用法有一个博主的文章介绍的很好,我们可以看看:https://zhuanlan.zhihu.com/p/344431341。

展示了一个从创建请求到得到响应的整个流程安装。

还有一个图,你们觉得能看懂哪个算哪个?哈哈

在以上的过程中,请求发送出去,即一旦调用了writeAndFlush方法,其中网络通信、方法调用等一系列的工作就都和当前线程无关了,我们只能使用CompletableFuture.get()方法等待结果。

为啥要使用CompletableFuture等待结果,才能进行异步操作?

使用CompletableFuture等待Future的结果是为了能够获取异步操作的结果或异常。CompletableFuture是Java 8中引入的一种新型异步编程工具,它可以支持非阻塞式、并发执行的异步操作。

在Java中,异步操作通常使用线程池来执行,以避免阻塞主线程。但是,线程池中的线程并不一定立即返回结果,而是异步地执行计算,并在计算完成后将结果设置给Future对象。

这时候,如果我们需要获取异步操作的结果,就可以使用Future.get()方法来等待结果。但是,这个方法是一个阻塞方法,会一直阻塞当前线程,直到异步操作完成并返回结果或异常。

为了避免这种阻塞,我们可以使用CompletableFuture类来创建一个带有回调函数的Future对象,当异步操作完成后,会自动触发回调函数,从而避免了等待阻塞。

CompletableFuture还提供了若干方法用于组合多个异步操作的结果,如thenCompose()、thenCombine()、allOf()和anyOf()方法等,这些方法可以帮助我们更加方便地进行复杂的异步编程。

因此,使用CompletableFuture等待Future的结果是为了获取异步操作的结果或异常,以及避免阻塞主线程。CompletableFuture提供了一种非阻塞式、并发执行的异步编程方式,使得我们可以更加高效地进行异步操作。

二、代理模式

rpc 是用来解决两个应用之间的通信,而网络则是两台机器之间的“桥梁”,只有架好了桥梁,我们才能把请求数据从一端传输另外一端。其实关于网络通信,你只要记住一个关键字就行了——可靠的传输。

对于服务端和客户端,他们做的事情都很确定:

服务端:暴露接口,等待客户端的远程访问,执行方法,返回结果。

客户端:引入接口,实现接口,在实现中编写网络请求代码和结果处理代码。

我们一定会发现,对于客户端而言,其中涉及的过程如 封装请求、选择通道、等待响应等功能(事实上,这个方法的功能远不止于此,后期我们还会开发异常重试、熔断保护、负载均衡等)都是一样的,我们不可能为每一个方法调用都编写相同的逻辑。仔细思考,这是不是在给方法调用做增强谈及增强我们可能第一时间想起了代理模式和装饰器模式。

目前是既没有被代理对象,也没有被装饰的类,有的只是一个孤零零的接口。这种情况无论是静态代理、还是装饰器都没有用武之地,只有动态代理可以大展身手,可以在运行期凭空捏造生成一个代理对象。

设计模式中生成代理对象代码如下

public T get() {
    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
    Class<T>[] classes = new Class[]{interfaceRef};
    InvocationHandler handler = new RpcConsumerInvocationHandler(registry,interfaceRef,group);

    // 使用动态代理生成代理对象
    Object helloProxy = Proxy.newProxyInstance(classLoader, classes, handler);

    return (T) helloProxy;
}

本质上调用代理对象的方法会最终落实到RpcConsumerInvocationHandler的invoke方法,并且会将方法对象、参数列表传入:

public class RpcConsumerInvocationHandler implements InvocationHandler {
    
    // 此处需要一个注册中心,和一个接口
    private final Registry registry;
    private final Class<?> interfaceRef;
    
    public RpcConsumerInvocationHandler(Registry registry, Class<?> interfaceRef) {
        this.registry = registry;
        this.interfaceRef = interfaceRef;
    }
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 1、利用方法、参数列表封装请求负载
        // 2、封装请求对象
        // 3、选取一个服务提供方
        // 4、建立连接
        // 5、写出请求
        // 6、等待响应(网络通信和方法调用是异步的)
        // 7、获得结果返回  
    }   
}

三、netty的pipeline

在netty中请求的处理都是使用的IO多路复用,同时他提供了非常友好的请求处理方式就是pipeline(流水线)。他提供了基本的入站和出站的能力,并且抽象了两个接口ChannelInboundHandler(入栈处理器)和ChannelOutboundHandler(出栈处理器),当然他们共同继承自ChannelHandler接口,同时为我们实现了大量的通用的入站和出站处理器。其图如下:

当我们在消费端调用writeAndFlush方法时,网络通信就开始了,大致的流程如下:

1、对于消费方,开始做出站的工作,中间会经历多个出站处理器,主要的核心逻辑是将请求对象封装成报文

2、消息经过消费方的出站处理程序后就变成了二进制字节流报文,就会进入服务提供方,开始进入提供方的入站逻辑,核心就是解析请求报文

3、得到请求的之后,提供方根据请求携带的负载选定合适的对象和方法进行方法调用,得到结果。

4、调用方开始封装响应,并调用writeAndFlush将响应写出,进入提供方的出站逻辑,主要就是封装响应报文

5、调用方接受响应,进入入站逻辑,解析响应,得到结果

下图可以清晰的表达出整个流程:

服务端的pipeline如下,这里我们通过addLast方法添加了四个出入站处理程序:

serverBootstrap = serverBootstrap.group(boss, worker)
    .channel(NioServerSocketChannel.class)
    .childHandler(new ChannelInitializer<SocketChannel>() {
        @Override
        protected void initChannel(SocketChannel socketChannel) throws Exception {
            // 是核心,我们需要添加很多入站和出站的handler
            socketChannel.pipeline()
                .addLast(new LoggingHandler())
                .addLast(new YrpcRequestDecoder())
                // 根据请求进行方法调用
                .addLast(new MethodCallHandler())
                .addLast(new YrpcResponseEncoder());
        }
    });

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值