guide-rpc-framework基于 Netty+Kyro+Zookeeper 實(shí)現(xiàn)的自定義 RPC 框架
guide-rpc-framework
本著開(kāi)源精神,本項(xiàng)目README已經(jīng)同步了英文版本。另外,項(xiàng)目的源代碼的注釋大部分也修改為了英文。
如訪問(wèn)速度不佳,可放在 Gitee 地址:https://gitee.com/SnailClimb/guide-rpc-framework 。如果要提交 issue 或者 pr 的話,請(qǐng)?jiān)?Github 提交:[https://github.com/Snailclimb/guide-rpc-framework]
前言
雖說(shuō) RPC 的原理實(shí)際不難,但是,自己在實(shí)現(xiàn)的過(guò)程中自己也遇到了很多問(wèn)題。[guide-rpc-framework] 目前只實(shí)現(xiàn)了 RPC 框架最基本的功能,一些可優(yōu)化點(diǎn)都在下面提到了,有興趣的小伙伴可以自行完善。
通過(guò)這個(gè)簡(jiǎn)易的輪子,你可以學(xué)到 RPC 的底層原理和原理以及各種 Java 編碼實(shí)踐的運(yùn)用。
你甚至可以把 [guide-rpc-framework] 當(dāng)做你的畢設(shè)/項(xiàng)目經(jīng)驗(yàn)的選擇,這是非常不錯(cuò)!對(duì)比其他求職者的項(xiàng)目經(jīng)驗(yàn)都是各種系統(tǒng),造輪子肯定是更加能贏得面試官的青睞。
如果你要將 [guide-rpc-framework] 當(dāng)做你的畢設(shè)/項(xiàng)目經(jīng)驗(yàn)的話,我希望你一定要搞懂,而不是直接復(fù)制粘貼我的思想。你可以 fork 我的項(xiàng)目,然后進(jìn)行優(yōu)化。如果你覺(jué)得的優(yōu)化是有價(jià)值的話,你可以提交 PR 給我,我會(huì)盡快處理。
介紹
[guide-rpc-framework] 是一款基于 Netty+Kyro+Zookeeper 實(shí)現(xiàn)的 RPC 框架。代碼注釋詳細(xì),結(jié)構(gòu)清晰,并且集成了 Check Style 規(guī)范代碼結(jié)構(gòu),非常適合閱讀和學(xué)習(xí)。
由于 Guide哥自身精力和能力有限,如果大家覺(jué)得有需要改進(jìn)和完善的地方的話,歡迎 fork 本項(xiàng)目,然后 clone 到本地,在本地修改后提交 PR 給我,我會(huì)在第一時(shí)間 Review 你的代碼。
**我們先從一個(gè)基本的 RPC 框架設(shè)計(jì)思路說(shuō)起!**
一個(gè)基本的 RPC 框架設(shè)計(jì)思路
> **注意** :我們這里說(shuō)的 RPC 框架指的是:可以讓客戶端直接調(diào)用服務(wù)端方法就像調(diào)用本地方法一樣簡(jiǎn)單的框架,比如我前面介紹的 Dubbo、Motan、gRPC 這些。 如果需要和 HTTP 協(xié)議打交道,解析和封裝 HTTP 請(qǐng)求和響應(yīng)。這類(lèi)框架并不能算是“RPC 框架”,比如 Feign。
一個(gè)最簡(jiǎn)單的 RPC 框架使用示意圖如下圖所示,這也是 [guide-rpc-framework](https://github.com/Snailclimb/guide-rpc-framework) 目前的架構(gòu) :
服務(wù)提供端 Server 向注冊(cè)中心注冊(cè)服務(wù),服務(wù)消費(fèi)者 Client 通過(guò)注冊(cè)中心拿到服務(wù)相關(guān)信息,然后再通過(guò)網(wǎng)絡(luò)請(qǐng)求服務(wù)提供端 Server。
作為 RPC 框架領(lǐng)域的佼佼者[Dubbo](https://github.com/apache/dubbo)的架構(gòu)如下圖所示,和我們上面畫(huà)的大體也是差不多的。
**一般情況下, RPC 框架不僅要提供服務(wù)發(fā)現(xiàn)功能,還要提供負(fù)載均衡、容錯(cuò)等功能,這樣的 RPC 框架才算真正合格的。**
**簡(jiǎn)單說(shuō)一下設(shè)計(jì)一個(gè)最基本的 RPC 框架的思路:**
1. **注冊(cè)中心** :注冊(cè)中心首先是要有的,推薦使用 Zookeeper。注冊(cè)中心負(fù)責(zé)服務(wù)地址的注冊(cè)與查找,相當(dāng)于目錄服務(wù)。服務(wù)端啟動(dòng)的時(shí)候?qū)⒎?wù)名稱及其對(duì)應(yīng)的地址(ip+port)注冊(cè)到注冊(cè)中心,服務(wù)消費(fèi)端根據(jù)服務(wù)名稱找到對(duì)應(yīng)的服務(wù)地址。有了服務(wù)地址之后,服務(wù)消費(fèi)端就可以通過(guò)網(wǎng)絡(luò)請(qǐng)求服務(wù)端了。
2. **網(wǎng)絡(luò)傳輸** :既然要調(diào)用遠(yuǎn)程的方法就要發(fā)請(qǐng)求,請(qǐng)求中至少要包含你調(diào)用的類(lèi)名、方法名以及相關(guān)參數(shù)吧!推薦基于 NIO 的 Netty 框架。
3. **序列化** :既然涉及到網(wǎng)絡(luò)傳輸就一定涉及到序列化,你不可能直接使用 JDK 自帶的序列化吧!JDK 自帶的序列化效率低并且有安全漏洞。 所以,你還要考慮使用哪種序列化協(xié)議,比較常用的有 hession2、kyro、protostuff。
4. **動(dòng)態(tài)代理** : 另外,動(dòng)態(tài)代理也是需要的。因?yàn)?RPC 的主要目的就是讓我們調(diào)用遠(yuǎn)程方法像調(diào)用本地方法一樣簡(jiǎn)單,使用動(dòng)態(tài)代理可以屏蔽遠(yuǎn)程方法調(diào)用的細(xì)節(jié)比如網(wǎng)絡(luò)傳輸。也就是說(shuō)當(dāng)你調(diào)用遠(yuǎn)程方法的時(shí)候,實(shí)際會(huì)通過(guò)代理對(duì)象來(lái)傳輸網(wǎng)絡(luò)請(qǐng)求,不然的話,怎么可能直接就調(diào)用到遠(yuǎn)程方法呢?
5. **負(fù)載均衡** :負(fù)載均衡也是需要的。為啥?舉個(gè)例子我們的系統(tǒng)中的某個(gè)服務(wù)的訪問(wèn)量特別大,我們將這個(gè)服務(wù)部署在了多臺(tái)服務(wù)器上,當(dāng)客戶端發(fā)起請(qǐng)求的時(shí)候,多臺(tái)服務(wù)器都可以處理這個(gè)請(qǐng)求。那么,如何正確選擇處理該請(qǐng)求的服務(wù)器就很關(guān)鍵。假如,你就要一臺(tái)服務(wù)器來(lái)處理該服務(wù)的請(qǐng)求,那該服務(wù)部署在多臺(tái)服務(wù)器的意義就不復(fù)存在了。負(fù)載均衡就是為了避免單個(gè)服務(wù)器響應(yīng)同一請(qǐng)求,容易造成服務(wù)器宕機(jī)、崩潰等問(wèn)題,我們從負(fù)載均衡的這四個(gè)字就能明顯感受到它的意義。
6. ......
項(xiàng)目基本情況和可優(yōu)化點(diǎn)
為了循序漸進(jìn),最初的是時(shí)候,我是基于傳統(tǒng)的 **BIO** 的方式 **Socket** 進(jìn)行網(wǎng)絡(luò)傳輸,然后利用 **JDK 自帶的序列化機(jī)制** 來(lái)實(shí)現(xiàn)這個(gè) RPC 框架的。后面,我對(duì)原始版本進(jìn)行了優(yōu)化,已完成的優(yōu)化點(diǎn)和可以完成的優(yōu)化點(diǎn)我都列在了下面 ??。
**為什么要把可優(yōu)化點(diǎn)列出來(lái)?** 主要是想給哪些希望優(yōu)化這個(gè) RPC 框架的小伙伴一點(diǎn)思路。歡迎大家 fork 本倉(cāng)庫(kù),然后自己進(jìn)行優(yōu)化。
- [x] **使用 Netty(基于 NIO)替代 BIO 實(shí)現(xiàn)網(wǎng)絡(luò)傳輸;**
- [x] **使用開(kāi)源的序列化機(jī)制 Kyro(也可以用其它的)替代 JDK 自帶的序列化機(jī)制;**
- [x] **使用 Zookeeper 管理相關(guān)服務(wù)地址信息**
- [x] Netty 重用 Channel 避免重復(fù)連接服務(wù)端
- [x] 使用 `CompletableFuture` 包裝接受客戶端返回結(jié)果(之前的實(shí)現(xiàn)是通過(guò) `AttributeMap` 綁定到 Channel 上實(shí)現(xiàn)的) 詳見(jiàn):[使用 CompletableFuture 優(yōu)化接受服務(wù)提供端返回結(jié)果](./docs/使用CompletableFuture優(yōu)化接受服務(wù)提供端返回結(jié)果.md)
- [x] **增加 Netty 心跳機(jī)制** : 保證客戶端和服務(wù)端的連接不被斷掉,避免重連。
- [x] **客戶端調(diào)用遠(yuǎn)程服務(wù)的時(shí)候進(jìn)行負(fù)載均衡** :調(diào)用服務(wù)的時(shí)候,從很多服務(wù)地址中根據(jù)相應(yīng)的負(fù)載均衡算法選取一個(gè)服務(wù)地址。ps:目前只實(shí)現(xiàn)了隨機(jī)負(fù)載均衡算法。
- [x] **處理一個(gè)接口有多個(gè)類(lèi)實(shí)現(xiàn)的情況** :對(duì)服務(wù)分組,發(fā)布服務(wù)的時(shí)候增加一個(gè) group 參數(shù)即可。
- [x] **集成 Spring 通過(guò)注解注冊(cè)服務(wù)**
- [x] **集成 Spring 通過(guò)注解進(jìn)行服務(wù)消費(fèi)** 。參考: [PR#10](https://github.com/Snailclimb/guide-rpc-framework/pull/10)
- [x] **增加服務(wù)版本號(hào)** :建議使用兩位數(shù)字版本,如:1.0,通常在接口不兼容時(shí)版本號(hào)才需要升級(jí)。為什么要增加服務(wù)版本號(hào)?為后續(xù)不兼容升級(jí)提供可能,比如服務(wù)接口增加方法,或服務(wù)模型增加字段,可向后兼容,刪除方法或刪除字段,將不兼容,枚舉類(lèi)型新增字段也不兼容,需通過(guò)變更版本號(hào)升級(jí)。
- [x] **對(duì) SPI 機(jī)制的運(yùn)用**
- [ ] **增加可配置比如序列化方式、注冊(cè)中心的實(shí)現(xiàn)方式,避免硬編碼** :通過(guò) API 配置,后續(xù)集成 Spring 的話建議使用配置文件的方式進(jìn)行配置
- [x] **客戶端與服務(wù)端通信協(xié)議(數(shù)據(jù)包結(jié)構(gòu))重新設(shè)計(jì)** ,可以將原有的 `RpcRequest`和 `RpcReuqest` 對(duì)象作為消息體,然后增加如下字段(可以參考:《Netty 入門(mén)實(shí)戰(zhàn)小冊(cè)》和 Dubbo 框架對(duì)這塊的設(shè)計(jì)):
- **魔數(shù)** : 通常是 4 個(gè)字節(jié)。這個(gè)魔數(shù)主要是為了篩選來(lái)到服務(wù)端的數(shù)據(jù)包,有了這個(gè)魔數(shù)之后,服務(wù)端首先取出前面四個(gè)字節(jié)進(jìn)行比對(duì),能夠在第一時(shí)間識(shí)別出這個(gè)數(shù)據(jù)包并非是遵循自定義協(xié)議的,也就是無(wú)效數(shù)據(jù)包,為了安全考慮可以直接關(guān)閉連接以節(jié)省資源。
- **序列化器編號(hào)** :標(biāo)識(shí)序列化的方式,比如是使用 Java 自帶的序列化,還是 json,kyro 等序列化方式。
- **消息體長(zhǎng)度** : 運(yùn)行時(shí)計(jì)算出來(lái)。
- ......
- [ ] **編寫(xiě)測(cè)試為重構(gòu)代碼提供信心**
- [ ] **服務(wù)監(jiān)控中心(類(lèi)似dubbo admin)**
項(xiàng)目模塊概覽
運(yùn)行項(xiàng)目
導(dǎo)入項(xiàng)目
fork 項(xiàng)目到自己的倉(cāng)庫(kù),然后克隆項(xiàng)目到自己的本地:`git clone [email protected]:username/guide-rpc-framework.git`,使用 IDEA 打開(kāi),等待項(xiàng)目初始化完成。
初始化 git hooks
這一步主要是為了在 commit 代碼之前,跑 Check Style,保證代碼格式?jīng)]問(wèn)題,如果有問(wèn)題的話就不能提交。
以下演示的是 Mac/Linux 對(duì)應(yīng)的操作,Window 用戶需要手動(dòng)將
config/git-hooks目錄下的pre-commit文件拷貝到 項(xiàng)目下的.git/hooks/目錄。
執(zhí)行下面這些命令:
? guide-rpc-framework git:(master) ? chmod +x ./init.sh ? guide-rpc-framework git:(master) ? ./init.sh
init.sh 這個(gè)腳本的主要作用是將 git commit 鉤子拷貝到項(xiàng)目下的 .git/hooks/ 目錄,這樣你每次 commit 的時(shí)候就會(huì)執(zhí)行了。
CheckStyle 插件下載和配置
IntelliJ IDEA-> Preferences->Plugins->搜索下載 CheckStyle 插件,然后按照如下方式進(jìn)行配置。
配置完成之后,按照如下方式使用這個(gè)插件!
下載運(yùn)行 zookeeper
這里使用 Docker 來(lái)下載安裝。
下載:
docker pull zookeeper:3.5.8
運(yùn)行:
docker run -d --name zookeeper -p 2181:2181 zookeeper:3.5.8
使用
服務(wù)提供端
實(shí)現(xiàn)接口:
@Slf4j
@RpcService(group = "test1", version = "version1")
public class HelloServiceImpl implements HelloService {
static {
System.out.println("HelloServiceImpl被創(chuàng)建");
}
@Override
public String hello(Hello hello) {
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被創(chuàng)建");
}
@Override
public String hello(Hello hello) {
log.info("HelloServiceImpl2收到: {}.", hello.getMessage());
String result = "Hello description is " + hello.getDescription();
log.info("HelloServiceImpl2返回: {}.", result);
return result;
}
}
發(fā)布服務(wù)(使用 Netty 進(jìn)行傳輸):
/**
* Server: Automatic registration service via @RpcService annotation
*
* @author shuang.kou
* @createTime 2020年05月10日 07:25:00
*/
@RpcScan(basePackage = {"github.javaguide.serviceimpl"})
public class NettyServerMain {
public static void main(String[] args) {
// Register service via annotation
new AnnotationConfigApplicationContext(NettyServerMain.class);
NettyServer nettyServer = new NettyServer();
// Register service manually
HelloService helloService2 = new HelloServiceImpl2();
RpcServiceProperties rpcServiceProperties = RpcServiceProperties.builder()
.group("test2").version("version2").build();
nettyServer.registerService(helloService2, rpcServiceProperties);
nettyServer.start();
}
}
服務(wù)消費(fèi)端
@Component
public class HelloController {
@RpcReference(version = "version1", group = "test1")
private HelloService helloService;
public void test() throws InterruptedException {
String hello = this.helloService.hello(new Hello("111", "222"));
//如需使用 assert 斷言,需要在 VM options 添加參數(shù):-ea
assert "Hello description is 222".equals(hello);
Thread.sleep(12000);
for (int i = 0; i < 10; i++) {
System.out.println(helloService.hello(new Hello("111", "222")));
}
}
}
ClientTransport clientTransport = new SocketRpcClient();
RpcServiceProperties rpcServiceProperties = RpcServiceProperties.builder()
.group("test2").version("version2").build();
RpcClientProxy rpcClientProxy = new RpcClientProxy(clientTransport, rpcServiceProperties);
HelloService helloService = rpcClientProxy.getProxy(HelloService.class);
String hello = helloService.hello(new Hello("111", "222"));
System.out.println(hello);
相關(guān)問(wèn)題
為什么要造這個(gè)輪子?Dubbo 不香么?
寫(xiě)這個(gè) RPC 框架主要是為了通過(guò)造輪子的方式來(lái)學(xué)習(xí),檢驗(yàn)自己對(duì)于自己所掌握的知識(shí)的運(yùn)用。
實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 RPC 框架實(shí)際是比較容易的,不過(guò),相比于手寫(xiě) AOP 和 IoC 還是要難一點(diǎn)點(diǎn),前提是你搞懂了 RPC 的基本原理。
我之前從理論層面在我的知識(shí)星球分享過(guò)如何實(shí)現(xiàn)一個(gè) RPC。不過(guò)理論層面的東西只是支撐,你看懂了理論可能只能糊弄住面試官。咱程序員這一行還是最需要?jiǎng)邮帜芰?,即使你是架?gòu)師級(jí)別的人物。當(dāng)你動(dòng)手去實(shí)踐某個(gè)東西,將理論付諸實(shí)踐的時(shí)候,你就會(huì)發(fā)現(xiàn)有很多坑等著你。
大家在實(shí)際項(xiàng)目上還是要盡量少造輪子,有優(yōu)秀的框架之后盡量就去用,Dubbo 在各個(gè)方面做的都比較好和完善。
如果我要自己寫(xiě)的話,需要提前了解哪些知識(shí)
**Java** :
1. 動(dòng)態(tài)代理機(jī)制;
2. 序列化機(jī)制以及各種序列化框架的對(duì)比,比如 hession2、kyro、protostuff。
3. 線程池的使用;
4. `CompletableFuture` 的使用
5. ......
**Netty** :
1. 使用 Netty 進(jìn)行網(wǎng)絡(luò)傳輸;
2. `ByteBuf` 介紹
3. Netty 粘包拆包
4. Netty 長(zhǎng)連接和心跳機(jī)制
**Zookeeper** :
1. 基本概念;
2. 數(shù)據(jù)結(jié)構(gòu);
3. 如何使用 Netflix 公司開(kāi)源的 zookeeper 客戶端框架 Curator 進(jìn)行增刪改查;
