架构师如何手撸一个较为完整的RPC框架?
以下文章来源Java架构师技术,回复”Spring“获惊喜礼包
上一篇推文:一款高颜值开源桌面自动化终极利器
大家好,我是Java架构师
最近在公司分享了手撸RPC,因此做一个总结。
概 念 篇
RPC 是什么?
RPC 称远程过程调用(Remote Procedure Call),用于解决分布式系统中服务之间的调用问题。通俗地讲,就是开发者能够像调用本地方法一样调用远程的服务。所以,RPC的作用主要体现在这两个方面:
屏蔽远程调用跟本地调用的区别,让我们感觉就是调用项目内的方法; 隐藏底层网络通信的复杂性,让我们更专注于业务逻辑。
RPC 框架基本架构
下面我们通过一幅图来说说 RPC 框架的基本架构
RPC 框架包含三个最重要的组件,分别是客户端、服务端和注册中心。在一次 RPC 调用流程中,这三个组件是这样交互的:
- 服务端在启动后,会将它提供的服务列表发布到注册中心,客户端向注册中心订阅服务地址;
客户端会通过本地代理模块 Proxy 调用服务端,Proxy 模块收到负责将方法、参数等数据转化成网络字节流; 客户端从服务列表中选取其中一个的服务地址,并将数据通过网络发送给服务端; 服务端接收到数据后进行解码,得到请求信息; 服务端根据解码后的请求信息调用对应的服务,然后将调用结果返回给客户端。
RPC 框架通信流程以及涉及到的角色
从上面这张图中,可以看见 RPC 框架一般有这些组件:服务治理(注册发现)、负载均衡、容错、序列化/反序列化、编解码、网络传输、线程池、动态代理等角色,当然有的RPC框架还会有连接池、日志、安全等角色。
具体调用过程
服务消费方(client)以本地调用方式调用服务 client stub 接收到调用后负责将方法、参数等封装成能够进行网络传输的消息体 client stub 将消息进行编码并发送到服务端 server stub 收到消息后进行解码 server stub 根据解码结果调用本地的服务 本地服务执行并将结果返回给 server stub server stub 将返回导入结果进行编码并发送至消费方 client stub 接收到消息并进行解码 服务消费方(client)得到结果
RPC 消息协议
RPC调用过程中需要将参数编组为消息进行发送,接收方需要解组消息为参数,过程处理结果同样需要经编组、解组。消息由哪些部分构成及消息的表示形式就构成了消息协议。
RPC调用过程中采用的消息协议称为RPC消息协议。
实 战 篇
从上面的概念我们知道一个RPC框架大概有哪些部分组成,所以在设计一个RPC框架也需要从这些组成部分考虑。
从RPC的定义中可以知道,RPC框架需要屏蔽底层细节,让用户感觉调用远程服务像调用本地方法一样简单,所以需要考虑这些问题:
- 用户使用我们的RPC框架时如何尽量少的配置
- 如何将服务注册到ZK(这里注册中心选择ZK)上并且让用户无感知
- 如何调用透明(尽量用户无感知)的调用服务提供者
- 启用多个服务提供者如何做到动态负载均衡
- 框架如何做到能让用户自定义扩展组件(比如扩展自定义负载均衡策略)
- 如何定义消息协议,以及编解码
- ...等等
上面这些问题在设计这个RPC框架中都会给予解决。
技术选型
注册中心 目前成熟的注册中心有Zookeeper,Nacos,Consul,Eureka,这里使用ZK作为注册中心,没有提供切换以及用户自定义注册中心的功能。 IO通信框架 本实现采用 Netty 作为底层通信框架,因为Netty 是一个高性能事件驱动型的非阻塞的IO(NIO)框架,没有提供别的实现,也不支持用户自定义通信框架。 消息协议 本实现使用自定义消息协议,后面会具体说明。
项目总体结构
从这个结构中可以知道,以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上,会根据这个两个元数据注册。 对于消费服务 消费服务需要使用自定义的 @RpcAutowired 注解标识,是一个复合注解,基于 @Autowired。
发布服务原理:
消费服务原理
注册中心
本项目注册中心使用ZK,由于注册中心被服务消费者和服务提供者都使用。所以把ZK放在rpc-core模块。
rpc-core 这个模块如上图所示,核心功能都在这个模块。服务注册在 register 包下。
服务注册接口,具体实现使用ZK实现。
负载均衡策略
负载均衡定义在rpc-core中,目前支持轮询(FullRoundBalance)和随机(RandomBalance),默认使用随机策略。由rpc-client-spring-boot-starter指定。 另外,搜索公众号Linux就该这样学后台回复“git书籍”,获取一份惊喜礼包。
通过ZK服务发现时会找到多个实例,然后通过负载均衡策略获取其中一个实例
可以在消费者中配置 rpc.client.balance=fullRoundBalance 替换,也可以自定义负载均衡策略,通过实现接口 LoadBalance,并将创建的类加入IOC容器即可。
由于我们配置 @ConditionalOnMissingBean,所以会优先加载用户自定义的 bean。
自定义消息协议、编解码
所谓协议,就是通信双方事先商量好规则,服务端知道发送过来的数据将如何解析。
自定义消息协议 - 编解码
编解码实现在 rpc-core 模块,在包 com.rrtv.rpc.core.codec下。 自定义编码器通过继承 netty 的 MessageToByteEncoder<MessageProtocol<T>>类实现消息编码。 自定义解码器通过继承 netty 的 ByteToMessageDecoder类实现消息解码。
魔数:魔数是通信双方协商的一个暗号,通常采用固定的几个字节表示。魔数的作用是防止任何人随便向服务器的端口上发送数据。
解码时需要注意TCP粘包、拆包问题
什么是TCP粘包、拆包
TCP 传输协议是面向流的,没有数据包界限,也就是说消息无边界。客户端向服务端发送数据时,可能将一个完整的报文拆分成多个小报文进行发送,也可能将多个报文合并成一个大的报文进行发送。因此就有了拆包和粘包。
在网络通信的过程中,每次可以发送的数据包大小是受多种因素限制的,如 MTU 传输单元大小、滑动窗口等。
所以如果一次传输的网络包数据大小超过传输单元大小,那么我们的数据可能会拆分为多个数据包发送出去。
如果每次请求的网络包数据都很小,比如一共请求了 10000 次,TCP 并不会分别发送 10000 次。TCP采用的 Nagle(批量发送,主要用于解决频繁发送小数据包而带来的网络拥塞问题) 算法对此作出了优化。
所以,网络传输会出现这样:
服务端恰巧读到了两个完整的数据包 A 和 B,没有出现拆包/粘包问题; 服务端接收到 A 和 B 粘在一起的数据包,服务端需要解析出 A 和 B; 服务端收到完整的 A 和 B 的一部分数据包 B-1,服务端需要解析出完整的 A,并等待读取完整的 B 数据包; 服务端接收到 A 的一部分数据包 A-1,此时需要等待接收到完整的 A 数据包; 数据包 A 较大,服务端需要多次才可以接收完数据包 A。
如何解决TCP粘包、拆包问题
解决问题的根本手段
找出消息的边界:
消息长度固定 每个数据报文都需要一个固定的长度。当接收方累计读取到固定长度的报文后,就认为已经获得一个完整的消息。当发送方的数据小于固定长度时,则需要空位补齐。 消息定长法使用非常简单,但是缺点也非常明显,无法很好设定固定长度的值,如果长度太大会造成字节浪费,长度太小又会影响消息传输,所以在一般情况下消息定长法不会被采用。 特定分隔符 在每次发送报文的尾部加上特定分隔符,接收方就可以根据特殊分隔符进行消息拆分。分隔符的选择一定要避免和消息体中字符相同,以免冲突。 否则可能出现错误的消息拆分。比较推荐的做法是将消息进行编码,例如 base64 编码,然后可以选择 64 个编码字符之外的字符作为特定分隔符。 - 消息长度 + 消息内容消息长度 + 消息内容是项目开发中最常用的一种协议,接收方根据消息长度来读取消息内容。
本项目就是利用 “消息长度 + 消息内容” 方式解决TCP粘包、拆包问题的。
所以在解码时要判断数据是否够长度读取,没有不够说明数据没有准备好,继续读取数据并解码,这里这种方式可以获取一个个完整的数据包。
序列化和反序列化
序列化和反序列化在 rpc-core 模块 com.rrtv.rpc.core.serialization 包下,提供了 HessianSerialization 和 JsonSerialization 序列化。
默认使用 HessianSerialization 序列化。用户不可以自定义。
序列化性能:
空间上
- 时间上
网络传输,使用netty
netty 代码固定的,值得注意的是 handler 的顺序不能弄错,以服务端为例,编码是出站操作(可以放在入站后面),解码和收到响应都是入站操作,解码要在前面。
客户端 RPC 调用方式
成熟的 RPC 框架一般会提供四种调用方式,分别为同步 Sync、异步 Future、回调 Callback和单向 Oneway。
Sync 同步调用 客户端线程发起 RPC 调用后,当前线程会一直阻塞,直至服务端返回结果或者处理超时异常。 - Future 异步调用客户端发起调用后不会再阻塞等待,而是拿到 RPC 框架返回的 Future 对象,调用结果会被服务端缓存,客户端自行决定后续何时获取返回结果。当客户端主动获取结果时,该过程是阻塞等待的
- Callback 回调调用客户端发起调用时,将 Callback 对象传递给 RPC 框架,无须同步等待返回结果,直接返回。当获取到服务端响应结果或者超时异常后,再执行用户注册的 Callback 回调
Oneway 单向调用 客户端发起请求之后直接返回,忽略返回结果。
这里使用的是第一种:客户端同步调用,其他的没有实现。逻辑在 RpcFuture 中,使用 CountDownLatch 实现阻塞等待(超时等待)
整体架构和流程
流程分为三块:服务提供者启动流程、服务消费者启动、调用过程。
服务提供者启动 服务消费者启动 - 调用过程
以上流程具体可以结合代码分析,代码后面会给出。
环境搭建
操作系统:Windows 集成开发工具:IntelliJ IDEA 项目技术栈:SpringBoot 2.5.2 + JDK 1.8 + Netty 4.1.42.Final 项目依赖管理工具:Maven 4.0.0 注册中心:Zookeeeper 3.7.0
项目测试
启动 Zookeeper 服务器:bin/zkServer.cmd 启动 provider 模块 ProviderApplication 启动 consumer 模块 ConsumerApplication 测试:浏览器输入 http://localhost:9090/hello/world?name=hello,成功返回您好:hello, rpc 调用成功
项 目 代 码
项目代码地址
https://gitee.com/listen_w/rpc
最后,整理了300多套项目,赠送读者。扫码下方二维码,后台回复【赚钱】即可获取。
--END--
来源:一个没有追求的技术人链接:juejin.cn/post/6992867064952127524版权申明:内容来源网络,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢!
往期惊喜:
扫码关注我们的Java架构师技术
带你全面深入Java
最新评论
推荐文章
作者最新文章
你可能感兴趣的文章
Copyright Disclaimer: The copyright of contents (including texts, images, videos and audios) posted above belong to the User who shared or the third-party website which the User shared from. If you found your copyright have been infringed, please send a DMCA takedown notice to [email protected]. For more detail of the source, please click on the button "Read Original Post" below. For other communications, please send to [email protected].
版权声明:以上内容为用户推荐收藏至CareerEngine平台,其内容(含文字、图片、视频、音频等)及知识版权均属用户或用户转发自的第三方网站,如涉嫌侵权,请通知[email protected]进行信息删除。如需查看信息来源,请点击“查看原文”。如需洽谈其它事宜,请联系[email protected]。
版权声明:以上内容为用户推荐收藏至CareerEngine平台,其内容(含文字、图片、视频、音频等)及知识版权均属用户或用户转发自的第三方网站,如涉嫌侵权,请通知[email protected]进行信息删除。如需查看信息来源,请点击“查看原文”。如需洽谈其它事宜,请联系[email protected]。