# xqd-rpc **Repository Path**: xuqiudong/xqd-rpc ## Basic Information - **Project Name**: xqd-rpc - **Description**: 手写的一个简单的rpc框。完成了以下功能:基于SPI思想实现框架的动态扩展;基于zookeeper实现注册中心的设计,包括服务发现、服务订阅、服务注册等功能;基于一致性hash和随机轮询的负载均衡策略;基于Netty和Tomcat的数据通信工作;基于JDK、Hessian与JSON方式的序列化和反序列化;基于JDK的动态代理对象的生成;最后实现了和spring框架的无缝衔接。 - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2022-02-07 - **Last Updated**: 2023-09-18 ## Categories & Tags **Categories**: Uncategorized **Tags**: study ## README # 基于JAVA的分布式RPC框架设计及编码 > by 许秋冬 > > - [我的主页](https://www.xuqiudong.cn) > > 设计并手写一个rpc框架,完成rpc框架的基础功能。 # 第一章 XQD-RPC框架设计 ## 1.1 Xqd-rpc架构设计 ![img](asset/clip_image002.jpg) 图1.1 xqd-rpc总体架构图 ### 1.1.1架构图中的角色说明 1. Registry:服务注册与发现的注册中心 2. Consumer:调用远程服务的服务消费方 3. Provider:暴露服务的服务提供方 4. Container: 服务运行容器 ### 1.1.2调用关系的说明 1. 服务容器负责启动,加载,运行服务提供者。 2. 服务提供者在启动时,向注册中心注册自己提供的服务。 3. 服务消费者在启动时,向注册中心订阅自己所需的服务。 4. 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。 5. 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用 ## 1.2 Xqd-rpc 框架启动和调用流程 ![img](asset/clip_image004.jpg) 图1.2 xqd-rpc框架启动流程图 如图1.2所示,xqd-rpc框架集成了spring框架,并通过自定义SPI作为各类组件的动态扩展点。其中本框架主要完成的自定义扩展点包括:注册中心、负载均衡、序列化/反序列化、通讯协议Protocol、动态代理。在接下来的章节中,将围绕上述几点分别展开叙述,最终完成xqd-rpc框架的设计和编码。 ## 1.3 Xqd-rpc 框架结构说明 Xqd-rpc 框架基于java语言开发,基于maven工具构件,基于git进行版本控制。 项目的结构如下图所示: ![img](asset/clip_image006.jpg) 图1.3项目代码结构 其中,xqd-rpc为mavan父项目,引入,统一管理了第三方依赖的版本,定义了如springboot,zookeeper,netty以及相关工具类等等的版本。子项目Xqd-rpc-core为本RPC框架的核心代码部分,完成了RPC的全部编码工作。Xqd-rpc-demo子项目为部分代码测试以及功能展示模块。 ### 1.3.1 核心模块 xqd-rpc-core extension:基于SPI思想的自定义扩展点加载器 serializer:负责通信中的数据的序列化和反序列化(**扩展点**) registry:注册中心,包括注册和订阅,查询等功能(**扩展点**) proxy:代理层,负责生成接口的代理类,在代理类中完成远程通信(**扩展点**) cluster:包括负载均衡等集群处理(**扩展点**) common:通用模块,包括通用异常,通用model,通用工具类,通用注解等 protocol:协议层,默认基于netty通信(**扩展点**) spring:负责和spring框架的集成工作 ### 1.3.2 集成测试模块:xqd-rpc-demo 本模块主要引用xqd-rpc-core,并基于springBoot进行了集成测试以及demo展示。 ### 1.3.3 几个比较核心的数据结构 一个框架设计的编码工作一般较为复杂,代码量也比较多。笔者也曾浅读过部分开源框架的源码,比如spring,dubbo。但是鉴于水平非常低,阅读起来十分的苦难。鉴于此,在这里罗列了xqd-rpc框架的部分核心的数据结构。虽然本框架十分简单,简陋。但是窥一斑而知全豹,通过以下几个中的要数据模型,可以一窥本框架的脉络。 ##### 1 XqdUrl: 资源统一定位符 ![img](asset/clip_image008.jpg) 图1.4统一资源定位符UML ``` 包含一个接口的全限定名,接口服务的地址,接口地址的端口(属性可扩展)。 并结合XqdUrlUtils工具类,提供了一些核心方法: 1. String url2String(XqdUrl url):把一个url通过一些算法转为String,并进行URLEncoder,最终作为zookeeper的path使用 2. XqdUrl string2Url(String urlString):把查询出来的zookeeper节点path转为XqdUrl对象。 3. String findParent(XqdUrl url):生成当前节点的父节点, 4. List toUrls(List children):多个path节点转为url列表 去重,去空 ``` ##### 2 XqdRequest:rpc请求参数 ![img](asset/clip_image010.jpg) 图1.5 RPC请求参数UML 封装了一个远程方法的基本信息,包括接口名,方法名,接口参数类型列表,接口参数列表。 ##### 3 XqdResponse:rpc响应结果对象 ![img](asset/clip_image012.jpg) 图1.6 RPC响应结果UML 封装了一个远程接口方法执行结果的信息,包括响应码,提示信息,方法返回对象。 #### 4 Invoker:携带PRC调用的核心数据 ![img](asset/clip_image014.jpg) 图1.6 核心数据Invoker类UML ``` 封装了客户端调用服务端时候需要携带的核心数据,包括接口名,请求参数XqdRequest,请求标识requestId(用于异步转同步),资源定位符XqdUrl,接口方法的返回类型。 ``` # 第二章 基于SPI思想的自定义SPI作为组件动态扩展点 ## 2.1 框架组件可扩展的必要性 作为一个应用级的系统rpc框架的设计,应满足面向对象设计原则:开闭原则(OPC), 也即在:当应用的需求改变时,在不修改软件实体的源代码或者二进制代码的前提下,可以扩展模块的功能,使其满足新的需求。鉴于此,xqd-rpc使用SPI机制实现对框架的扩展和组件的动态替换。 ## 2.2 SPI 简单说明 ### 2.2.1 什么是java SPI SPI ,全称为 Service Provider Interface,是一种服务发现机制。它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。 SPI的使用场景比较广泛,例如:数据库JDBC驱动加载,SLF4J不同日志框架适配,Servlet中的ServletContainerInitializer等等。 **但是JAVA自身的SPI有着一些天然的缺点:** \1. JAVA SPI一次性实例化所有的扩展点,即使不使用也会加载初始化,较为耗时,且造成资源浪费 \2. JAVA SPI获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。 \3. JAVA SPI多个并发多线程使用 ServiceLoader 类的实例是不安全的。 ## 2.3 自定义SPI扩展XqdSpiExtensionLoader ★ ### 2.3.1 XqdSpiExtensionLoader自定义扩展点的功能 ★ \1. 在本框架中,每个扩展点对应一个XqdSpiExtensionLoader实例 \2. 动态读取扩展点的全局实现的class信息 \3. 根据配置的扩展点name或者默认的扩展点name,获取一个扩展点 \4. 缓存扩展点Class和XqdSpiExtensionLoader实例,即一个类型的扩展定制一个load实例 \5. 缓存已加载的扩展点class信息 \6. 缓存扩展点的实例 \7. 完成框架扩展点的IOC工作,即对于扩展点相互引用的的情况,作为容器,通过反射注入相应扩展点 ### 2.3.2 扩展点约定 \1. 扩展点配置文件位置**:**为:”**META-INF/xqd/****接口全限定名**” \2. 扩展点文件内容:自定义扩展点名称=扩展实现类全限定名;每一行配置一个自定义扩展点,另 # 表示注释符。 \3. 所有的扩展点接口需要被自定义注解” XqdExtension”标识 \4. 某个扩展点需要引入其他的扩展点作为属性的,需要被自定义注解” InjectExtension”标识 \5. 配置使用某个扩展点的方式:配置文件为xqd-rpc.properties;配置的key参见每个扩展点接口上的” @XqdExtension”注解中的configKey属性(定义在常量类cn.xuqiudong.rpc.common.constants.ConfigKey之中) ### 2.3.3 xqd-rpc框架当前支持的扩展点 #### 1. 负载均衡扩展点 **接口**:cn.xuqiudong.rpc.balance.LoadBalance **功能**:框架中获取服务提供者的负载均衡策略 **框架支持**,random(随机策略),hash(一致性hash策略) **默认**: random #### 2通信协议扩展点 接口:cn.xuqiudong.rpc.protocol.Protocol 功能:启动服务端rpc服务,客户端发送数据方法 框架支持:xqd(基于netty的通信协议),http(基于tomcat的通信协议) 默认:xqd #### 3. 代理工厂扩展点 接口:cn.xuqiudong.rpc.proxy.ProxyFactory ``` 功能:持有Protocol ,生成业务接口的代理类,完成远程调用 框架支持:jdk动态代理工厂 默认:jdk ``` #### 4 注册中心工厂扩展点 ``` 接口:cn.xuqiudong.rpc.registry.RegistryFactory 功能:生成注册中心Registry,完成服务注册,服务取消注册,服务订阅,服务取消订阅,服务发现功能 框架支持:zookeeper注册中心工厂 默认:zookeeper ``` ##### 5 序列化/反序列化扩展点 ``` 接口:cn.xuqiudong.rpc.serializer.XqdSerializer 功能:完成协议数据传输过程中的数据的序列号和反序列化功能功能,用以支持网络传输 框架支持:json序列化,hessian序列化,jdk序列化 默认:hessian ``` ### 2.3.4 自定义扩展模块UML ![img](asset/clip_image002-1679381569196.jpg) 图2.1自定义扩展模块UML ![img](asset/clip_image002-1679381608604.jpg) 图2.2自定义扩展组件类加载器UML **XqdExtensionFactory**扩展点工厂接口: > 见代码 `XqdExtensionFactory ` SpiXqdExtensionFactory:实现了XqdExtensionFactory接口,是 基于SPI的扩展工厂, 是单例的。其中获取自适应扩展类的方式参见2.3.2扩展点约定。 本框架中一般获取扩展点的方式如下: XqdExtensionFactory.defaultExtendFactory().getAdaptiveXqdExtension(扩展点接口class); 比如,获取序列化方式扩展点: ``` private static XqdSerializer serializer = XqdExtensionFactory.defaultExtendFactory().getAdaptiveXqdExtension(XqdSerializer.class); ``` 自定义扩展组件加载器XqdSpiExtensionLoader作为本框架中最基础的支持类,详见代码。 # 第三章 注册中心 ## 3.1 注册中心的作用和功能 为了高可用,在生产环境中服务提供方都是以集群的方式对外提供服务,集群里面的这些 IP 随时可能会变化,通过注册中心即时获取到对应的服务节点,这个获取的过程我们一般叫做“服务发现”。服务发现的本质,就是完成了接口跟服务提供者的 IP 的映射。 **注册中心一般提供一下功能:** 1. 注册服务:服务提供者把服务地址注册到服务中心 2. 取消注册:和注册服务对应 3. 服务订阅:服务消费者订阅某个接口,在注册数据发生变化的时候主动把数据推送过来 4. 取消订阅:和订阅服务对应 5. 服务发现:查询符合条件的注册服务的列表 ## 3.2 为什么使用zookeeper作为默认的注册中心组件 1. 数据模型简单,由一系列的ZNode的数据节点组成 2. 高性能:将全量数据存储在内存中,与传统的磁盘文件系统不同 3. 高可用: 支持集群 4. 支持事件监听:天然满足服务订阅功能 5. 临时节点:注册临时节点作为服务提供者,当服务提供者断开的时候自动删除对应节点,触发事件监听 ## 3.3 注册中心编码 ![img](asset/clip_image002-1679381839279.jpg) 图3.1注册中心UML **Registry**:定义注册中心核心功能的接口; **ZookeeperRegistry**: 基于zookeeper的注册中心的实现类, 实现了注册中心的全部功能: 1. 注册服务:创建一个zookeeper的临时节点 2. 取消注册:删除节点 3. 订阅节点:订阅一个节点的父目录,并缓存监听器。当子节点发生变化的时候进行通知 4. 取消订阅:移除子节点监听器 5. 发现服务:查询出对应的全局子节点,并进行数据结构转换 ![img](asset/clip_image001.png) 图3.2注册中心工厂UML **RegistryFactory**:注册中心工厂:选择合适的注册中心,默认为zookeeper, 支持扩展 **ZookeeperRegistryFactory**:基于zookeeper的注册中心工厂 **其他相关类简单说明:** NotifyListener:接口,当注册中心的服务发生变化时候的回调通知接口,全量通知。 ChildListener:子节点变化监听器,发出子节点发生变化的通知 **XqdZookeeperClient**:基于Curator对zookeeper的一些基本操作进行简单的封装。并进行部分缓存工作,如缓存持久节点,缓存监听器等。功能包括:创建递归临时/持久节点,创建/删除节点,创建/删除监听器,查询路径等等。 XqdCuratorWatcherImpl:自定义zookeeper的监听,监听节点新增,删除,更新等时刻的事件,触发ChildListener的事件回调。 RegistrySingle:获取当前项目中单例的注册中心, 屏蔽一些复杂代码 。是一个典型的基于双重锁检测的单例模型。 # 第四章**cluster** **集群处理层** 一个服务的提供者往往是集群的,故存在路由选择的问题。当系统处于特殊的高并发的情况下(如类似双十一活动),理应提供服务降级处理。另外当服务提供者发生错误的时候,应提供服务容错的功能。另外,也应该考虑数据查询服务接口应提供缓存调用结果的功能。本**cluster** **层就是基于这样的考虑而设计的。** **但是限于时间和篇幅的问题,xqd-rpc暂时只提供了部分负载均衡方法的扩展点。** ## 4.1 LoadBalance 简介 LoadBalance 意为负载均衡,它的职责是将网络请求,或者其他形式的负载“均摊”到不同的机器上。避免集群中部分服务器压力过大,而另一些服务器比较空闲的情况。通过负载均衡,可以让每台服务器获取到适合自己处理能力的负载。在为高负载服务器分流的同时,还可以避免资源浪费,一举两得。负载均衡可分为软件负载均衡和硬件负载均衡。硬件F5负载均衡作为开发者一般很难接触到。但软件负载均衡还是很常见的,比如 Nginx, spring cloud中使用的Ribbon等。 ## 4.2 xqd-rpc中的负载均衡扩展点 xqd-rpc中的负载均衡为客户端均衡策略。若需要服务端均衡,可考虑使用nginx为服务端做proxy,实现服务端的负载。 ### 4.2.1 扩展点接口cn.xuqiudong.rpc.cluster.balance.LoadBalance > 详见代码 负载均衡策略扩展点,根据传递而来的多个服务提供XqdUrl列表和Rpc调用信息Invoker选择某个远程服务: 1. 若数量唯一,则直接返回这个资源地址 2. 若存在多个资源,则选择相应的均衡策略,根据策略的算法返回远程服务资源地址 ### 4.2.2 随机负载均衡策略RandomLoadBalance(random) Xqd-rpc中设计的较为简单,只随机选择一个服务资源返回。 后续可考虑加入权重信息,即通过加权随机算法获得一个资源。若如此设计,则需要在服务提供者端注册服务的时候,提供给注册中心自身的权重信息。 ### 4.2.3 基于一致性hash算法的负载均衡ConsistentHashLoadBalance(hash) 一致性hash算法在分布式集群环境下的使用场景非常的广泛,比如各种中间件的负载均衡,数据分片,动态扩容等等。 它的基本原理比较简单:就是在一个hash环上(如范围0-2^32-1)计算服务器节点的hash值,如果一个object要寻找应该路由的服务器节点,则计算其hash值,并在环上顺时针查找离它最近的节点。 这里的ConsistentHashLoadBalance只是进行了最简单最基本的一致性hash算法的实现: 1. 把url列表转为SortedMap, key为hash(通过HashCodeBuilder计算),value为url 2. 计算出客户端请求的hash:根据相同的参数计算出来, 即相同的参数应落在一个服务端 3. 得到SortedMap中第一个大于hash的url 4. 如果SortedMap没有对应hash的url,则取第一个 > 详见代码 `ConsistentHashLoadBalance ` ## 4.3 Cluster 集群的入口 ClusterFacade ClusterFacade类作为集群门面入口,暂时比较简陋,只引入负载均衡(以及服务订阅) 后续可考虑引入熔断和消费降级等。 ![img](asset/clip_image002-1679382040929.jpg) 图4.1集群门面UML 暂时主要提供了以下功能: 1. 从注册中心或缓存获取URL 并进行负载均衡: 根据客户端调用信息Invoker,从缓存中获取服务列表(若缓存中没有则去注册中心中查询,然后存入本地缓存),然后根据负载均衡策略返回一个服务端资源url 2. 订阅注册中心的url更新推送,实时更新本地缓存 > 详见代码 `ClusterFacade ` # 第五章序列化/反序列化 数据的传输形式是简单的字节序列形式传递,即在底层,系统不认识对象,只认识字节序列,而为了达到进程通讯的目的,需要先将数据序列化,而序列化就是将对象转化字节序列的过程。相反地,当字节序列被运到相应的进程的时候,进程为了识别这些数据,就要将其反序列化,即把字节序列转化为对象 在xqd-rpc框架中,消费端和服务端的通信可以基于netty提供的TCP,也可以基于http(服务端为tomcat容器),但是不分管哪一种通信方式,数据都需要转为byte数组数传,接收的数据流亦需要转化为java对象。 Xqd-rpc提供了三种序列化和反序列方式,json、jdk、hessian(默认)。 Xqd-rpc框架中所有参与远程调用的对象均需要实现Serializable接口。 ## 5.1 序列化/反序列化扩展点 扩展点接口:cn.xuqiudong.rpc.serializer.XqdSerializer 定义了对象的序列化/反序列化方法以及是否自描述,默认的方式为hessian ![img](asset/clip_image002-1679382097275.jpg) 图5.1 序列化/反序列化UML ## 5.2 三种序列化/反序列化方式简单说明 Xqd-rpc中的序列化和反序列化代码都较为简单,并没有做太多的额外功能扩展,只单纯的调用了相关API进行了对象和字节数组之间的相互转化。 其中jdk和hessian序列化和反序列化是自我描述的,序列化和反序列化都不需要额外的操作。但是需要注意的是Hessian 序列化时,先序列化子类,然后序列化父类,因此反序列化结果会导致子类同名成员变量被父类的值覆盖。 而json方式的序列化/反序列化使用的是jackson提供的ObjectMapper实现的,由于序列化的时候并不包含类信息描述,所以反序列化的时候稍微麻烦一下,特别是多层泛型嵌套的情况下,框架根据序列化方式是否自描述在远程调用返回后进行再次序列化反序列化以得到实际泛型类型。 # 第六章 通信协议Protocol ``` 作为xqd-rpc框架的核心通信功能,封装了RPC调用过程,以Invoker、XqdUrl 、XqdRequest、XqdResponse数据模型为基础,对上向代理层提供封装好的远程调用过程, 对下使用序列化/反序列化扩展点,使用注册中心通过的服务,以及负载均衡扩展点的功能。屏蔽了复杂的数据通信。 ``` ## 6.1 通信协议模块核心功能 ![img](asset/clip_image002-1679382111540.jpg) 图6.1通讯协议UML ### 通信扩展点接口:cn.xuqiudong.rpc.protocol.Protocol Protocol协议接口定义了如下方法: 1. 根据端口号启动服务方法start,为服务提供端使用 2. 根据Invoker发送消息给服务端方法,为客户端使用 ## 6.2基于netty的Tcp通信协议XqdProtocol ### 6.2.1 netty通信数据格式定义 ![img](asset/clip_image002-1679382145631.jpg) 图6.2 netty通信协议UML **NettyHeader****:netty 通信数据格式的头部分(总长度16字节);** Magic:魔数-用来验证报文的身份(2个字节) serialType:预留的序列化方式(1个字节) requestType:操作类型:请求/响应(1个字节) requestId:请求标识id(8个字节) length:本次请求体的数据长度(4个字节) **NettyProtocolData****:netty 通信数据格式:** Header:请求头标识 ​ Body: 实际传输的数据(XqdRequest或XqdResponse) ### 6.2.2 自定义netty编码器和解码器 #### 6.2.2.1 编码器XqdEncoder 按照NettyHeader的定义逐个写入即可 > 详见代码 `XqdEncoder ` #### 6.2.2.2解码器XqdDecoder 如果读取的数据长度不足NettyHeader的长度(16个字节),则继续等待。否则,读到到数据组装为NettyHeader,并根据其中的数据长度,继续读取对应长度的字节,然后根据header中的请求数据类型,转为为对应请求body或响应body. > 详见代码` XqdDecoder ` ### 6.2.3 netty服务端编码 ![img](asset/clip_image002-1679385617738.jpg) 图6.3 netty服务端UML `XqdNettyServer`作为服务端的入口,根据端口号,启动netty服务。并指定其编码器解码器为自定义的编码器解码器,且在netty管道中添加自定义的数据接收逻辑处理器为XqdNettyServerHandler, 在XqdNettyServerHandler中最终调用XqdChannelRunnable线程处理请求返回数据 > 详见代码 `XqdNettyServerHandler` `XqdChannelRunnable` XqdChannelRunnable线程根据请求体,通过XqdServiceHolder查找到对应服务端的服务提供者对象,然后根据参数通过反射找到对应方法,然后执行方法,返回结果给客户端。 关于**XqdServiceHolder**,其为服务端持有spring中的接口方法实现类的实例对象的工具类,在后续的框架和spring集成模块会讲解。 ### 6.2.4 netty客户端编码 ![img](asset/clip_image002-1679385700106.jpg) 图6.4 netty客户端UML XqdNettyClient:netty客户端,提供和netty服务端通信的功能,基于Invoker,构建NettyProtocolData 格式的数据发送给服务端。 XqdNettyClientHandler:接收到服务端返回数据后的处理逻辑。 ### 6.2.5 XqdProtocol功能概要 XqdProtocol实现了Protocol,在start方法中实例化XqdNettyServer,并启动服务。 在send方法中,完成和服务端的数据交互。 > 详见代码 XqdProtocol #### Send方法概述与异步转同步 虽然Netty的通信是异步非阻塞的,但是接口的RPC调用一般却是需要同步的,所以这里需要设计一种方法,能够把异步的RPC调用转为同步,即:阻塞接口,异步调用服务,在服务端返回数据后,解除阻塞,同步返回结果给调用者。 这里的异步转同步,主要是在发起远程调用的时候设置请求标识requestId ,在异步请求返回后,找到对应Future把数据设置进去,然后阻塞线程唤醒。 **Send**方法: 1. 从ClusterFacade中获取到当前客户端请求的服务端资源地址 2. 构建可复用的XqdNettyClient client 3. 设置请标识requestId 4. 构建XqdFuture future; 5. 把future存放RequestHolder中其中的key为当前请求标识 6. 调用client异步发送数据 7. future.getPromise().get()阻塞请求,等待返回值 在XqdNettyClientHandler接收服务端请求数据的方法中,会根据服务端返回的请求头中携带的requestId,从RequestHolder中获取future,调用其future.getPromise().setSuccess(msg.getBody());设置请求结果。 XqdFuture数据格式如下: ![img](asset/clip_image002-1679385847416.jpg) 图6.5 异步转同步UML 其持有netty的Promise接口。Promise 接口继承自 Future 接口, 在 Future 基础上提供了设置处理结果的功能. ## 6.3 基于tomcat的http通信协议HttpProtocol ![img](asset/clip_image002-1679385861459.jpg) 图6.6 tomcat协议UML 在HttpProtocol的start方法中调用XqdHttpServer启动tomcat服务。 在其send方法中通过HttpURLConnection完成和服务端的通信工作。 整体设计方式类似基于netty的XqdProtocol,但是更为简单,此处省略详细描述,详见代码。 # 第七章 代理对象生成ProxyFactory ## 7.1 动态代理概要 客户端需要为服务接口生成一个代理类,对接口方法实现远程方法的调用,并返回调用结果。到这一步为止,大部分的功能都已经完成,只要在生成代理类的方法中把上述相关功能组装起来即可完成RPC远程调用。 Java中可以实现创建代理对象的方法有很多,比如基于JDK自带的Proxy 类创建代理对象(基于接口),或者使用cglib 的 Enhancer 类创建代理对象(基于子类),还可以通过Javassist创建代理类。 ## 7.2 动态代理扩展点 接口:cn.xuqiudong.rpc.proxy.ProxyFactory 只要在对应的实现类中实现获取代理对象即可,其中的代理对象的方法中需要完成远程调用工作。 ![img](asset/clip_image002-1679385923043.jpg) 图7.1 JDK动态代理UML JdkProxyFactory实现了ProxyFactory,通过jdk自带的Proxy.newProxyInstance创建代理对象。 JdkProxyInvocation:为代理方法中实现了真正进行远程通信的过程。 JdkProxyInvocation持有Protocol, 并且组装好Invoker 对象(包括其中的请求对象XqdRequest,接口信息等)通过Protocol的send方法发送数据给服务端。 # 第八章 和spring的集成 一个rpc框架很少存在单独运行时机,而是一般集成到容器中去运行的。在第一章的xqd-rpc架构图中,就表明了无论是客户端还是服务端都是在容器中运行的。 这里说的容器就是指spring。为什么选择spring作为容器,是因为Spring框架为开发Java应用程序提供了全面的基础架构支持。它包含一些很好的功能,如依赖注入和开箱即用的模块。是当前java开发最流行的开发框架,可以说java生态的繁荣除了JVM的跨平台之外,还和spring息息相关。 ## 8.1 服务端和spring的集成 ### 8.1.1 xqd-rpc服务端和spring集成需要的功能 1. 在服务启动的时候,调用Protocol,启动rpc服务。 2. 服务提供者接口实现类在注入到spring容器的同时把相关信息到注册中心 3. 持有接口实现类在spring容器中的实例的引用。当服务端Protocol接收到请求的时候可以找到这个实例进行业务处理。 ![img](asset/clip_image002-1679385962083.jpg) 图8.1 服务提供者集成spring后置处理器UML ### 8.1.2 相关编码说明 **XqdService**:自定义注解,且该注解被标记为@Component。用此注解标记服务接口实现类,在会被扫描到spring容器,且后续根据此注解,实现自定义功能。 #### 8.1.2.1 集成spring的入口XqdProviderAutoConfigurationspring 服务端集成到spring的入口,在这个类中获取到配置的RPC服务端口号,然后向spring注册了XqdSpringProviderBeanProcessor #### 8.1.2.1 利用InitializingBean启动RPC服务 XqdSpringProviderBeanProcessor实现了InitializingBean接口。 InitializingBean是Spring提供的拓展性接口,InitializingBean接口为bean提供了属性初始化后的处理方法,它只有一个afterPropertiesSet方法,凡是继承该接口的类,在bean的属性初始化后都会执行该方法。 在`afterPropertiesSet`中完成了启动服务rpc服务的过程: #### 8.1.2.2利用BeanPostProcessor注册服务信息到注册中心 XqdSpringProviderBeanProcessor实现了BeanPostProcessor接口。 BeanPostProcessor也称为Bean后置处理器,它是Spring中定义的接口,在Spring容器的创建过程中(具体为Bean初始化前后)会回调BeanPostProcessor中定义的两个方法: postProcessBeforeInitialization和postProcessAfterInitialization。 此处重写postProcessAfterInitialization,即在每个bean对象的初始化方法调用之后被回调。如果这个bean被XqdService标记,在注册到注册中心,且通过XqdServiceHolder(前面章节6.2.3中netty服务端编码中需要此类)保留对bean的引用。 ## 8.2 客户端和spring的集成 ### 8.2.1 xqd-rpc客户端和spring集成需要的功能 在spring初始化的时候,如果服务接口作为属性被注入到某个spring管理的bean对象中,则应该为此接口生成一个可以完成远程通信代理对象,此对象被被spring管理,且也是单例的。 ![img](asset/clip_image002-1679386022003.jpg) 图8.2服务消费端集成spring后置处理器UML ### 8.2.2 相关编码说明 XqdReference:自定义注解,且该注解被标记为@Autowired。用此注解标记springBean中注入的服务接口属性会被spring依赖注入。Xqd-rpc框架会在其实例化之前把代理类注册到spring容器,用于其后面的依赖注入。 #### 8.2.2.1集成spring的入口XqdConsumerAutoConfiguration 在此入口类中向spring容器注册XqdSpringReferenceBeanProcessor。 #### 8.2.2.2利用BeanFactoryPostProcessor注册服务接口代理类到spring容器 XqdSpringReferenceBeanProcessor实现了BeanFactoryPostProcessor, BeanFactoryPostProcessor是spring对外提供的接口,用来拓展spring,能够在spring容器加载了所有bean的信息(beandefinition)之后、bean实例化之前执行,修改bean的定义属性,此时bean尚未初始化。 在重写的postProcessBeanFactory方法中, 1. 获取到所有的bean定义,反射获得Bean的class信息 2. 遍历Class的所有属性,如果属性被注解XqdReference标注,则获取到这个Field的信息,新建一个BeanDefinition,这个BeanDefinition最终通过XqdBeanFactory生成一个代理bean,这个代理bean会本注册到spring中 3. 最终通过spring的依赖注入把代理bean注入给相应的bean。 XqdBeanFactory 实现了FactoryBean,持有ProxyFactory,最终通过代理工厂生成一个代理类。 > 详见代码 `XqdSpringReferenceBeanProcessor` > > # 第九章 demo与集成测试 ## 9.1 新建测试项目进行代码集成测试 至此,xqd-rpc-core部分的代码基本完成。新建xqd-rpc-demo项目简单的集成测试。 代码结构如图: ![img](asset/clip_image001-1679386144237.png) 图9.1 测试项目结构 xqd-rpc-demo-common:为服务端和客户端共用代码,包括model和服务接口等。 xqd-rpc-demo-consumer:为消费端springboot项目,依赖xqd-rpc-demo-common和xqd-rpc-core。在测试service里注入服务接口作为属性,并被@XqdReference标注。 xqd-rpc-demoprovider:为服务端springboot项目,依赖xqd-rpc-demo-common和xqd-rpc-core。并实现common中的服务接口,且在实现类上标注@XqdService注解。 其他代码略, 分别启动服务端和客户端,通过浏览器(或单元测试)访问consumer中的测试service接口的方法,可以成功调用到服务提供方的服务。 ## 9.2 部分测试过程说明: ### 9.2.1 启动zookeeper zookeeper版本为3.7.1),通过可视化客户端ZooInspector查看此时zookeeper中的只存在原始节点zookeeper ![img](asset/clip_image002-1679386158726.jpg) 图9.2 zookeeper初始数据展示 ### 9.2.2 启动服务提供端 打包xqd-rpc-demoprovider服务提供端,并复制两份(provider001,provider002,provider003); 1、 配置相同的zookeeper为注册中心 2、 三者配置不动的server.name和server.port 3、 三者配置不同的protocol.port(rpc通讯协议端口,默认netty通信),若“接口名+ip:port”相同,则后者的接口提供者无法注册到注册中心 4、 启动服务提供端provider001和provider002、provider003 ![img](asset/clip_image002.png) 图9.3 测试服务端启动注册到zookeeper 此时zookeeper中的信息如下: ![img](asset/clip_image002-1679386181885.jpg) 图9.4 注册单个服务提供者后的zookeeper节点数据 其中: a) Xqd 为xqd-rpc的根节点 b) 二级子节点cn.xuqiudong.rpc.demo.common.api.DemoInterface为接口的全路径 c) 路径的子节点proviiders表示当前目录为服务提供服 d) Providers的子节点为UrlEncode编码后的接口通信地址信息, 如上图 192.168.0.225%3A20066%3FinterfaceName%3Dcn.xuqiudong.rpc.demo.common.api.DemoInterface编码前为: 192.168.0.225:20066?interfaceName=cn.xuqiudong.rpc.demo.common.api.DemoInterface 当启动provider002没有修改通信协议端口protocol.port的时候,则提示重复注册provider;且如果provider部署在相同服务器,则通信框架netty(或者tomcat)因端口占用而启动失败,如下图所示: ![img](asset/clip_image002-1679386194616.jpg) 图9.5 重复注册提供者展示 修改配置文件,启动provider002和provider003,此时zookeeper中的节点信息如下: ![img](asset/clip_image002-1679386204900.jpg) 图9.6 注册多个服务提供者集群后的zookeeper节点数据 如上图所示,每个接口下的providers包含三个服务提供者; 此时,关闭provider001和provider002,则zookeeper中接口下的providers下的提供者数量相应减少两个,只剩下provider003。如下图 ![img](asset/clip_image002-1679386212746.jpg) 图9.7 删除某个服务提供者后的zookeeper节点数据 因为,注册中心中注册的提供者节点为临时节点,当session关闭的时候,节点会被删除。此时会触发相关监听器,消费端本地缓存中会更新服务提供者列表。 ### 9.2.3 启动服务消费端 xqd-rpc-demo-consumer消费端注册中心配置和提供端一致,rpc通信协议为netty的时候,需配置消费端netty本身的协议端口 ![img](asset/clip_image002-1679386223492.jpg) 图9.8 服务消费者启动后动态对象注入 ### 9.2.4通过浏览器调用相关实例请求 以下截图只展示某个接口(消费端一次调用10次接口) 1、 只启动provider001的时候,所有的客户端请求均落在provider001 ![img](asset/clip_image002-1679386230706.jpg) 图9.9 单个提供者消费端请求记录展示 2、 启动provider002,继续测试本请求,则客户端请求有的随机落在provider001和provider002 ![img](asset/clip_image002-1679386251800.jpg) 图9.10 两个提供者消费端请求记录展示 3、 继续启动provider003,测试本请求,则客户端请求有的随机落在provider001、provider002和provider003上 ![img](asset/clip_image002-1679386258750.jpg) 图9.11 三个提供者消费端请求记录展示 4、 此时关闭provider003,则在消费端输出如下日志,跟新了消费端的服务提供者列表 ![img](asset/clip_image002-1679386265157.jpg) 图9.12 关闭某个提供者时消费端动态更新缓存展示 5、 再次进行请求,则所有请求落在provider001和provider002 ![img](asset/clip_image001-1679386272762.png) 图9.13 动态删除提供者后消费端请求记录展示 如上步骤,所有的请求均随机请求到不同的提供端,因为xqd-rpc的默认负载策略为random策略。当修改策略为一致性hash算法的时候,新增和删除provider节点的时候,请求分布会有所不同(本项目中的一致性hash只简单的使用了hash环,并没有通过新增虚拟节点的方式,增加负载的平衡性),测试过程略。 # 总结 本论文的编写,一方面是毕业设计的需求,另一方面是一直以来,盘旋在我脑海中的一个想法。虽然上网关于如何编写一个rpc框架的文章有很多,但是大多数都较为简略,而且设计的不是很全面。故萌发了自己编写一个rpc框架的念头。 随着互联网的发展,分布式架构的流行,网路编程的重要性显而易见,但是在实际的开发工作中,一般很少用到网络编程的技术。也想借此机会,稍微了解一下netty相关的网络编程,为此,我也简单通读了一下《netty in action》与《Java Concurrency in Practice》两本著作,奈何我技术有限,只能简单走马观花一睹,收获甚微。 在xqd-rpc编码的过程中,本人基本遵守了《代码的简洁之道》以及《阿里巴巴代码规约》中的代码规范,包括命名、作用域、安全性,OO(面向对象),异常处理等等,比较注意代码的规范与整洁。另也在框架中适当运用了一些设计模式,比如单例模式、工厂模式、代理模式,观察者模式等。 受限于时间的问题,xqd-rpc依然有许多的不足之处: 1. 注册中心,应该在创建连接的时候添加更多的监听事件,以及设计重连机制。 2. 在代理类的invoke方法中,由于json序列化的特殊处理,显得不够优雅。 3. 在基于netty的Ptotocol中暂时统一使用了异步转同步的策略,而没有采取根据动态配置既可以同步,也可以异步,还可以使用Future。 4. 对于服务接口实现类(被注解XqdService标记的)暂未处理多实现的问题。 5. 对于XqdService和 XqdReference注解,没有设置更多的属性,比如:多版本,是否延迟加载,直连设置,分组设置,动态配置(如负载均衡),超时重试,mock数据等等。因为这样设计的话,项目的复杂度会直线上升,限于篇幅和时间,暂时搁浅。 6. Cluster集群路由层,设计的十分简陋,只实现了两种负载均衡策略,在原本的设想里,应该包括熔断机制,降级机制,mock机制,缓存机制。 7. 没有设计配置中心:由于服务端和客户端的配置相对简单,走的是全量配置,所以没有设计配置中心。 8. 没有设计监控中心:在服务端和客户端运行的过程中,理应把相关运行详细写入到监控中心,监控中心是可视化的。 9. 没有做好模块划分:在原本的设计中,不同的模块应该是划分为不同的mavan子模块的,但是由于时间和只有本人一个开发,暂未划分。 10.没有处理服务提供端和调用端循环依赖的问题。 11.没有做完整的单元测试,只进行部分功能点的简单测试。 通过对一个框架的设计与编写,可以清楚的意识到,一个可在生产环境中使用的成熟框架的设计的远远比我们想象中要复杂的多。不但要考虑通用性,兼容性,可扩展性,各种分支情况,在代码层面也要充分考虑到代码的简洁性、健壮性,可读性,代码间的组合,设计模式等等诸多问题。 作为一个demo级别的框架,我完成了xqd-rpc的基本功能点的开发,在完成我的论文的同时,我也希望能对那些有志阅读框架源码的同学有所帮助,抛砖引玉,能窥此斑而知阅读源码的要点,激发同学学习的热情。 > by 许秋冬 2022-2023