# lonni-rpc 基于SpringBoot+Netty手写rpc框架 **Repository Path**: hiLzd/lonni-rpc ## Basic Information - **Project Name**: lonni-rpc 基于SpringBoot+Netty手写rpc框架 - **Description**: 基于SpringBoot手写rpc框架 - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: https://doc-lonni.netlify.app/ - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2024-06-27 - **Last Updated**: 2024-07-30 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # lonni-rpc 基于SpringBoot手写rpc框架 # 介绍 `lonni-rpc` 框架是基于`SpringBoot`整合 `Netty` `zookeeper` 实现的简易`RPC` 框架,实现简易的RPC请求调用;涉及到的知识点有 **Netty** **自定义协议** **序列化** **负载均衡** **服务注册和发现(zookeeper)** **java SPI** **SpringBoot以及扩展点接口** ; **为什么会有此项目?** 在学习了`Netty` 相关知识后,需要整体的项目来巩固,在此之前写了一个基于`Netty`的IM项目,实现了业务端和服务端消息收发和集群功能,由于即时通讯业务庞大以及个人原因,该项目暂停开发; 但是 `RPC`框架不像 im那样业务庞大且涉及多端,单从知识点来说 rpc框架涉及了很多知识点,通过**造轮子的方式,来学习 整合 检验自己所学知识!** **实际项目中一般使用 Dubbo**作为rpc框架! ## 一、RPC简介 RPC,全称为Remote Procedure Call,即远程过程调用,它是一个计算机通信协议。它允许像调用本地服务一样调用远程服务。它可以有不同的实现方式。如RMI( 远程方法调用)、Hessian、Http invoker等。另外,RPC是与语言无关的。 现在互联网应用的量级越来越大,单台计算机的能力有限,需要借助可扩展的计算机集群来完成,分布式的应用可以借助RPC来完成机器之间的调用。 ## 二、 RPC框架原理 在RPC框架中主要有三个角色:Provider、Consumer和Registry。如下图所示: ![](https://images2018.cnblogs.com/blog/137084/201805/137084-20180506151205222-740014430.png) 节点角色说明: * Server: 暴露服务的服务提供方。 * Client: 调用远程服务的服务消费方。 * Registry: 服务注册与发现的注册中心。 图中服务端启动时将自己的服务节点信息注册到注册中心,客户端调用远程方法时会订阅注册中心中的可用服务节点信息,拿到可用服务节点之后远程调用方法,当注册中心中的可用服务节点发生变化时会通知客户端,避免客户端继续调用已经失效的节点。那客户端是如何调用远程方法的呢,来看一下远程调用示意图 ![](https://segmentfault.com/img/bVcY1tt) 客户端模块代理所有远程方法的调用 将目标服务、目标方法、调用目标方法的参数等必要信息序列化 序列化之后的数据包进一步压缩,压缩后的数据包通过网络通信传输到目标服务节点 服务节点将接受到的数据包进行解压 解压后的数据包反序列化成目标服务、目标方法、目标方法的调用参数 通过服务端代理调用目标方法获取结果,结果同样需要序列化、压缩然后回传给客户端 通过以上描述,相信读者应该大体上了解了 RPC 是如何工作的,接下来看如何使用代码具体实现上述的流程。鉴于篇幅笔者会选择重要或者网络上介绍相对较少的模块来讲述。 ## rpc基础组件 ### 服务注册和发现 由于是分布式系统,需要将服务提供者和消费者注册到远程注册中心,同时在本地缓存一份服务列表; 同时需要监听服务上下线更新本地缓存; 注册中心可选用 `Zookeper Nacos Redis` **ZooKeeper** ZooKeeper 为我们提供了高可用、高性能、稳定的分布式数据一致性解决方案,通常被用于实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。并且,ZooKeeper 将数据保存在内存中,性能是非常棒的。 在“读”多于“写”的应用程序中尤其地高性能,因为“写”会导致所有的服务器间同步状态。(“读”多于“写”是协调服务的典型场景)。 > 注意:当前使用的版本必须大于等于:3.6.0,目前使用3.7.0版本也可以的;否则会导致节点无法注册上去 ### 序列化和反序列化 要在网络传输数据就要涉及到序列化。为什么需要序列化和反序列化呢? - 序列化: 将数据结构或对象转换成二进制字节流的过程 - 反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程 因为网络传输的数据必须是二进制的。因此,我们的 Java 对象没办法直接在网络中传输。为了能够让 Java 对象在网络中传输我们需要将其序列化为二进制的数据。我们最终需要的还是目标 Java 对象,因此我们还要将二进制的数据“解析”为目标 Java 对象,也就是对二进制数据再进行一次反序列化。 **序列化工具** - kryo 序列化 Kryo 是一个高性能的序列化/反序列化工具,是 **专门针对 Java** 语言序列化方式并且性能非常好,由于其变长存储特性并使用了字节码生成机制,拥有较高的运行速度和较小的字节码体积。 另外,Kryo 已经是一种非常成熟的序列化实现了,已经在 Twitter、Groupon、Yahoo 以及多个著名开源项目(如 Hive、Storm)中广泛的使用。 - hessian Hessian 是一个轻量级的,自定义描述的二进制 RPC 协议。Hessian 是一个比较老的序列化实现了,并且同样也是跨语言的。 - fastjson json序列化,体积大 - Protobuf Protobuf 出自于 Google,性能还比较优秀,体积小;但是需要额外定义 IDL 文件和生成对应的序列化代码,不灵活; - jdk 序列化 需要继承 `java.io.Serializable`接口; 缺点: 不支持跨语言调用; 性能差,序列化之后体积很大; ### 网络传输 使用Netty实现 rpc框架中网络传输;需要实现如下功能 - 自定义传输协议 - 同步返回结果 - 异步返回结果 - 回调方式返回结果 - 实现序列化和反序列 ### 动态代理 使用 jdk动态代理实现远程调用; # 前置知识 ## JDK静态代理和动态代理 [动态代理参考](https://segmentfault.com/a/1190000039303463) 代理类实例在程序运行时,由JVM根据反射机制动态的生成。也就是说代理类不是用户自己定义的,而是由JVM生成的。 由于其原理是通过Java反射机制实现的,所以在学习前,要对反射机制有一定的了解 ## springboot扫描和初始化簇 ### springboot初始化接口 - InitializingBean InitializingBean是Spring提供的拓展性接口,InitializingBean接口为bean提供了属性初始化后的处理方法,它只有一个afterPropertiesSet方法,凡是继承该接口的类,在bean的属性初始化后都会执行该方法。 ```java @Component public class MyInitializingBean implements InitializingBean { @Override public void afterPropertiesSet() throws Exception { System.out.println("我是启动时加载..."); } } ``` - ApplicationContextAware 调用时机在bean初始化前早于BeanFactoryPostProcessor 接口 - BeanFactoryPostProcessor BeanFactory的后置处理器(beanFactory的扩展接口) 在BeanFactory组建完之后(注:组建完并不是指所有bean装载完) 可以对beanFactory里面的东西 比如beanDefinition相关属性 进行操作 (beanDefinition就位于beanFactory中) 调用时机在spring在读取beanDefinition信息之后,实例化bean之前。 - InstantiationAwareBeanPostProcessor 该接口 继承了 BeanPostProcess 接口,区别如下: BeanPostProcess接口只在bean的初始化阶段进行扩展(注入spring上下文前后),而InstantiationAwareBeanPostProcessor接口在此基础上增加了3个方法,把可扩展的范围增加了实例化阶段和属性注入阶段 执行时间略早于BeanPostProcess。 - BeanPostProcessor BeanPostProcessor提供了两个方法 `postProcessBeforeInitialization` , `postProcessAfterInitialization` postProcessBeforeInitialization: 在Bean的初始化之前调用 postProcessAfterInitialization: 在Bean的初始化之后调用 (bean后置处理器) ## ZooKeeper ## redis ## nacos ## Netty ### 字节说明 - int 4字节(32位) -231~ 231-1 0 Integer - short 2字节(16位) -215~215-1 0 Short - long 8字节(64位) -263~263-1 0 Long - byte 1字节(8位) -27~27-1 0 Byte - 浮点 float 4字节(32位) -3.4e+38 ~ 3.4e+38 0.0f Float - 类型 double 8字节(64位) -1.7e+308 ~ 1.7e+308 0 Double - 字符 char 2字节(16位) u0000~uFFFF(‘’~‘?’) ‘0’ Character (0~216-1(65535)) - 布尔 boolean 1/8字节(1位) true, false FALSE Boolean ## SPI 动态服务发现 SPI (Service Provider Interface) 是一种服务发现机制,它允许第三方提供者为核心库或主框架提供实现或扩展。这种设计允许核心库/框架在不修改自身代码的情况下,通过第三方实现来增强功能。 SPI机制是JDK提供接口,第三方Jar包实现,接口由启动类加载器加载,实现类不在JDK中,需要反向委派,由线程上下文加载器加载。它约定:在 jar 包的 META-INF/services 包下,以接口全限定名为文件名,文件内容是实现类名称 **使用** - 建立一个接口 ,完成接口名 `com.lonni.service.IUserService` - 在 `src/main/resources/`目录下新建 `META-INF/services/` 目录,在此目录下新建 `com.lonni.service.IUserService`文件,* *注意:没有任何文件扩展名**; - 在文件中写入实现了 `IUserService`接口的类的完全限定名,**多个换行**; - 使用 ```java ServiceLoader loaders=ServiceLoader.load(MessageService.class); for(MessageService service:loaders){ System.out.println(service.getMessage()); } ``` **扩展** spi默认加载路径为 `META-INF/services/` ,为了不与其他系统冲突,本框架需自定义目录加载类; 查看 `ServiceLoader`源码,发现最终调用了 `LazyIterator`类, 查看`nextService` 类方法和`hasNextService`如下: ```java private boolean hasNextService(){ if(nextName!=null){ return true; } if(configs==null){ try{ String fullName=PREFIX+service.getName(); if(loader==null) configs=ClassLoader.getSystemResources(fullName); else configs=loader.getResources(fullName); }catch(IOException x){ fail(service,"Error locating configuration files",x); } } while((pending==null)||!pending.hasNext()){ if(!configs.hasMoreElements()){ return false; } pending=parse(service,configs.nextElement()); } nextName=pending.next(); return true; } private S nextService(){ if(!hasNextService()) throw new NoSuchElementException(); String cn=nextName; nextName=null; Class c=null; try{ c=Class.forName(cn,false,loader); }catch(ClassNotFoundException x){ fail(service, "Provider "+cn+" not found"); } if(!service.isAssignableFrom(c)){ fail(service, "Provider "+cn+" not a subtype"); } try{ S p=service.cast(c.newInstance()); providers.put(cn,p); return p; }catch(Throwable x){ fail(service, "Provider "+cn+" could not be instantiated", x); } throw new Error(); // This cannot happen } ``` 原理是通过读取读取指定路径的文件内容,将内容解析出来,然后使用 `Class.forName` 将实现类加载到jvm中; **由于本项目是结合spring使用,所以后期会使用spring相关扩展点来实现个性化需求** # 扫描指定注解注册到spring容器中 ## 第一种 spring默认会扫描 `@Component`注解标识的类,可以在新建注解上添加 `@Component`注解,这样就不用重写扫描了 ## 第二种 自定义扫描器 ClassPathBeanDefinitionScanner 用过Mybatis的小伙伴应该知道我们扫描包是在启动类上面加一个@MapperScan(“com.xxx.xxx.dao”)这种方式。 其实`Mybatis` 的Mapper注册器`ClassPathMapperScanner` 是同过继承 `ClassPathBeanDefinitionScanner` ,并且自定义了过滤器规则来实现的。那我只要仿造Mybatis的方式自己写一个就行; **ClassPathBeanDefinitionScanner** 可以扫描指定包路径下的被 `@Component`标识的类,将这些类解析为 `BeanDefinition`注册到spring容器中; 此外,`ClassPathBeanDefinitionScanner` 通过注册相关注解后处理器,提供了对 `@Order、@Priority、@Autowired、@Configuration` 和 `@EventListener` 等注解的支持。 **成员变量** - registry:BeanDefinition注册器,实际上就是Spring容器。 - beanDefinitionDefaults:BeanDefinition默认属性的封装工具。 - autowireCandidatePatterns: - beanNameGenerator:beanName生成器,会先获取@Component、@ManagedBean、@Named或@Component子注解的value属性,没有再按类名生成。 - scopeMetadataResolver:作用域解析器,会先获取@Scope注解的value和proxyMode属性,没有则使用默认值(singleton和ScopedProxyMode.NO)。 - includeAnnotationConfig:是否往Spring容器注册默认的XxxProcessor,默认为true。 ## 分支说明 - rpc_01 初始化项目和md文档 - rpc_02 dto传输和负载均衡算法 - rpc_03 增加spring注解和路径扫描和测试 - rpc_04 Netty客户端和服务端 动态代理 - rpc_05 服务注册和发现 - rpc_06 spring集成