1 Star 4 Fork 4

Ticsmyc / T_RPC_Framework

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
README.md 14.78 KB
一键复制 编辑 原始数据 按行查看 历史
Ticsmyc 提交于 2021-04-16 11:18 . update readme

T_RPC_Framework

一个rpc远程过程调用的框架。

使用方法

见项目 : T_RPC_Framework_Demo

步骤(基于3.8版本):

  1. 启动nacos

  2. 引入依赖

    <dependency>
        <groupId>fun.ticsmyc.rpc</groupId>
        <artifactId>t-rpc-all</artifactId>
        <version>3.8</version>
    </dependency>
  3. 在resources目录下,放入trpc.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 #注册中心地址
  4. 在配置类声明

    @EnableTRPC
  5. 提供服务:

    在想要提供的服务接口上声明

    @TRPCInterface //这一步是为了解决某个实现类实现了多个接口的情况 

    在想要提供的服务实现类上声明

    @TRPCService(group="t")   // group属性是为了处理一个接口想要注册多个实现类的情况消费服务
  6. 消费服务:

    通过@RpcClient注解即可注入rpc服务

    @RpcClient(group = "t")
    private HelloService helloService;

原理图

image-20201109152554001

目录说明

└─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

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

一个接口对应多个实现类的处理

从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表现为: 【只与一个服务器建立了连接, 每次却有若干个心跳包发送】

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接口的注释中有提到

       * <p>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的解决方案)

Java
1
https://gitee.com/TicsmycL/t-rpc-framework.git
git@gitee.com:TicsmycL/t-rpc-framework.git
TicsmycL
t-rpc-framework
T_RPC_Framework
master

搜索帮助