为了带你们搞懂RPC,我手写了一个RPC框架

      最后更新:2022-07-06 00:33:28 手机定位技术交流文章

      如今,分布式系统正在进行。RPC 发挥 了 重要 作用 。流行的Duboo,Thrift,gRpc和其他颈部和颈部的框架。深入了解RPC对于初学者和老鸟来说是必不可少的。你知道RPC的实施原则吗?你想用手实现一个简单的RPC框架 吗?本文将带你穿过一个RPC项目,找到答案,大量代码展示,干货满满,如果你能再次钻进项目代码,我们相信您可以获得包括开放源代码RPC原则、Java基础(注释、反射、同步器、未来、SPI、动态代理)、Java代码增强、服务注册和发现、Netty网络通信、传输协议、序列、包压缩、TCP粘贴/拆卸、长期连接重用、心脏跳动检测、SpringBoot自动装载、服务分组、接口版本、客户端连接池、负载平衡、异步调用等关键知识。

      RPC定义

      远程程序调用的概念有着悠久的历史.它已经于1981年提出,最初的目的是像调用本地方法一样简单地调用远程方法,经过四十多年的更新和迭代,RPC的总体思维变得更加稳定,今天,数以百计的 RPC 协议和框架,杜博(Ali)、FaceBook(FaceBook)、gRpc(Google)、brpc(Baidu)等都集中在不同的方面来解决原来的目标,有的想极致完美,有的追求极致性能,有的偏向极致简单。

      RPC原理

      让我们回到RPC的最初目的。为了实现像本地调用方法一样简单的远程调用方法,至少必须解决以下问题:

      1. 如何获取可用的远程服务器

      2. 如何表示数据

      3. 如何传递数据

      4. 如何在服务端确定和调用目标方法

      上述四点问题,可与当前分布式系统的加热术语相匹配,如何获取可用的远程服务器(服务注册与发现)、如何表示数据(序列化与反序列化)、如何传递数据(网络通讯)、如何在服务端确定和调用目标方法(调用方法映射)。作者将通过一个简单的RPC项目解决这些问题.

      让我们先看一下RPC的整体系统架构:

      在图片中服务结束时,请将服务节点信息注册到注册中心,当调用远程方法时,客户会订阅注册中心的可用服务节点信息,获取可用服务节点后远程调用方法,通知客户端在注册中心的可用服务节点发生更改时,避免客户继续调用已经不活跃的节点。客户端如何调用远程方法?请看远程调用图:

      1. 所有远程方法的客户端模块代理调用

      2. 编列所需的信息,例如目标服务、目标方法、调用目标方法的参数

      3. 数据包在序列化后进一步压缩。压缩后,数据包通过网络通信传输到目标服务节点。

      4. 服务节点将压缩它接收的数据包

      5. 压缩包逆序向目标服务、目标方法、目标方法调用参数

      6. 通过调用服务部门的代理来获取目标方法,并对结果进行序列化、压缩和传递给客户

      通过上述描述,我们认为读者一般应该了解RPC如何工作,然后看看如何使用代码实现上述过程。 鉴于文章的长度,作者将选择重要或介绍在网络上相对少数模块来告诉。

      RPC实现细节

      服务注册与发现

      系统使用 Zookeeper作为注册中心,而ZooKeeper存储数据在高性能的内存中。它特别适合小场景,因为该操作将使所有服务器同步。 服务登记和发现是典型的服务场景的协调较少Zookeeper是一个典型的CP系统,在服务选择或团体半数机延迟时不可用,在服务发现中比主流AP系统略少,但对于理解RPC实现也有用。

      ZooKeeper节点

      • 持久节点(PERSISENT):一旦创建,存储器是永久持久的,除非删除操作被激活。

      • 临时节点(EPHEMERAL):与客户端会话结合,客户端会话失败,所有由客户端创建的临时节点被删除。

      • 节点序列 ( SEQUENTIAL ): 创建子节点时,如果 SEQUENTIAL属性设置,然后在节点名称之后自动添加一个正交数,上限是格式的最大值;共享序列在同一目录下,例如(/a001,/b0000000002,/c0000000003,/test0000000004)。

      服务注册

      根据服务名称在ZooKeeper根节点下创建持久节点/rpc/{serviceName}/service创建所有服务节点,使用临时节点/rpc/{serviceName}/service在目录下,代码如下(为了显示方便,与项目代码相比,删除下列显示代码):

      如图所示,两个服务在本地启动,两个服务节点信息在服务下:

      存储的节点信息包括服务名称、服务IP:PORT、序列化协议、压缩协议等。

      服务发现

      客户端启动后,它不会立即从注册中心获取可用的服务节点,而是在调用远程方法时获取节点信息(懒负荷),并将其置于后续调用的本地缓存MAP中。注册中心通知目录更改时,它清除所有服务节点缓存,代码如下:

      PS: 各种分布式ID生成系统 Leaf使用动物园主序列节点注册工人ID和临时节点存储节点IP:PORT信息。

      Client

      远程调用方法和客户端调用本地方法的完美体验与Java动态代理的强大的隐私是不可区分的。

      DefaultRpcBaseProcessor抽象类实现了ApplicationListener,onApplicationEvent方法在春季项目启动及完成时收到时间通知ApplicationContext在上下文之后开始注入服务injectService(有可靠的服务)或启动服务startServer(有服务实现)。

      injectService方法会遍历ApplicationContext上下文中的所有BeanBean中是否有属性使用了InjectService注释:如果存在的话,生成一个代理类,并将其注入Bean在属性中,代码如下:

      调用ClientProxyFactory类的getProxy取决于服务接口、服务分段、服务版本以及是否指定代理类创建接口,所有接口的方法将使用指定代理类进行调用。 方法调用的实现细节都已提供ClientInvocationHandler中的invoke方法, 主要内容是获取服务节点信息, 选择调用节点, 构建请求对象, 最后调用网络模块发送请求.

      网络传输

      在客户端封装调用对象后,需要通过网络向服务端发送调用信息,在发送请求对象之前,需要进行两个阶段的序列和压缩。

      序列化与反序列化

      序列和逆序列的核心功能是保存和重构对象,让客户端和服务端通过字节流传递对象,并快速交互。

      • 序列是指将Java对象转换成字符串序列的过程。

      • 反序列是指将字符串序列恢复到Java对象的过程。

      编译Java有很多方法,如JDKSerializableProtobufkryo因此,上述三个作者的自我评价表现最高Kryo、其次是ProtobufJson作为一种简单高效的序列方法,有多种方法来简化框架。序列化接口相对简单,读者可以自行查看实现代码。

      压缩与解压

      网络通信的成本很高,为了减少网络传输包的数量,它是一个很好的选择,不要在序列之后失去位码压缩。Gzip压缩算法的比重大约是3到10倍,服务器的网络带宽可以大大节省,所有流行的Web服务器也支持Gzip压缩算法。也更容易访问Java,输入代码可以查看下面的接口的实现。

      网络通信

      万事俱备只欠东风。将请求对象列为字节代码,并且压缩体积之后,你需要使用网络将位码转移到服务器上。常见的网络协议包括HTTP、TCP、WebSocke t等。HTTP和WebSocket是应用程序层协议,TCP是一个层级协议。一些寻求简单、易于使用RPC框架的人也选择HTTP协议。TCP传输的高可靠性和极高的性能是选择主流RPC框架的主要原因。谈论Java生态通信领域,Netty如果Nety被选为网络通信模块,那么TCP数据流的绑定和不绑定是不可避免的。

      粘包、拆包

      TCP传输协议是一种面向连接的、可靠的、基于字节流的传输层通信协议。

      Nety提供三个类型的解码器来处理TCP粘贴/解码问题:

      • 定长消息解码器:FixedLengthFrameDecoder.发送者和接收者指定一个固定的消息长度,不足以填补空格和其他字符,因此每个接收者每当它不足以保留当前接收数据时从接收流中读取一个固定长度的消息,然后在下一个流中检索剩下的数据数目。

      • 分隔符解码器:LineBasedFrameDecoderDelimiterBasedFrameDecoderLineBasedFrameDecoder是行分离器解码器,分离器是nrnDelimiterBasedFrameDecoder一个自定义的分离器解码器,可以定义一个或多个分离器。 接收器在接收字节流中搜索分离器,然后返回分离器前的数据,并在没有找到分离器的情况下继续从下一个字节流搜索。

      • 数据长度解码器:LengthFieldBasedFrameDecoder。消息将分成标题和主体,存储消息的长度(字节),文体是发送消息的内容。同时,发送者和接收者必须协商该标题的字节数目,因为int可以指长度,长度也可以指长度。接收器首先从节点流中读取n(头目数)之前一个头目。然后根据长度读取相应的字节,如果不够,从下一个数据流搜索。

      如果您不想使用内置解码器,您也可以定义解码器,一个定制传输协议。

      网络通信的这一部分内容更为复杂,演讲的长度,可读代码,读者可以先读代码。

      Server

      客户端通过网络传输将请求对象序列化,然后压缩代码并将其发送到服务端,然后通过压缩和逆序列重建代码到请求对象。

      上述抽象类RequestBaseHandler是调用服务方法的抽象实现handleRequest请求对象的服务名称、服务分组和服务版本是serverRegister.getServiceObject获取代理对象,然后叫invoke一种抽象的方法,通过代理对象调用方法真正获得结果.

      1. 如何生成该服务的代理对象?

      2. 如何通过代理对象打电话?

      生成服务代理对象

      带着上述问题来看DefaultRpcBaseProcessor抽象类:

      DefaultRpcBaseProcessor抽象类也有两个实现类DefaultRpcReflectProcessorDefaultRpcJavassistProcessor实现关键生成代理对象startServer方法。

      作为代理对象服务接口实现的 `Bean`类

      DefaultRpcReflectProcessor中获取到所有有RpcService注释服务接口实现类Bean,然后将该Bean作为服务代理人注册的对象serverRegister用于上述反射调用。

      使用`Javassist`生成新的代理对象

      DefaultRpcJavassistProcessorDefaultRpcReflectProcessor区别在于后者直接实现该服务的类对象Bean作为服务代理对象,前者通过

      ProxyFactory.makeProxy(value, beanName, declaredMethods)创建一个新的代理对象,并登记新的代理对象到serverRegister用于后续调用。 采用的方法Javassist为了生成代理类,代码是冗余的,因此建议阅读源代码。

      首先,我们的服务接口是:

      服务的实现类是:

      最后生成的代理类如下:

      清理全限定类名:

      • 代理类HelloService$proxy1649315143476有一个服务接口类型HelloService的静态属性serviceProxy,值就是通过ApplicationContext背景获取服务接口实现类HelloServiceImpl这个BeanSpringContext已经被提前缓存到Container在课堂上, 读者可以自行搜索代码.

      • public RpcResponse invoke(RpcRequest request) throws Exception确定调用方法的名称是hello来调用代理类中的hello方法。

      • public RpcResponse hello(RpcRequest request) throws Exception该方法通过调用serviceProxy.hello()的方法获取结果。

      HelloService$proxy1649315143476类实现InvokeProxy接口(ProxyFactory.makeProxy代码中有体现)。InvokeProxy接口只有一个invoke方法.这里你可以通过调用代理对象来理解invoke可以间接调用服务接口实现类的方法HelloServiceImpl的对应方法了。

      调用代理对象方法

      生成代理对象后, 开始调用代理对象的方法.

      上文中写到的抽象类RequestBaseHandler有两个实现类RequestJavassistHandlerRequestReflectHandler

      Java 反射调用

      先看RequestReflectHandler

      Object value = method.invoke(serviceObject.getObj(), request.getParameters());

      这个行代码是众所周知的,它使用Java框架中最常见的反射来调用代理类,就像在大多数RPC框架中一样。

      调用Javaassists生成的代理对象的“调用”方法

      接着看RequestJavassistHandler:

      直接将代理对象转为InvokeProxy,调用InvokeProxy.invoke()获取返回值的方法,如不能在这里理解,请回顾并再次使用Javassist在这一节中生成一个新的代理对象。

      调用代理对象获取结果的方法仍然必须通过序列和压缩,然后通过网络将数据包转移到客户端,客户端接收响应结果再压缩,逆序列获取结果对象。

      Javassist

      Javassist它是一个开放源代码类库,用于分析、编辑和创建Java代码。 是东京理工学院数学和计算机科学系千叶滋使用源级 api来修改节点代码。DubooMyBatis也都使用了Javassist.杜布的作者也选择Javassist作为杜布代理工具,您可以点击这里查看杜布作者也可以选择Javassist的原因。

      Javassist它也可以兼容Java兼容的商业软件,例如抓取包装工具Charles这个代码是用来分享和学习的。

      在使用Javassist下面的洞穴有步骤供参考:

      1. Javassist是运行时,没有JDK静态编译过程,JDK许多语法糖在静态编译程序中被处理,因此它们需要自编码,例如自动拆卸箱。

      2. 自定义类需要使用类的完全有限名称,因此生成的代理类中的类都是完全有限的名称。

      选择哪种代理方式

      可以通过配置文件application.properties修改hp.rpc.server-proxy-type选择代理模式的值。

      性能测试, 机器Macbook Pro M1 8C 16G, 代码如下:

      测试结果(ms):

      请求次数 反射调用1 反射调用2 反射调用3 Javassist1 Javassist2 Javassist3
      10000 1303 1159 1164 1126 1235 1094
      100000 6110 6103 6065 6259 5854 6178
      1000000 54475 51890 52329 52560 52099 52794

      测试结果的差异并不大,Javassist在模式下,它只是稍微更快,而且几乎可以忽略它。 测试结果与杜布的博客第6楼的评论一致。 所以如果你想要简单和普遍性,你可以使用反射模式,或者你可以使用Javassist学习更多知识的模式,因为Javassist许多特殊情况需要自适应,而JDK的反思电话已经帮助你做到这一点。

      总结

      在此,我们了解RPC、服务登记和发现、客户代理、网络传输的基本原则,介绍了服务端的两个代理模式,并学习Javassist如何实现代理。

      有很多事情没有得到太多的关注,甚至没有提到,例如粘贴的包处理、定制的包协议,Javassist如何实现方法过载,如何解决服务接口类具有多个实现,如何解决实现类实现多个服务接口,在SpringBoot如何自动加载,如何立即打开箱子,如何实现异步调用,如何扩展序列,压缩算法等。感兴趣的读者可以在源代码中找到答案,或者寻找优化项,当然,您也可以搜索错误。如果读者能理解整个项目的实施,我相信你会从中得到一些东西。之后,我们将有机会写更多的文章,与他人交流和学习。因笔者水平有限,事情不完美的地方, 让我们做好.感谢各位的阅读,谢谢。

      项目地址: https://github.com/pphuang/rpc-spring-starter

      测试 demo: https://github.com/pphuang/rpc-spring-starter-demo

      本文由 在线网速测试 整理编辑,转载请注明出处,原文链接:https://www.wangsu123.cn/news/29386.html

          热门文章

          文章分类