自己实现一个RPC框架

Posted 编程那些烦心事

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自己实现一个RPC框架相关的知识,希望对你有一定的参考价值。


不点蓝字,我们哪来故事?



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

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

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

  • 我的团队开发一个新服务,别的团队可能会调用。


RPC调用的变与不变

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

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

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

  • 角色的定义(发起调用的是客户端,接受调用的是服务端)

  • 通信的机制(网络IO,序列化,传输协议,同步异步)

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

  • 调用的目标。服务透明化,目标是让用户像以本地调用方式调用远程服务。

  • 调用的方式。服务端的服务要被调用,客户端在本地直接调用服务端提供的接口即可,而不需要调用真实的接口实现。于是服务端就是需要利用一些很多反射操作去完成。


RPC需要什么

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

  • 网络IO,BIONIOAIO,Socket编程,HTTP通信,一个就行。

  • 序列化,JDK序列化,JSON、Hessian、Kryo、ProtoBuffer、ProtoStuff、Fst知道一个就行。

  • 反射,JDK或者Cglib的动态代理。


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

  • 多个实例,选哪个调用好?负载均衡

  • 服务注册中心每次都查?缓存相关

  • 客户端每次要等服务器返回结果?异步调用

  • 服务是要升级的?版本控制

  • 多个服务依赖,某个有问题?熔断器

  • 某个服务出了问题怎么办?监控 ...


Dubbo

其实要考虑的问题是非常多的,瞻仰一下Dubbo的流程图和Dubbo团队对未来的规划图:自己实现一个RPC框架

自己实现一个RPC框架


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

自己实现一个RPC框架

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


整体调用流程

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

  • Server端启动进行服务注册到etcd;

  • Client端启动获取etcd的服务注册信息,定期更新;

  • Client以本地调用方式调用服务(使用接口,例如helloService.sayHi("world"));

  • Client通过RpcProxy会使用对应的服务名生成动态代理相关类,而动态代理类会将请求的对象中的方法、参数等组装成能够进行网络传输的消息体RpcRequest;

  • Client通过一些的负载均衡方式确定向某台Server发送编码(RpcEncoder)过后的请求(netty实现)

  • Server收到请求进行解码(RpcDecoder),通过反射(cglib的FastMethod实现)会进行本地的服务执行

  • Server端writeAndFlush()将RpcResponse返回;

  • Clinet将返回的结果会进行解码,得到最终结果。


Netty学习

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


Netty重要的几个概念

  • Channel:这并不是Netty专有的概念,Java NIO里也有。可以看作是入站或者出战数据的载体,有各种基本的read、write、connect、bind等方法,相当于传统IO的Socket,需要关注一下ServerChannel,ServerChannel负责创建子Channel,子Channel具体去执行一些具体accept之后的读写操作。项目中用的NiosocketChannel和NioServerSocketChannel。

  • EventLoop和EventLoopGroup:Netty的核心抽象,channel的整个生命周期都是通过EventLoop去处理。EventLoop相当于对Thread的封装,一个EventLoop里面拥有一个永远都不会改变的Thread,同时任务的提交只需要通过EventLoop就可执行;而EventLoopGroup负责为每个Channel分配一个EventLoop/

  • ChannelFuture:Netty所有的IO操作都是异步的原因。

  • ChannelHandler和ChannelPipeline:开发人员主要关注的也可能是唯一需要关注的两个组件,用来管理数据流以及执行应用程序处理逻辑。

  • ChannelInboundHandler和ChannelOutboundHandler:两个常见的ChannelHandler适配器,前者管理入站的数据和操作,后者管理出站的数据和操作,谨记:入站顺序执行,出站逆序执行。

  • ChannelPipeline:一个拦截流经某个channel的入站和出站时间的ChannelHandle实例链,每一个Channel刚被创建就会被分配一个ChannelPipeline,永久不可更改。

  • ChannelHandlerContext:ChannelHandle和ChannelPipeline中间管理的纽带,每一个ChannelHandler分配一个ChannelHandlerContext用来跟其他Handler作交互。

  • ByteBuf:网络数据的基本单位是字节,Java NIO使用的ByteBuffer作为字节容器,而Netty使用ByteBuf替代ByteBuffer作为数据容器进行读写。

  • BootStrap:将各种组件拼图进行组装,ServerBootstrap用来引导服务端,Bootstrap用来引导客户端。ServerBootstrap的Group一般会放入两个EventLoopGroup,需要结合Channel去理解,ServerChannel会有子Channel,那为了处理这个Channel,你需要为每一个子Channel分配一个EventLoop,第二个EventLoopGroup是为了让子Channel去共享一个EventLoop,避免额外的线程创建以及上下文切换。

  • ByteToMessageDecoder和MessageToByteEncoder:编解码器的解码器和编码器,MessageToByteEncoder继承了ChannelOutboundHandlerAdapter接口,ByteToMessageDecoder继承了ChannelInboundHandlerAdapter接口。解码器是将字节解码为消息;编码器是将消息编码成字节。


Netty学习的其他问题

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


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


3.ChannelPipeline执行顺序?

  • InboundHandler顺序执行,OutboundHandler逆序执行

  • InboundHandler之间传递数据,通过ctx.fireChannelRead(msg)

  • InboundHandler通过ctx.write(msg),则会传递到outboundHandler

  • 使用ctx.write(msg)传递消息,Inbound需要放在结尾,在Outbound之后,不然outboundhandler会不执行;但是使用channel.write(msg)、pipline.write(msg)情况会不一致,都会执行,那是因为channel和pipline会贯穿整个流。

  • outBound和Inbound谁先执行,针对客户端和服务端而言,客户端是发起请求再接受数据,先outbound(写)再inbound(读),服务端则相反。

  • outBound可以理解为数据“出航”,inBound可以理解为“归航”,所以请求从客户端到服务端就意味着,请求数据从客户端出航,在服务端归航,服务端响应请求是从服务端到客户端的,所以就是响应数据从服务端出航,在客户端归航。


4.三种最常见的ChannelHandle的子类型?

  • 基于 ByteToMessageDecoder,我们可以实现自定义解码,而不用关心 ByteBuf 的强转和 解码结果的传递。

  • 基于 SimpleChannelInboundHandler,这主要针对的最常见的一种情况,你去接收一种(泛型)解码信息,然后对数据应用业务逻辑然后继续传下去。我们可以实现每一种指令的处理,通过泛型不再需要强转,不再有冗长乏味的 if else 逻辑,不需要手动传递对象。

  • 基于 MessageToByteEncoder,我们可以实现自定义编码,而不用关心 ByteBuf 的创建,不用每次向对端写 Java 对象都进行一次编码。


5.Netty关于拆包粘包理论与解决方案?

本次使用的是LengthFieldBasedFrameDecoder。

  • 固定长度的拆包器 FixedLengthFrameDecoder 如果你的应用层协议非常简单,每个数据包的长度都是固定的,比如 100,那么只需要把这个拆包器加到 pipeline 中,Netty 会把一个个长度为 100 的数据包 (ByteBuf) 传递到下一个 channelHandler。

  • 行拆包器 LineBasedFrameDecoder 从字面意思来看,发送端发送数据包的时候,每个数据包之间以换行符作为分隔,接收端通过 LineBasedFrameDecoder 将粘过的 ByteBuf 拆分成一个个完整的应用层数据包。

  • 分隔符拆包器 DelimiterBasedFrameDecoder DelimiterBasedFrameDecoder 是行拆包器的通用版本,只不过我们可以自定义分隔符。

  • 基于长度域拆包器 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> 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> create(Class<T> interfaceClass) {  
        return (T) Proxy.newProxyInstance(  
                interfaceClass.getClassLoader(),  
                new Class<?>[]{interfaceClass},  
                new RpcInvocationHandler<>(interfaceClass)  
        );  
    }  


序列化实现

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


自己实现一个RPC框架


文章来源 | yuque.com/xavior.wx/point/rpc-practice

自己实现一个RPC框架

Hi

感谢你的到来

我不想错过你

编程那些烦心事

自己实现一个RPC框架
自己实现一个RPC框架





精彩推荐




喜欢就点个在看再走吧

以上是关于自己实现一个RPC框架的主要内容,如果未能解决你的问题,请参考以下文章

手写模拟Dubbo实现一个自己的RPC框架

自己动手从0开始实现一个分布式RPC框架

自己实现一个RPC框架

自己实现一个RPC框架

自己动手从0开始实现一个分布式RPC框架

一文带你实现RPC框架