文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

如何手撸一个较为完整的RPC框架?

2024-12-02 06:03

关注

RPC 框架基本架构

下面我们通过一幅图来说说 RPC 框架的基本架构

RPC 框架包含三个最重要的组件,分别是客户端、服务端和注册中心。在一次 RPC 调用流程中,这三个组件是这样交互的:

RPC 框架通信流程以及涉及到的角色

从上面这张图中,可以看见 RPC 框架一般有这些组件:服务治理(注册发现)、负载均衡、容错、序列化/反序列化、编解码、网络传输、线程池、动态代理等角色,当然有的RPC框架还会有连接池、日志、安全等角色。

具体调用过程


  1. 服务消费方(client)以本地调用方式调用服务
  2. client stub 接收到调用后负责将方法、参数等封装成能够进行网络传输的消息体
  3. client stub 将消息进行编码并发送到服务端
  4. server stub 收到消息后进行解码
  5.  server stub 根据解码结果调用本地的服务
  6. 本地服务执行并将结果返回给 server stub
  7. server stub 将返回导入结果进行编码并发送至消费方
  8. client stub 接收到消息并进行解码
  9. 服务消费方(client)得到结果

RPC 消息协议

RPC调用过程中需要将参数编组为消息进行发送,接收方需要解组消息为参数,过程处理结果同样需要经编组、解组。消息由哪些部分构成及消息的表示形式就构成了消息协议。

RPC调用过程中采用的消息协议称为RPC消息协议。

实战篇

从上面的概念我们知道一个RPC框架大概有哪些部分组成,所以在设计一个RPC框架也需要从这些组成部分考虑。从RPC的定义中可以知道,RPC框架需要屏蔽底层细节,让用户感觉调用远程服务像调用本地方法一样简单,所以需要考虑这些问题:

上面这些问题在设计这个RPC框架中都会给予解决。

技术选型

项目总体结构

从这个结构中可以知道,以rpc命名开头的是rpc框架的模块,也是本项目RPC框架的内容,而consumer是服务消费者,provider是服务提供者,provider-api是暴露的服务API。

整体依赖情况

项目实现介绍

要做到用户使用我们的RPC框架时尽量少的配置,所以把rpc框架设计成一个starter,用户只要依赖这个starter,基本那就可以了。

为什么要设计成两个 starter (client-starter/server-starter) ?

这个是为了更好的体现出客户端和服务端的概念,消费者依赖客户端,服务提供者依赖服务端,还有就是最小化依赖。

为什么要设计成 starter ?

基于spring boot自动装配机制,会加载starter中的 spring.factories 文件,在文件中配置以下代码,这里我们starter的配置类就生效了,在配置类里面配置一些需要的bean。

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.rrtv.rpc.client.config.RpcClientAutoConfiguration

发布服务和消费服务

服务提供者需要在暴露的服务上增加注解 @RpcService,这个自定义注解是基于 @service 的,是一个复合注解,具备@service注解的功能,在@RpcService注解中指明服务接口和服务版本,发布服务到ZK上,会根据这个两个元数据注册

服务提供者启动之后,根据spring boot自动装配机制,server-starter的配置类就生效了,在一个 bean 的后置处理器(RpcServerProvider)中获取被注解 @RpcService 修饰的bean,将注解的元数据注册到ZK上。

消费服务需要使用自定义的 @RpcAutowired 注解标识,是一个复合注解,基于 @Autowired。

要让客户端无感知的调用服务提供者,就需要使用动态代理,如上面所示, HelloWordService 没有实现类,需要给它赋值代理类,在代理类中发起请求调用。

基于spring boot自动装配,服务消费者启动,bean 后置处理器 RpcClientProcessor 开始工作,它主要是遍历所有的bean,判断每个bean中的属性是否有被 @RpcAutowired 注解修饰,有的话把该属性动态赋值代理类,这个再调用时会调用代理类的 invoke 方法。

代理类 invoke 方法通过服务发现获取服务端元数据,封装请求,通过netty发起调用。

注册中心

本项目注册中心使用ZK,由于注册中心被服务消费者和服务提供者都使用。所以把ZK放在rpc-core模块。

rpc-core 这个模块如上图所示,核心功能都在这个模块。服务注册在 register 包下。

服务注册接口,具体实现使用ZK实现。

负载均衡策略

负载均衡定义在rpc-core中,目前支持轮询(FullRoundBalance)和随机(RandomBalance),默认使用随机策略。由rpc-client-spring-boot-starter指定。

通过ZK服务发现时会找到多个实例,然后通过负载均衡策略获取其中一个实例

可以在消费者中配置 rpc.client.balance=fullRoundBalance 替换,也可以自定义负载均衡策略,通过实现接口 LoadBalance,并将创建的类加入IOC容器即可。由于我们配置 @ConditionalOnMissingBean,所以会优先加载用户自定义的 bean。

自定义消息协议、编解码

所谓协议,就是通信双方事先商量好规则,服务端知道发送过来的数据将如何解析。

编解码

编解码实现在 rpc-core 模块,在包 com.rrtv.rpc.core.codec下。

自定义编码器通过继承 netty 的 MessageToByteEncoder>类实现消息编码。

自定义解码器通过继承 netty 的 ByteToMessageDecoder类实现消息解码。

解码时需要注意TCP粘包、拆包问题

什么是TCP粘包、拆包

TCP 传输协议是面向流的,没有数据包界限,也就是说消息无边界。客户端向服务端发送数据时,可能将一个完整的报文拆分成多个小报文进行发送,也可能将多个报文合并成一个大的报文进行发送。因此就有了拆包和粘包。

在网络通信的过程中,每次可以发送的数据包大小是受多种因素限制的,如 MTU 传输单元大小、滑动窗口等。

所以如果一次传输的网络包数据大小超过传输单元大小,那么我们的数据可能会拆分为多个数据包发送出去。如果每次请求的网络包数据都很小,比如一共请求了 10000 次,TCP 并不会分别发送 10000 次。TCP采用的 Nagle(批量发送,主要用于解决频繁发送小数据包而带来的网络拥塞问题) 算法对此作出了优化。

所以,网络传输会出现这样:

tcp_package.png

  1.    服务端恰巧读到了两个完整的数据包 A 和 B,没有出现拆包/粘包问题;
  2.    服务端接收到 A 和 B 粘在一起的数据包,服务端需要解析出 A 和 B;
  3.    服务端收到完整的 A 和 B 的一部分数据包 B-1,服务端需要解析出完整的 A,并等待读取完整的 B 数据包;
  4. 服务端接收到 A 的一部分数据包 A-1,此时需要等待接收到完整的 A 数据包;
  5. 数据包 A 较大,服务端需要多次才可以接收完数据包 A。

如何解决TCP粘包、拆包问题

解决问题的根本手段:找出消息的边界:

每个数据报文都需要一个固定的长度。当接收方累计读取到固定长度的报文后,就认为已经获得一个完整的消息。当发送方的数据小于固定长度时,则需要空位补齐。

消息定长法使用非常简单,但是缺点也非常明显,无法很好设定固定长度的值,如果长度太大会造成字节浪费,长度太小又会影响消息传输,所以在一般情况下消息定长法不会被采用。

在每次发送报文的尾部加上特定分隔符,接收方就可以根据特殊分隔符进行消息拆分。分隔符的选择一定要避免和消息体中字符相同,以免冲突。否则可能出现错误的消息拆分。比较推荐的做法是将消息进行编码,例如 base64 编码,然后可以选择 64 个编码字符之外的字符作为特定分隔符

消息长度 + 消息内容是项目开发中最常用的一种协议,接收方根据消息长度来读取消息内容。

本项目就是利用 “消息长度 + 消息内容” 方式解决TCP粘包、拆包问题的。所以在解码时要判断数据是否够长度读取,没有不够说明数据没有准备好,继续读取数据并解码,这里这种方式可以获取一个个完整的数据包。

序列化和反序列化

序列化和反序列化在 rpc-core 模块 com.rrtv.rpc.core.serialization 包下,提供了 HessianSerialization 和 JsonSerialization 序列化。

默认使用 HessianSerialization 序列化。用户不可以自定义。

序列化性能:

serialization_space.png

serialization_time.png

网络传输,使用netty

netty 代码固定的,值得注意的是 handler 的顺序不能弄错,以服务端为例,编码是出站操作(可以放在入站后面),解码和收到响应都是入站操作,解码要在前面。

image.png

客户端 RPC 调用方式

成熟的 RPC 框架一般会提供四种调用方式,分别为同步 Sync、异步 Future、回调 Callback和单向 Oneway。

客户端线程发起 RPC 调用后,当前线程会一直阻塞,直至服务端返回结果或者处理超时异常。

sync.png

客户端发起调用后不会再阻塞等待,而是拿到 RPC 框架返回的 Future 对象,调用结果会被服务端缓存,客户端自行决定后续何时获取返回结果。当客户端主动获取结果时,该过程是阻塞等待的

future.png

客户端发起调用时,将 Callback 对象传递给 RPC 框架,无须同步等待返回结果,直接返回。当获取到服务端响应结果或者超时异常后,再执行用户注册的 Callback 回调

callback.png

客户端发起请求之后直接返回,忽略返回结果

oneway.png

这里使用的是第一种:客户端同步调用,其他的没有实现。逻辑在 RpcFuture 中,使用 CountDownLatch 实现阻塞等待(超时等待)

整体架构和流程

流程分为三块:服务提供者启动流程、服务消费者启动、调用过程

服务提供者启动

  1. 服务提供者 provider 会依赖 rpc-server-spring-boot-starter
  2. ProviderApplication 启动,根据springboot 自动装配机制,RpcServerAutoConfiguration 自动配置生效
  3. RpcServerProvider 是一个bean后置处理器,会发布服务,将服务元数据注册到ZK上
  4. RpcServerProvider.run 方法会开启一个 netty 服务

服务消费者启动

  1.    服务消费者 consumer 会依赖 rpc-client-spring-boot-starter
  2.    ConsumerApplication 启动,根据springboot 自动装配机制,RpcClientAutoConfiguration 自动配置生效
  3. 将服务发现、负载均衡、代理等bean加入IOC容器
  4. 后置处理器 RpcClientProcessor 会扫描 bean ,将被 @RpcAutowired 修饰的属性动态赋值为代理对象

调用过程

  1.    服务消费者 发起请求http://localhost:9090/hello/world?name=hello
  2.    服务消费者 调用 helloWordService.sayHello() 方法,会被代理到执行 ClientStubInvocationHandler.invoke() 方法
  3. 服务消费者 通过ZK服务发现获取服务元数据,找不到报错404
  4. 服务消费者 自定义协议,封装请求头和请求体
  5. 服务消费者 通过自定义编码器 RpcEncoder 将消息编码
  6. 服务消费者 通过 服务发现获取到服务提供者的ip和端口, 通过Netty网络传输层发起调用
  7. 服务消费者 通过 RpcFuture 进入返回结果(超时)等待
  8. 服务提供者 收到消费者请求
  9. 服务提供者 将消息通过自定义解码器 RpcDecoder 解码
  10. 服务提供者 解码之后的数据发送到 RpcRequestHandler 中进行处理,通过反射调用执行服务端本地方法并获取结果
  11. 服务提供者 将执行的结果通过 编码器 RpcEncoder 将消息编码。(由于请求和响应的协议是一样,所以编码器和解码器可以用一套)
  12. 服务消费者 将消息通过自定义解码器 RpcDecoder 解码
  13. 服务消费者 通过RpcResponseHandler将消息写入 请求和响应 池中,并设置 RpcFuture 的响应结果
  14. 服务消费者 获取到结果

以上流程具体可以结合代码分析,代码后面会给出

环境搭建

项目测试

项目代码地址

​https://gitee.com/listen_w/rpc.git​

来源:Java知音内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯