# 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; ``` ### 原理图 ![image-20201109152554001](pic/image-20201109152554001.png) ## 目录说明 ``` └─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的解决方案)