# T_RPC_Framework
**Repository Path**: TicsmycL/t-rpc-framework
## Basic Information
- **Project Name**: T_RPC_Framework
- **Description**: 一个基于Netty的RPC框架,使用Java实现,与Spring整合,提供SpringBoot Starter。是一个运行在传输层,不需要额外配置,基于注解开箱即用的rpc框架,结合了Dubbo的高性能OpenFeign简单易用的优点,提供rpc框架的第三种解决方案
- **Primary Language**: Java
- **License**: MulanPSL-2.0
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 4
- **Forks**: 4
- **Created**: 2020-10-23
- **Last Updated**: 2022-05-15
## Categories & Tags
**Categories**: rpc
**Tags**: None
## README
# T_RPC_Framework
一个rpc远程过程调用的框架。
## 使用方法
> 见项目 : **[T_RPC_Framework_Demo](https://gitee.com/TicsmycL/t_rpc_framework_demo)**
步骤(基于3.8版本):
0. 启动nacos
1. 引入依赖
```
fun.ticsmyc.rpc
t-rpc-all
3.8
```
2. 在resources目录下,放入trpc.properties 配置文件 (可选)
```properties
port=8999 # 作为服务提供方,使用的端口号 (默认是8888)
loadbalancer=round # 作为服务消费方,使用的负载均衡机制,可选random(随机)和round(轮询),默认是随机
serializer=json # 作为服务消费方,发送rpc请求时使用的负载均衡器,可选kryo(默认)、json、protobuf、hessian
networkIO=netty #可用netty和socket 。 亲测netty更快
nameServiceAddress=127.0.0.1:8848 #注册中心地址
```
3. 在配置类声明
```
@EnableTRPC
```
4. 提供服务:
在想要提供的服务接口上声明
```java
@TRPCInterface //这一步是为了解决某个实现类实现了多个接口的情况
```
在想要提供的服务实现类上声明
```java
@TRPCService(group="t") // group属性是为了处理一个接口想要注册多个实现类的情况消费服务
```
5. 消费服务:
通过@RpcClient注解即可注入rpc服务
```
@RpcClient(group = "t")
private HelloService helloService;
```
### 原理图

## 目录说明
```
└─src
└─main
├─java
│ └─fun
│ └─ticsmyc
│ └─rpc
│ ├─client :客户端
│ │ ├─annotation:供客户端使用的注解
│ │ ├─proxy :动态代理
│ │ └─transport :网络传输层
│ │ ├─bio :基于socket实现的传输
│ │ └─netty :基于netty实现的传输
│ │ ├─codec :编码、解码器
│ │ └─handler :自定义的处理器
│ │ └─util:客户端使用的工具类
│ ├─common :通用
│ │ ├─entity :网络通信使用的实体类
│ │ ├─enumeration :枚举类
│ │ ├─exception :异常类
│ │ ├─factory :工厂
│ │ └─serializer :序列化器
│ │ └─impl
│ ├─nacos
│ │ ├─loadbalance 负载均衡器
│ │ │ └─impl
│ │ └─registry 注册中心
│ │ └─impl
│ ├─server :服务端
│ │ ├─annotation:客户端使用的注解
│ │ ├─handler :业务:根据收到的信息调用相应服务
│ │ ├─provider:服务端本地使用的服务注册
│ │ │ └─impl
│ │ └─transport :网络传输层
│ │ ├─bio
│ │ └─netty
│ │ ├─codec
│ │ └─handler
│ └─test
└─resources
```
## 最新版本说明
> 其他版本记录见目录下 [历史版本记录.md](./wiki/历史版本记录.md)
### v3.8
- 优化代码结构
- 修复:SingletonFactory中错误的双重判断
- 修复:对线程不安全的Kryo序列化器错误的单例使用方式
- 优化:服务注销时,使用更粗粒度的锁
- 新增:增加Nacos服务列表本地缓存机制
- 新增:外部配置文件新增对Nacos注册中心地址的配置
- 优化:static初始化顺序
- 优化:rpc服务端Netty启动时间调整为IOC容器完全启动后,通过Listener实现
- 修复:如果有多个网卡,可能导致ip地址获取错误的问题
- 修复:打成jar包后,读取不到properties配置文件的问题
### v3.7
- 优化代码结构
- 使用 Runtime.getRuntime().addShutdownHook 机制 和DisposableBean机制来保证系统正常退出时,从nacos中注销服务
- 修改nacos中注销服务的方法为线程安全
- 优化了使用方式,用户只需要在配置类中声明@EnableTRPC,即可自动注册发布服务、自动为接口注入实现类
- 可以使用trpc.properties配置文件来配置端口、覆盖均衡器、序列化器
- 修复:bootstrap.connect().sync()监听器线程同步错误,导致多次连接重试,最终与同一个服务器建立多个连接的情况。
- 修复:json序列化器对心跳包进行序列化时的空指针问题
- 修复:当Spring容器使用cglib为bean生成代理时,RpcClient不能正常注入的问题。
- 修复:当Spring容器使用jdk动态代理为bean生成代理时,RpcClient不能正常注入的问题。
- 优化:根据代理对象找到原始对象,进行RpcClient注入。
- 修复:轮询负载均衡策略 int溢出 的问题
- 修复:当调用代理类的Object方法 或者 代理类特有的方法时,不触发rpc逻辑
## 一些令人骄傲的设计和bug修复
> 还有一些不太骄傲的设计,在 [历史问题与优化记录.md](./wiki/历史问题与优化记录.md)
### 一个接口对应多个实现类的处理
#### 从AOP代理对象中找被代理对象
> fun.ticsmyc.rpc.common.util.AopTargetUtils 这个工具类。 用于从代理对象中找到原始对象。
>
> - 在服务端发布服务时使用: 为了获取代理对象上的自定义注解,从而知道这个Service是哪个分组 。
> - 在客户端:为接口注入服务代理类时使用。遇到的问题之一是因为使用了@Transactional,导致这个Bean使用cglib代理,从而导致扫描不到@RpcClient注解
- 为什么要用到这个
- 在一个接口对应多个实现类的情况下,Service端注册服务时,使用的名称是【接口名称_所属服务分组】,需要区分这几个服务。 Client端生成代理类时,也需要明确得知指出调用的是这个接口的哪个实现类。
- Service端的服务实现类所属服务分组 使用自定义注解在实现类上标注。 Client端的接口调用的实现类所属分组在字段上使用自定义注解标注。
- 对于复杂的实现类,如使用了@Transactional注解的实现类,加入IOC容器时,会被Spring生成代理类,IOC容器中放的是代理类。 代理类上没有被代理类的注解。
- 所以需要根据代理类找到被代理类,然后再用反射的方式读出代理类上的注解,才能获取到这个服务的分组。
- 具体实现
- 共有三种情况 ,可以通过AopUtils这个工具类中的方法判断。
1. 未被代理 : 直接反射读注解即可。
2. 使用JDK动态代理:根据代理类的生成规律,找到内部的被代理类,反射读取被代理类中的注解
3. 使用cglib代理:同上
- 这种方法其实并不可靠, 一般写动态代理,都在内部保存被代理类,但是Spring的动态代理内部保存的是TargetSource类型
- TargetSource有多种实现方式, 如果是SingletonTargetSource,则内部就存一个单例的被代理类,但如果是其他类型,则代理类的结构会发生变化, 这种根据代理类找被代理类的方法就会失效。
#### 在服务消费方为什么不能像MyBatis一样直接使用IOC容器进行注入,而要自定义注解手动注入
MyBatis用的是ImportBeanDefinitionRegistrar (被BeanFactoryPostProcessor处理), 获取注解上配置的元数据(basepackage路径)。Spring整合Mybatis的原理如下:
1. 间接使用BeanFactoryPostProcessor,在IOC容器初始化第一阶段(注册BeanDefinition)结束后,进行扩展:
1. 扫描所有的Mapper,拿到BeanDefinition。 这个BeanDefinition的id是对应的Mapper(可以注入)
2. 修改BeanDefinition中保存的对象类型,为MyBatis内部定义的一个FactoryBean。
- 这个FactoryBean重写getObject方法为: 调用SqlSession的getMapper方法
3. 这时,对Mapper接口进行注入时,实际时调用那个FactoryBean的getObject方法,就可以获取到相应的Mapper。
不难发现,MyBatis的每个Mapper接口,都有且仅有一种确定的实现类, 所以对于每个Mapper接口都可以创建一个FactoryBean用于创建Bean。
而这个Rpc框架中,因为考虑到一个接口对应多种实现类的情况,每个接口可能需要多个FactoryBean,情况很复杂。所以干脆自定义注解,然后在BeanPostProcessor中扫描注解,然后根据接口名和分组,生成代理类,手动使用反射进行注入。
### 客户端的失败重连机制
> fun.ticsmyc.rpc.client.transport.netty.NettyRpcClient的 45-53 行
客户端发送请求的方式: 先根据请求方法和所在的组,从nacos获取到服务提供者的ip和端口。 然后使用netty发起网络请求。
最初重试机制放在了拿到ip端口之后, 如果连接不成功,会重复连接。感觉也没什么问题。
测试时发现, 由于nacos的延迟,当服务提供者频繁上下线时,nacos中的信息不会及时更新,导致客户端拿到的ip和端口是过期的,多次重试仍连接不上。
最后修改为,每次重连都重新从nacos拉取一次服务提供者ip。
### json序列化时反序列化失败的情况
> fun.ticsmyc.rpc.common.serializer.impl.JsonSerializer的 90-107行
json是文本序列化器,反序列化时如果不知道原始类型, 可能会导致反序列化失败。
- 如果使用Object类型接收反序列化后的Object,无法识别原始的类型,会变成String或者其他奇怪的类型。
- 如Date对象会反序列化为Long,嵌套的RpcRequest对象会被反序列化为LinkedHashMap。。。。
- 所以只能在请求体里面带上参数的Class对象,在反序列化之后判断是否反序列化正确。如果未正确反序列化,就序列化成二进制,根据原始类型再反序列化一次。
【这种场景下,使用基于二进制的序列化器更好,以下是几种二进制序列化器的优劣测评】
1. protobuf序列化器
- 使用方式
- 可以自己写.proto文件(每个类都要对应一个proto文件),然后使用他提供的代码生成器生成scheme(也是一个.java文件),加入项目中,性能高一点,但很麻烦,而且类的结构一变,就得重写proto文件
- 引入protobuf-runtime,在运行时根据传入的类,使用字节码生成技术,动态生成该类。
- **在本地使用并发Map存储,使用懒加载单例的双重验证机制存储生成过的schema。避免多次重复生成shcema。 初次性能低,但使用方便。**
2. Kryo序列化器
- 只能在java中使用。性能很好。比kyro更高效的序列化库就只有google的protobuf了
- 本身不是线程安全的,所以要存在ThreadLocal中。 不能做成单例的。
- 遇到了ThreadLocal的内存泄漏问题!!!
3. Hession序列化器
- 编码后长度短,但性能低
- 拿来凑数 不太了解
### 客户端连接同步错误,导致的一个服务端多个心跳包现象
> fun.ticsmyc.rpc.client.transport.netty.RpcRequestSender 的 42-49 行
该bug表现为: 【只与一个服务器建立了连接, 每次却有若干个心跳包发送】
```java
bootstrap.connect(xxxx).addListener( ()->{
//代码1
this.channel = sync.channel();
}).sync();
//代码2
```
连接建立之后,代码1和代码2在两个线程中同步执行,无法保证代码1和代码2执行的先后顺序。
在代码1区域为channel赋值的操作可能晚于代码2发生,导致线程同步错误。 应该等到代码1执行完毕后,代码2再执行。
如果代码2先于代码1执行,因为此时channel还未赋值,检测为null,会触发重连操作。 最终系统中会与这一个服务器维持多个连接,导致每次发送多个心跳。
### **BeanPostProcessor导致@Value失效**
> fun.ticsmyc.rpc.Config 这个类的static代码块
场景: 想要将服务端配置文件从static改成@Component。 使用properties文件编写配置,使用@Value进行注入。
使用InitializingBean进行赋值。 发现属性还处在配置文件引用阶段("${}"这样),没有替换成配置文件的内容。
- @Value获取不到值的场景:
- 将properties的值 使用@Value("${}")注入到 RpcProperties类中。
- 将RpcProperties注入到另一个类中。 注入Config类时,只能获取到${}字符串。。。,注入其他类却可以正常获取
- 最后发现,是**BeanPostProcessor导致@Value失效** 。 当把使用了@Value的Bean直接或者间接的注入到BeanPostProcessor中时,会导致@Value失效。即使BeanPostProcessor中根本没有用到@Value的值
- 原因: BeanPostProcesser的实例化按照优先级分批进行,优先级高的先于优先级低的进行实例化。 在实例化时,内部依赖的Bean也会实例化。这些被依赖的Bean因为实例化太早,无法享受同等优先级以及更低优先级BeanPostProcesser的处理,所以@Value不会替换。
- @Value被AutowiredAnnotationBeanPostProcessor处理,这个BeanPostProcessor也是PriorityOrdered级别的。
- 详细信息在PriorityOrdered接口的注释中有提到
```JAVA
*
Note: {@code PriorityOrdered} post-processor beans are initialized in
* a special phase, ahead of other post-processor beans. This subtly
* affects their autowiring behavior: they will only be autowired against
* beans which do not require eager initialization for type matching.
```
如果在bean启动的过程中需要通过BeanPostProcessor注册服务,所以必须保证在bean容器初始化的过程之前就读取好了配置文件的内容,所以还是用static比较合适,【但是static乱序初始化也容易造成nullptr】。
### idea自动调用toString的问题
> fun.ticsmyc.rpc.client.proxy.ServiceProxy 的 45-60 行
idea在debug时,会自动对类中属性调用toString,显示在界面上。
如果不做特殊处理,在对客户端的根据rpc服务接口生成的代理类调用toString时,也会触发rpc逻辑,导致发送了一个`java.lang.Object_t`的调用请求。 自然就请求错误了。
解决方法: 在动态代理的invoke方法中加入短路逻辑。 如果调用的是Object类的方法或者代理类特有的方法,就本地调用,不执行rpc逻辑。(MyBatis中MapperProxy的解决方案)