一、异步
目前消费端已经可以获取一个可用的连接地址,接下来要做的就是建立消费端和服务提供方的之间的长连接,并且进行通信。但因为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());
}
});