# miniRPC
**Repository Path**: xxjay/miniRPC
## Basic Information
- **Project Name**: miniRPC
- **Description**: 基于Netty实现的RPC框架,支持Zookeeper、Redis两种注册中心
- **Primary Language**: Java
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 6
- **Forks**: 0
- **Created**: 2022-02-13
- **Last Updated**: 2023-02-15
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# Mini-RPC
Mini-RPC 是 基于Netty开发的 使用TCP通信的 RPC框架。提供了多种注册中心、多种调用方法、客户端负载均衡、服务端过滤器等功能。
- [x] 同步、异步、Future三种调用方法
- [x] SPI机制,提供插件化扩展功能
- [x] Zookeeper、Nacos、Redis三种注册中心支持
- [x] 注解化配置方式
- [x] 多种客户端负载均衡算法
- [x] 服务端过滤器,可注解化配置的过滤器链,提供exlusions和优先级配置
## 使用说明
### 概念说明
RPC系统中共有三种角色,注册中心、服务提供者(**Provider**)、服务消费者(**Consumer**)。
- **Provider**: 服务提供者,提供具体一种服务的服务器。
- **Service**:服务,在Mini-RPC中服务以 Java 类为载体,一个Java类就是一个服务。
- **Version**:服务版本,服务可以有不同的版本,因此同一个服务可以对应多个Java类。
- **Consumer**:服务消费者,通过RPC客户端调用远程服务。
- **Registry**:注册中心,消费者通过注册中心了解服务提供者的地址。Mini-RPC支持Zookeeper、Redis 和 Nacos作为注册中心。
### maven依赖
Mini-RPC提供SpringBoot支持,添加Mini-RPC-SpringBoot starter依赖即可使用。
```xml-dtd
com.jay
mini-rpc-spring-boot-starter
1.0-SNAPSHOT
```
### 配置文件
在项目**Resources**目录下创建**mini-rpc.properties**文件
#### 通用配置
```properties
# 注册中心类型
mini-rpc.registry.type = redis/zookeeper/nacos
```
#### Provider配置
```properties
# provider 服务器端口
mini-rpc.server.port = 8888
```
#### Consumer配置
```properties
# 客户端负载均衡算法
mini-rpc.client.load-balance = random
# 客户端最大连接数,客户端会按照这个数量去建立连接
mini-rpc.client.max-conn = 10
```
### 创建服务
使用@RpcService注解来声明一个服务类,Mini-RPC会在服务类扫描过程中通过注解识别。
```java
public interface HelloService{
String sayHello(String name);
}
// 在注解的name属性输入服务名
@RpcService(type = HelloService.class, version=1)
public class HelloServiceImplV1 implements HelloService{
@Overrides
public String sayHello(String name){
return "hello v1 " + name;
}
}
// 通过version属性来完成版本控制
@RpcService(type=HelloService.class, version=2)
public class HelloServiceImplV2 implements HelloService{
@Overrides
public String sayHello(String name){
return "hello v2 " + name;
}
}
```
### 远程调用
MiniRPC支持同步、异步、Future三种调用方式。
#### 同步调用
使用MiniRpcProxy的createInstance方法创建RPC代理对象。该方法参数列表如下:
- 服务接口类
- 服务名称
- 版本号
```java
@Test
public void test(){
// 调用 由 组hello-group中的服务器 提供的hello-service服务
HelloService serviceV1 = (HelloService)MiniRpcProxy.createInstance(HelloService.class, "hello-service", 1);
// 调用不同版本的服务
HelloService serviceV2 = (HelloService)MiniRpcProxy.createInstance(HelloService.class, "hello-service", 2);
log.info("v1: {}", serviceV1.sayHello("world"));
log.info("v2: {}", serviceV2.sayHello("world"));
}
```
#### @RpcAutowired注解同步调用
使用RpcAutowired注解可以借助Spring容器来加载一个RPC代理对象,具体的用法如下:
```java
@RestController
public class TestController {
// 在注解中指定调用服务的版本,也可以指定Provider地址
@RpcAutowired(version = 1, provider="127.0.0.1:9999")
private HelloService helloService;
@GetMapping("/test/v1/{name}")
public String testHelloV1(@PathVariable("name") String name){
return helloService.hello(name);
}
}
```
#### 异步调用
使用MiniRpcProxy的callAsync方法异步调用,该方法需要指定目标接口、版本号、方法、Callback和参数列表
```java
public static void callAsync(Class> targetClass, int version, Method method, AsyncCallback callback, Object[] args)
```
Callback需要实现AsyncCallback接口:
```java
public interface AsyncCallback {
/**
* 收到response
* @param response {@link RpcResponse}
*/
void onResponse(RpcResponse response);
/**
* 捕获到异常
* @param throwable {@link Throwable}
*/
void exceptionCaught(Throwable throwable);
}
```
#### Future调用
使用MiniRpcProxy的callFuture方法进行Future调用,该方法返回一个CompletableFuture对象。
```java
public static CompletableFuture callFuture(Class> targetClass, int version, Method method, Object[] args)
```
### 过滤器
通过配置过滤器可以实现对请求的筛选过滤,过滤器可以配置exlusions来排除请求,也可以配置优先级来调节过滤器在执行链中的位置。
```java
// 通过注解配置排除的请求(请求类名/版本号/方法名)和优先级(值大优先)
@RpcFilter(exclusions = "com.jay.service.HelloService/1/sayHello", priority = 500)
public class MyFilter extends AbstractFilter {
@Override
public boolean doFilter(RpcRequest rpcRequest) {
// 检查参数是否是null
return Arrays.stream(rpcRequest.getParameters()).allMatch(Objects::nonNull);
}
}
```
## Zookeeper注册中心
添加Zookeeper依赖,配置文件中修改配置
```xml-dtd
com.jay
mini-rpc-nacos-registry
1.0-SNAPSHOT
```
```properties
# 注册中心类型
mini-rpc.registry.type = zookeeper
# zookeeper
mini-rpc.registry.zookeeper.host = 127.0.0.1
mini-rpc.registry.zookeeper.port = 6379
```
服务注册后的Zookeeper节点如下表所示:
| Path | 作用 |
| ---------------------------------------------- | ---------------------------------------- |
| /mini-rpc/services/{{ServiceName}}/{{version}} | 服务根目录 |
| 服务根目录/{{address}} | 服务Provider节点,data为Provider信息JSON |
Mini-RPC使用**CuratorFramework**的**TreeCacheListener**来监听Zookeeper注册中心节点的改变,以此来更新Consumer本地缓存。
## Redis注册中心
添加Redis注册中心依赖,并在配置文件中配置Redis
```xml-dtd
com.jay
mini-rpc-redis-registry
1.0-SNAPSHOT
```
```properties
# 注册中心类型
mini-rpc.registry.type = redis
# redis
mini-rpc.registry.redis.host = 127.0.0.1
mini-rpc.registry.redis.port = 6379
```
服务注册后,Redis中的Key Value如下表所示,
| Key | Value |
| --------------------------------------------- | ------------------------------------ |
| mini-rpc/services/{{serviceName}}/{{version}} | Hash,key是Provider地址,Value是JSON |
## Nacos注册中心
添加Nacos注册中心依赖,并在配置文件中配置相关内容:
```xml-dtd
com.jay
mini-rpc-nacos-registry
1.0-SNAPSHOT
```
```properties
# 注册中心类型
mini-rpc.registry.type = nacos
# Nacos
mini-rpc.registry.nacos.address = 127.0.0.1:8848
mini-rpc.registry.nacos.user = nacos
mini-rpc.registry.nacos.password = nacos
```
## SPI
Mini-RPC仿照Dubbo实现了SPI机制来加载扩展类。按照以下步骤即可实现SPI:
1. 编写SPI接口,并添加@SPI注解
2. 在META-INF/extensions目录下添加名称为扩展接口的文件
3. 编写SPI扩展类,并在扩展文件中添加名称与类名映射
4. 使用ExtensionLoader加载扩展类
```properties
extension1 = com.jay.test.extension.MyExtension1
```
```java
@SPI
public interface MyExtension {
void hello();
}
```
```java
public class MyExtension1 implements MyExtension {
@Override
public void hello(){
System.out.println("hello");
}
}
```
```java
public class MyTest {
@Test
public void testExtension(){
// 获取ExtensionLoader
ExtensionLoader extensionLoader = ExtensionLoader.getExtensionLoader(MyExtension.class);
// 获取Extension
MyExtension ext1 = extensionLoader.getExtension("ext1");
ext1.hello();
}
}
```
## 性能测试
### 测试1
单线程发送10万次请求
```java
@Test
public void singleThread(){
HelloService instance = (HelloService) MiniRpcProxy.createInstance(HelloService.class, 1);
long testStart = System.currentTimeMillis();
int loop = 100000;
for(int i = 0; i < loop; i++){
String hello = instance.hello("name");
Assert.assertEquals("hello v1 name", hello);
}
long timeUsed = System.currentTimeMillis() - testStart;
log.info("测试结束,用时:{}ms,QPS:{}", timeUsed, (loop * 1000) / timeUsed);
}
```
测试结果如下,QPS为8000左右
```
2022-04-22 11:56:17,143 [main] INFO - 测试结束,用时:12125ms,QPS:8247
```
### 测试2
1000个线程并发,每个线程发送100次请求
```java
@Test
public void concurrent() throws InterruptedException {
HelloService instance = (HelloService) MiniRpcProxy.createInstance(HelloService.class, 1);
int threadCount = 1000;
int loop = 100;
int total = threadCount * loop;
CountDownLatch countDownLatch = new CountDownLatch(threadCount);
Runnable task = ()->{
for(int j = 0; j < loop; j++){
String hello = instance.hello("name");
Assert.assertEquals("hello v1 name", hello);
}
countDownLatch.countDown();
};
long testStart = System.currentTimeMillis();
for(int i = 0; i < threadCount; i++){
new Thread(task).start();
}
countDownLatch.await();
long timeUsed = System.currentTimeMillis() - testStart;
log.info("测试结束,用时:{}ms,QPS:{}", timeUsed, (total * 1000L) / timeUsed);
}
```
测试结果如下,并发环境下的QPS大约为3万
```
2022-04-22 12:01:54,802 [main] INFO - 测试结束,用时:2772ms,QPS:36075
```