自己实现一个RPC框架
Posted 编程那些烦心事
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自己实现一个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框架
借此实现的机会,自己又学习实践了包括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> 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。
文章来源 | yuque.com/xavior.wx/point/rpc-practice
Hi
感谢你的到来
我不想错过你
编程那些烦心事
以上是关于自己实现一个RPC框架的主要内容,如果未能解决你的问题,请参考以下文章