RPC实现以及相关学习

我们即希望能够敏捷开发,不做重复的劳动,用别人的势能赋能自己;又要成为一名能够赋能别人的人,拥有自身的势能。

在一个拥有成千上万大大小小的服务的公司里,每个团队在不同的机器上部署它们自己的服务,所以真实开发一个新服务的场景一定需要考虑两个问题:

  1. 我的团队开发一个新服务,可能需要调用别人的服务。
  2. 我的团队开发一个新服务,别的团队可能会调用。

调用的变与不变

由于服务部署在不同机器,想要进行服务间的调用必须进行网络通信,那服务消费方每调用一个服务都要写一大堆网络通信的东西,不仅复杂而且极易出错。

我们知道此时我们的技术选型时很丰富的,关于各种技术的优缺点网上很多文章,可以去编乎的相关问题去看看,我觉得概括的比较好一句话是良好的RPC调用是面向服务的封装,针对的是服务的可用性和效率,减轻网络服务开发和调用的复杂性

但我们不管选择何种进程间通信手段,http,TCP通信或是消息中间件、RPC通信,调用本身很多东西是不可能变的:

  1. 角色的定义(发起调用的是客户端,接受调用的是服务端)
  2. 通信的机制(网络IO,序列化,传输协议,同步异步)

那真正变的是什么?这些都是你遇到的场景以及你的目标导致的,对于RPC来说,主要来说和其他相比比较大的变化在于下面两条吧。

  1. 调用的目标。服务透明化,目标是让用户像以本地调用方式调用远程服务。
  2. 调用的方式。服务端的服务要被调用,客户端在本地直接调用服务端提供的接口即可,而不需要调用真实的接口实现。于是服务端就是需要利用一些很多反射操作去完成。

RPC需要什么

想要实现一个基本的RPC框架,其实需要什么?

  1. 网络IO,BIO\NIO\AIO,Socket编程,HTTP通信,一个就行。
  2. 序列化,JDK序列化,JSON、Hessian、Kryo、ProtoBuffer、ProtoStuff、Fst知道一个就行。
  3. 反射,JDK或者Cglib的动态代理。

那一个优秀的RPC框架,还需要考虑什么问题?

  1. 一个服务可能有多个实例,你在调用时,要如何获取这些实例的地址?服务注册中心
  2. 多个实例,选哪个调用好?负载均衡
  3. 服务注册中心每次都查?缓存相关
  4. 客户端每次要等服务器返回结果?异步调用
  5. 服务是要升级的?版本控制
  6. 多个服务依赖,某个有问题?熔断器
  7. 某个服务出了问题怎么办?监控
    ...

Dubbo

其实要考虑的问题是非常多的,瞻仰一下Dubbo的流程图Dubbo团队对未来的规划图

自己实现的一个简单RPC框架

看了很多网上的博客实现的,最后自己实现的地址:https://github.com/1000-7/xinrpc
在看阿里技术大学的HSF视频课的时候,视频的讲师说想要理解RPC就把下面这张图理解清楚就够了,这张图也是HSF官方文档中介绍一次调用流程使用的图。

借此实现的机会,自己又学习实践了包括Netty、Java反射、序列化、java注解、SpringBoot等很多方面的知识。

整体调用流程

由于采用了etcd做服务注册中心,所以整体调用流程可以被概括为下面这样:

  1. Server端启动进行服务注册到etcd;
  2. Client端启动获取etcd的服务注册信息,定期更新;
  3. Client以本地调用方式调用服务(使用接口,例如helloService.sayHi("world"));
  4. Client通过RpcProxy会使用对应的服务名生成动态代理相关类,而动态代理类会将请求的对象中的方法、参数等组装成能够进行网络传输的消息体RpcRequest;
  5. Client通过一些的负载均衡方式确定向某台Server发送编码(RpcEncoder)过后的请求(netty实现)
  6. Server收到请求进行解码(RpcDecoder),通过反射(cglib的FastMethod实现)会进行本地的服务执行
  7. Server端writeAndFlush()将RpcResponse返回;
  8. Clinet将返回的结果会进行解码,得到最终结果。

Netty学习

Netty是一款异步的时间去懂的网络应用程序框架,支持快速地开发可维护的高性能的面向协议的服务器和客户端。

Netty重要的几个概念

  1. Channel:这并不是Netty专有的概念,Java NIO里也有。可以看作是入站或者出战数据的载体,有各种基本的read、write、connect、bind等方法,相当于传统IO的Socket,需要关注一下ServerChannel,ServerChannel负责创建子Channel,子Channel具体去执行一些具体accept之后的读写操作。项目中用的NioSocketChannel和NioServerSocketChannel。
  2. EventLoop和EventLoopGroup:Netty的核心抽象,channel的整个生命周期都是通过EventLoop去处理。EventLoop相当于对Thread的封装,一个EventLoop里面拥有一个永远都不会改变的Thread,同时任务的提交只需要通过EventLoop就可执行;而EventLoopGroup负责为每个Channel分配一个EventLoop/
  3. ChannelFuture:Netty所有的IO操作都是异步的原因。
  4. ChannelHandler和ChannelPipeline:开发人员主要关注的也可能是唯一需要关注的两个组件,用来管理数据流以及执行应用程序处理逻辑。
  5. ChannelInboundHandler和ChannelOutboundHandler:两个常见的ChannelHandler适配器,前者管理入站的数据和操作,后者管理出站的数据和操作,谨记:入站顺序执行,出站逆序执行。
  6. ChannelPipeline:一个拦截流经某个channel的入站和出站时间的ChannelHandle实例链,每一个Channel刚被创建就会被分配一个ChannelPipeline,永久不可更改。
  7. ChannelHandlerContext:ChannelHandle和ChannelPipeline中间管理的纽带,每一个ChannelHandler分配一个ChannelHandlerContext用来跟其他Handler作交互。
  8. ByteBuf:网络数据的基本单位是字节,Java NIO使用的ByteBuffer作为字节容器,而Netty使用ByteBuf替代ByteBuffer作为数据容器进行读写。
  9. BootStrap:将各种组件拼图进行组装,ServerBootstrap用来引导服务端,Bootstrap用来引导客户端。ServerBootstrap的Group一般会放入两个EventLoopGroup,需要结合Channel去理解,ServerChannel会有子Channel,那为了处理这个Channel,你需要为每一个子Channel分配一个EventLoop,第二个EventLoopGroup是为了让子Channel去共享一个EventLoop,避免额外的线程创建以及上下文切换。
  10. ByteToMessageDecoder和MessageToByteEncoder:编解码器的解码器和编码器,MessageToByteEncoder继承了ChannelOutboundHandlerAdapter接口,ByteToMessageDecoder继承了ChannelInboundHandlerAdapter接口。解码器是将字节解码为消息;编码器是将消息编码成字节。

netty学习的其他问题

1.序列化和编码都是把 Java 对象封装成二进制数据的过程,这两者有什么区别和联系?
序列化是把内容变成计算机可传输的资源,而编码则是让程序认识这份资源。

2.与服务端启动相比,客户端启动的引导类少了哪些方法,为什么不需要这些方法?
服务端:需要两个线程组,NioServerSocketChannel线程模型,可以设置childHandle
客户端:一个线程组,NioSocketChannel线程模型,只可以设置handler

3.ChannelPipeline执行顺序?
(1)InboundHandler顺序执行,OutboundHandler逆序执行
(2)InboundHandler之间传递数据,通过ctx.fireChannelRead(msg)
(3)InboundHandler通过ctx.write(msg),则会传递到outboundHandler
(4) 使用ctx.write(msg)传递消息,Inbound需要放在结尾,在Outbound之后,不然outboundhandler会不执行;但是使用channel.write(msg)、pipline.write(msg)情况会不一致,都会执行,那是因为channel和pipline会贯穿整个流。
(5) outBound和Inbound谁先执行,针对客户端和服务端而言,客户端是发起请求再接受数据,先outbound(写)再inbound(读),服务端则相反。

4.三种最常见的ChannelHandle的子类型?
a. 基于 ByteToMessageDecoder,我们可以实现自定义解码,而不用关心 ByteBuf 的强转和 解码结果的传递。
b. 基于 SimpleChannelInboundHandler,这主要针对的最常见的一种情况,你去接收一种(泛型)解码信息,然后对数据应用业务逻辑然后继续传下去。我们可以实现每一种指令的处理,通过泛型不再需要强转,不再有冗长乏味的 if else 逻辑,不需要手动传递对象。
c. 基于 MessageToByteEncoder,我们可以实现自定义编码,而不用关心 ByteBuf 的创建,不用每次向对端写 Java 对象都进行一次编码。

5.Netty关于拆包粘包理论与解决方案?本次使用的是LengthFieldBasedFrameDecoder。

a.固定长度的拆包器 FixedLengthFrameDecoder
如果你的应用层协议非常简单,每个数据包的长度都是固定的,比如 100,那么只需要把这个拆包器加到 pipeline 中,Netty 会把一个个长度为 100 的数据包 (ByteBuf) 传递到下一个 channelHandler。
b.行拆包器 LineBasedFrameDecoder
从字面意思来看,发送端发送数据包的时候,每个数据包之间以换行符作为分隔,接收端通过 LineBasedFrameDecoder 将粘过的 ByteBuf 拆分成一个个完整的应用层数据包。
c.分隔符拆包器 DelimiterBasedFrameDecoder
DelimiterBasedFrameDecoder 是行拆包器的通用版本,只不过我们可以自定义分隔符。
d.基于长度域拆包器 LengthFieldBasedFrameDecoder
最后一种拆包器是最通用的一种拆包器,只要你的自定义协议中包含长度域字段,均可以使用这个拆包器来实现应用层拆包。由于上面三种拆包器比较简单,读者可以自行写出 demo,接下来,我们就结合我们小册的自定义协议,来学习一下如何使用基于长度域的拆包器来拆解我们的数据包。

CGLib学习

反射和动态代理

反射机制是Java语言提供的一种基础功能,赋予程序在运行时 自省 (introspect,官方用语)的能力。通过反射我们可以直接操作类或者对象,比如获取某个对象的类定义,获取类声明的属性和方法,调用方法或者构造对象,甚至可以运行时修改类定义。

动态代理是一种方便运行时动态构建代理、动态处理代理方法调用的机制,很多场景都是利用类似机制做到的,比如用来包装 RPC 调用、面向切面的编程(AOP)。
实现动态代理的方式很多,比如 JDK 自身提供的动态代理,就是主要利用了上面提到的反射机制。还有其他的实现方式,比如利用传说中更高性能的字节码操作机制,类似 ASM、cglib(基于 ASM)等。

总结:反射是java的一种能力,而动态代理是一种解决问题的方案。
动态代理是一种代理模式。代理可以看作是对调用目标的一个包装,这样我们对目标代码的调用不是直接发生的,而是通过代理完成。通过代理可以让调用者与实现者之间解耦 。

CGLib实现反射

    FastClass fastClass = FastClass.create(serviceClass);
    FastMethod fastMethod = fastClass.getMethod(methodName, parameterTypes);
    return fastMethod.invoke(serviceBean, parameters);

CGLib实现动态代理

实现MethodInterceptor接口,然后使用Enhancer构建

   public static <T> T createByCglib(Class<T> clazz) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(clazz);
        enhancer.setCallback(new RpcMethodInterceptor(clazz));
        return (T) enhancer.create();
    }

JDK实现动态代理

实现InvocationHandler接口,然后使用Proxy创建

   public static <T> T create(Class<T> interfaceClass) {
        return (T) Proxy.newProxyInstance(
                interfaceClass.getClassLoader(),
                new Class<?>[]{interfaceClass},
                new RpcInvocationHandler<>(interfaceClass)
        );
    }

序列化实现

序列化有多种实现方式,不同序列化优缺点不同,网上有很多比较天梯图
我实现了五种,JSON,FST,HESSIAN2,PROTO_STUFF,KRYO。

发表评论

电子邮件地址不会被公开。