README.md 13.7 KB
Newer Older
G
guide 已提交
1
# guide-rpc-framework
S
shuang.kou 已提交
2

G
guide 已提交
3 4
中文|[English](./README-EN.md)

G
guide 已提交
5
本着开源精神,本项目README已经同步了英文版本。另外,项目的源代码的注释大部分也修改为了英文。
S
shuang.kou 已提交
6

G
guide 已提交
7
如访问速度不佳,可放在 Gitee 地址:https://gitee.com/SnailClimb/guide-rpc-framework 。如果要提交 issue 或者 pr 的话,请在 Github 提交:[https://github.com/Snailclimb/guide-rpc-framework](https://github.com/Snailclimb/guide-rpc-framework)
S
shuang.kou 已提交
8

G
guide 已提交
9 10
相关项目:

G
guide 已提交
11
1. [Netty 从入门到实战](https://github.com/Snailclimb/netty-practical-tutorial)(正在更新中...)
G
guide 已提交
12

S
shuang.kou 已提交
13
## 前言
14

G
guide 已提交
15
虽说 RPC 的原理实际不难,但是,自己在实现的过程中自己也遇到了很多问题。[guide-rpc-framework](https://github.com/Snailclimb/guide-rpc-framework) 目前只实现了 RPC 框架最基本的功能,一些可优化点都在下面提到了,有兴趣的小伙伴可以自行完善。
S
shuang.kou 已提交
16

G
guide 已提交
17
通过这个简易的轮子,你可以学到 RPC 的底层原理和原理以及各种 Java 编码实践的运用。
18

G
guide 已提交
19
你甚至可以把 [guide-rpc-framework](https://github.com/Snailclimb/guide-rpc-framework) 当做你的毕设/项目经验的选择,这是非常不错!对比其他求职者的项目经验都是各种系统,造轮子肯定是更加能赢得面试官的青睐。
G
guide 已提交
20

G
guide 已提交
21
如果你要将 [guide-rpc-framework](https://github.com/Snailclimb/guide-rpc-framework) 当做你的毕设/项目经验的话,我希望你一定要搞懂,而不是直接复制粘贴我的思想。你可以 fork 我的项目,然后进行优化。如果你觉得的优化是有价值的话,你可以提交 PR 给我,我会尽快处理。
G
guide 已提交
22

23 24
## 介绍

G
guide 已提交
25
 [guide-rpc-framework](https://github.com/Snailclimb/guide-rpc-framework) 是一款基于 Netty+Kyro+Zookeeper 实现的 RPC 框架。代码注释详细,结构清晰,并且集成了 Check Style 规范代码结构,非常适合阅读和学习。
S
shuang.kou 已提交
26

G
guide 已提交
27
由于 Guide哥自身精力和能力有限,如果大家觉得有需要改进和完善的地方的话,欢迎 fork 本项目,然后 clone 到本地,在本地修改后提交 PR 给我,我会在第一时间 Review 你的代码。
S
shuang.kou 已提交
28

29 30
**我们先从一个基本的 RPC 框架设计思路说起!**

S
shuang.kou 已提交
31
### 一个基本的 RPC 框架设计思路
32

G
guide 已提交
33 34 35 36 37 38 39
> **注意** :我们这里说的 RPC 框架指的是:可以让客户端直接调用服务端方法就像调用本地方法一样简单的框架,比如我前面介绍的 Dubbo、Motan、gRPC 这些。 如果需要和 HTTP 协议打交道,解析和封装 HTTP 请求和响应。这类框架并不能算是“RPC 框架”,比如 Feign。

一个最简单的 RPC 框架使用示意图如下图所示,这也是 [guide-rpc-framework](https://github.com/Snailclimb/guide-rpc-framework) 目前的架构 :

![](./images/rpc-architure.png)

服务提供端 Server 向注册中心注册服务,服务消费者 Client 通过注册中心拿到服务相关信息,然后再通过网络请求服务提供端 Server。
40

G
guide 已提交
41
作为 RPC 框架领域的佼佼者[Dubbo](https://github.com/apache/dubbo)的架构如下图所示,和我们上面画的大体也是差不多的。
42

G
guide 已提交
43
<img src="./images/dubbo-architure.jpg" style="zoom:80%;" />
44

G
guide 已提交
45 46 47 48 49 50 51
**一般情况下, RPC 框架不仅要提供服务发现功能,还要提供负载均衡、容错等功能,这样的 RPC 框架才算真正合格的。**

**简单说一下设计一个最基本的 RPC 框架的思路:**

![](./images/rpc-architure-detail.png)

1. **注册中心** :注册中心首先是要有的,推荐使用 Zookeeper。注册中心负责服务地址的注册与查找,相当于目录服务。服务端启动的时候将服务名称及其对应的地址(ip+port)注册到注册中心,服务消费端根据服务名称找到对应的服务地址。有了服务地址之后,服务消费端就可以通过网络请求服务端了。
S
shuang.kou 已提交
52
2. **网络传输** :既然要调用远程的方法就要发请求,请求中至少要包含你调用的类名、方法名以及相关参数吧!推荐基于 NIO 的 Netty 框架。
53
3. **序列化** :既然涉及到网络传输就一定涉及到序列化,你不可能直接使用 JDK 自带的序列化吧!JDK 自带的序列化效率低并且有安全漏洞。 所以,你还要考虑使用哪种序列化协议,比较常用的有 hession2、kyro、protostuff。
G
guide 已提交
54
4. **动态代理** : 另外,动态代理也是需要的。因为 RPC 的主要目的就是让我们调用远程方法像调用本地方法一样简单,使用动态代理可以屏蔽远程方法调用的细节比如网络传输。也就是说当你调用远程方法的时候,实际会通过代理对象来传输网络请求,不然的话,怎么可能直接就调用到远程方法呢?
55 56 57 58 59
5. **负载均衡** :负载均衡也是需要的。为啥?举个例子我们的系统中的某个服务的访问量特别大,我们将这个服务部署在了多台服务器上,当客户端发起请求的时候,多台服务器都可以处理这个请求。那么,如何正确选择处理该请求的服务器就很关键。假如,你就要一台服务器来处理该服务的请求,那该服务部署在多台服务器的意义就不复存在了。负载均衡就是为了避免单个服务器响应同一请求,容易造成服务器宕机、崩溃等问题,我们从负载均衡的这四个字就能明显感受到它的意义。
6. ......

### 项目基本情况和可优化点

G
guide 已提交
60
为了循序渐进,最初的是时候,我是基于传统的 **BIO** 的方式 **Socket** 进行网络传输,然后利用 **JDK 自带的序列化机制** 来实现这个 RPC 框架的。后面,我对原始版本进行了优化,已完成的优化点和可以完成的优化点我都列在了下面 👇。
61

G
guide 已提交
62
**为什么要把可优化点列出来?** 主要是想给哪些希望优化这个 RPC 框架的小伙伴一点思路。欢迎大家 fork 本仓库,然后自己进行优化。
63

S
shuang.kou 已提交
64
- [x] **使用 Netty(基于 NIO)替代 BIO 实现网络传输;**
65
- [x] **使用开源的序列化机制 Kyro(也可以用其它的)替代 JDK 自带的序列化机制;**
S
shuang.kou 已提交
66 67 68
- [x] **使用 Zookeeper 管理相关服务地址信息**
- [x] Netty 重用 Channel 避免重复连接服务端
- [x] 使用 `CompletableFuture` 包装接受客户端返回结果(之前的实现是通过 `AttributeMap` 绑定到 Channel 上实现的) 详见:[使用 CompletableFuture 优化接受服务提供端返回结果](./docs/使用CompletableFuture优化接受服务提供端返回结果.md)
S
shuang.kou 已提交
69
- [x] **增加 Netty 心跳机制** : 保证客户端和服务端的连接不被断掉,避免重连。
S
shuang.kou 已提交
70
- [x] **客户端调用远程服务的时候进行负载均衡** :调用服务的时候,从很多服务地址中根据相应的负载均衡算法选取一个服务地址。ps:目前只实现了随机负载均衡算法。
71
- [x] **处理一个接口有多个类实现的情况** :对服务分组,发布服务的时候增加一个 group 参数即可。
G
guide 已提交
72
- [x] **集成 Spring 通过注解注册服务**
G
guide 已提交
73
- [x] **增加服务版本号** :建议使用两位数字版本,如:1.0,通常在接口不兼容时版本号才需要升级。为什么要增加服务版本号?为后续不兼容升级提供可能,比如服务接口增加方法,或服务模型增加字段,可向后兼容,删除方法或删除字段,将不兼容,枚举类型新增字段也不兼容,需通过变更版本号升级。
G
guide 已提交
74
- [x] **对 SPI 机制的运用** 
S
shuang.kou 已提交
75
- [ ] **增加可配置比如序列化方式、注册中心的实现方式,避免硬编码** :通过 API 配置,后续集成 Spring 的话建议使用配置文件的方式进行配置
76
- [ ] **使用注解进行服务消费**
S
shuang.kou 已提交
77
- [ ] **客户端与服务端通信协议(数据包结构)重新设计** ,可以将原有的 `RpcRequest``RpcReuqest` 对象作为消息体,然后增加如下字段(可以参考:《Netty 入门实战小册》和 Dubbo 框架对这块的设计):
78 79 80 81
  - **魔数** : 通常是 4 个字节。这个魔数主要是为了筛选来到服务端的数据包,有了这个魔数之后,服务端首先取出前面四个字节进行比对,能够在第一时间识别出这个数据包并非是遵循自定义协议的,也就是无效数据包,为了安全考虑可以直接关闭连接以节省资源。
  - **序列化器编号** :标识序列化的方式,比如是使用 Java 自带的序列化,还是 json,kyro 等序列化方式。
  - **消息体长度** : 运行时计算出来。
  - ......
S
shuang.kou 已提交
82
- [ ] **编写测试为重构代码提供信心**
83 84

### 项目模块概览
85 86 87

![](./images/RPC框架各个模块介绍.png)

88 89
## 运行项目

G
guide 已提交
90
### 导入项目
91

G
guide 已提交
92
fork 项目到自己的仓库,然后克隆项目到自己的本地:`git clone git@github.com:username/guide-rpc-framework.git`,使用 IDEA 打开,等待项目初始化完成。
93

G
guide 已提交
94
### 初始化 git hooks
95

S
shuang.kou 已提交
96 97
**这一步主要是为了在 commit 代码之前,跑 Check Style,保证代码格式没问题,如果有问题的话就不能提交。**

S
shuang.kou 已提交
98
> 以下演示的是 Mac/Linux 对应的操作,Window 用户需要手动将 `config/git-hooks` 目录下的`pre-commit` 文件拷贝到 项目下的 `.git/hooks/` 目录。
99

S
shuang.kou 已提交
100
执行下面这些命令:
101 102 103

```shell
➜  guide-rpc-framework git:(master)chmod +x ./init.sh
S
shuang.kou 已提交
104
➜  guide-rpc-framework git:(master) ✗ ./init.sh
105 106
```

S
shuang.kou 已提交
107
`init.sh` 这个脚本的主要作用是将 git commit 钩子拷贝到项目下的 `.git/hooks/` 目录,这样你每次 commit 的时候就会执行了。
108

G
guide 已提交
109
### CheckStyle 插件下载和配置
110

S
shuang.kou 已提交
111
IntelliJ IDEA-> Preferences->Plugins->搜索下载 CheckStyle 插件,然后按照如下方式进行配置。
112

S
shuang.kou 已提交
113
![CheckStyle 插件下载和配置](./images/setting-check-style.png)
114

115 116
配置完成之后,按照如下方式使用这个插件!

S
shuang.kou 已提交
117
![插件使用方式](./images/run-check-style.png)
118

G
guide 已提交
119
### 下载运行 zookeeper
120 121 122

这里使用 Docker 来下载安装。

S
shuang.kou 已提交
123 124 125
下载:

```shell
126
docker pull zookeeper:3.5.8
S
shuang.kou 已提交
127 128 129 130 131
```

运行:

```shell
132
docker run -d --name zookeeper -p 2181:2181 zookeeper:3.5.8
S
shuang.kou 已提交
133 134 135 136 137 138 139 140 141
```

## 使用

### 服务提供端

实现接口:

```java
G
guide 已提交
142 143
@Slf4j
@RpcService(group = "test1", version = "version1")
S
shuang.kou 已提交
144
public class HelloServiceImpl implements HelloService {
G
guide 已提交
145 146 147 148 149
    static {
        System.out.println("HelloServiceImpl被创建");
    }

    @Override
S
shuang.kou 已提交
150
    public String hello(Hello hello) {
G
guide 已提交
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170
        log.info("HelloServiceImpl收到: {}.", hello.getMessage());
        String result = "Hello description is " + hello.getDescription();
        log.info("HelloServiceImpl返回: {}.", result);
        return result;
    }
}
	
@Slf4j
public class HelloServiceImpl2 implements HelloService {

    static {
        System.out.println("HelloServiceImpl2被创建");
    }

    @Override
    public String hello(Hello hello) {
        log.info("HelloServiceImpl2收到: {}.", hello.getMessage());
        String result = "Hello description is " + hello.getDescription();
        log.info("HelloServiceImpl2返回: {}.", result);
        return result;
S
shuang.kou 已提交
171 172 173 174
    }
}
```

S
shuang.kou 已提交
175
发布服务(使用 Netty 进行传输):
S
shuang.kou 已提交
176 177

```java
G
guide 已提交
178 179 180 181 182 183
/**
 * Server: Automatic registration service via @RpcService annotation
 *
 * @author shuang.kou
 * @createTime 2020年05月10日 07:25:00
 */
G
guide 已提交
184
@RpcScan(basePackage = {"github.javaguide.serviceimpl"})
G
guide 已提交
185 186
public class NettyServerMain {
    public static void main(String[] args) {
G
guide 已提交
187 188 189 190
        // Register service via annotation
        new AnnotationConfigApplicationContext(NettyServerMain.class);
        NettyServer nettyServer = new NettyServer();
        // Register service manually
G
guide 已提交
191 192
        HelloService helloService2 = new HelloServiceImpl2();
        RpcServiceProperties rpcServiceProperties = RpcServiceProperties.builder()
G
guide 已提交
193 194 195
                .group("test2").version("version2").build();
        nettyServer.registerService(helloService2, rpcServiceProperties);
        nettyServer.start();
G
guide 已提交
196 197
    }
}
S
shuang.kou 已提交
198 199 200 201 202 203
```

### 服务消费端

```java
ClientTransport rpcClient = new NettyClientTransport();
G
guide 已提交
204 205 206
RpcServiceProperties rpcServiceProperties = RpcServiceProperties.builder()
  .group("test1").version("version1").build();
RpcClientProxy rpcClientProxy = new RpcClientProxy(rpcClient, rpcServiceProperties);
S
shuang.kou 已提交
207 208 209 210
HelloService helloService = rpcClientProxy.getProxy(HelloService.class);
String hello = helloService.hello(new Hello("111", "222"));
```

211 212
## 相关问题

S
shuang.kou 已提交
213
### 为什么要造这个轮子?Dubbo 不香么?
214 215 216

写这个 RPC 框架主要是为了通过造轮子的方式来学习,检验自己对于自己所掌握的知识的运用。

S
shuang.kou 已提交
217
实现一个简单的 RPC 框架实际是比较容易的,不过,相比于手写 AOP 和 IoC 还是要难一点点,前提是你搞懂了 RPC 的基本原理。
218

S
shuang.kou 已提交
219
我之前从理论层面在我的知识星球分享过如何实现一个 RPC。不过理论层面的东西只是支撑,你看懂了理论可能只能糊弄住面试官。咱程序员这一行还是最需要动手能力,即使你是架构师级别的人物。当你动手去实践某个东西,将理论付诸实践的时候,你就会发现有很多坑等着你。
220 221 222

大家在实际项目上还是要尽量少造轮子,有优秀的框架之后尽量就去用,Dubbo 在各个方面做的都比较好和完善。

S
shuang.kou 已提交
223 224 225 226 227
### 如果我要自己写的话,需要提前了解哪些知识

**Java**

1. 动态代理机制;
S
shuang.kou 已提交
228
2. 序列化机制以及各种序列化框架的对比,比如 hession2、kyro、protostuff。
S
shuang.kou 已提交
229 230 231 232 233 234 235 236 237 238 239 240 241 242 243
3. 线程池的使用;
4. `CompletableFuture` 的使用
5. ......

**Netty**

1. 使用 Netty 进行网络传输;
2. `ByteBuf` 介绍
3. Netty 粘包拆包
4. Netty 长连接和心跳机制

**Zookeeper** :

1. 基本概念;
2. 数据结构;
S
shuang.kou 已提交
244 245
3. 如何使用 Netflix 公司开源的 zookeeper 客户端框架 Curator 进行增删改查;

G
guide 已提交
246 247 248
## 教程

Guide 的星球正在更新《从零开始手把手教你实现一个简单的 RPC 框架》。扫描下方二维码关注“**JavaGuide**”后回复 “**星球**”即可。
S
shuang.kou 已提交
249

G
guide 已提交
250
![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png)
S
shuang.kou 已提交
251

S
shuang.kou 已提交
252