我手写了一个RPC框架。成功帮助读者斩获字节、阿里等大厂offer。
本着开源精神,本项目README已经同步了英文版本。另外,项目的源代码的注释大部分也修改为了英文。
如访问速度不佳,可放在 Gitee 地址:https://gitee.com/SnailClimb/guide-rpc-framework 。如果要提交 issue 或者 pr 的话,请在 Github 提交:https://github.com/Snailclimb/guide-rpc-framework 。
相关项目:
- Netty 从入门到实战 : https://github.com/Snailclimb/netty-practical-tutorial
- 「Java学习+面试指南」一份涵盖大部分Java程序员所需要掌握的核心知识。: https://github.com/Snailclimb/JavaGuide
前言
虽说 RPC 的原理实际不难,但是,自己在实现的过程中自己也遇到了很多问题。guide-rpc-framework 目前只实现了 RPC 框架最基本的功能,一些可优化点都在下面提到了,有兴趣的小伙伴可以自行完善。
通过这个简易的轮子,你可以学到 RPC 的底层原理和原理以及各种 Java 编码实践的运用。
你甚至可以把 guide-rpc-framework 当做你的毕设/项目经验的选择,这是非常不错!对比其他求职者的项目经验都是各种系统,造轮子肯定是更加能赢得面试官的青睐。
如果你要将 guide-rpc-framework 当做你的毕设/项目经验的话,我希望你一定要搞懂,而不是直接复制粘贴我的思想。你可以 fork 我的项目,然后进行优化。如果你觉得的优化是有价值的话,你可以提交 PR 给我,我会尽快处理。
介绍
guide-rpc-framework 是一款基于 Netty+Kyro+Zookeeper 实现的 RPC 框架。代码注释详细,结构清晰,并且集成了 Check Style 规范代码结构,非常适合阅读和学习。
由于 Guide哥自身精力和能力有限,如果大家觉得有需要改进和完善的地方的话,欢迎 fork 本项目,然后 clone 到本地,在本地修改后提交 PR 给我,我会在第一时间 Review 你的代码。
我们先从一个基本的 RPC 框架设计思路说起!
一个基本的 RPC 框架设计思路
注意 :我们这里说的 RPC 框架指的是:可以让客户端直接调用服务端方法就像调用本地方法一样简单的框架,比如我前面介绍的 Dubbo、Motan、gRPC 这些。 如果需要和 HTTP 协议打交道,解析和封装 HTTP 请求和响应。这类框架并不能算是“RPC 框架”,比如 Feign。
一个最简单的 RPC 框架使用示意图如下图所示,这也是 guide-rpc-framework 目前的架构 :
服务提供端 Server 向注册中心注册服务,服务消费者 Client 通过注册中心拿到服务相关信息,然后再通过网络请求服务提供端 Server。
作为 RPC 框架领域的佼佼者Dubbo的架构如下图所示,和我们上面画的大体也是差不多的。
一般情况下, RPC 框架不仅要提供服务发现功能,还要提供负载均衡、容错等功能,这样的 RPC 框架才算真正合格的。
简单说一下设计一个最基本的 RPC 框架的思路:
- 注册中心 :注册中心首先是要有的,推荐使用 Zookeeper。注册中心负责服务地址的注册与查找,相当于目录服务。服务端启动的时候将服务名称及其对应的地址(ip+port)注册到注册中心,服务消费端根据服务名称找到对应的服务地址。有了服务地址之后,服务消费端就可以通过网络请求服务端了。
- 网络传输 :既然要调用远程的方法就要发请求,请求中至少要包含你调用的类名、方法名以及相关参数吧!推荐基于 NIO 的 Netty 框架。
- 序列化 :既然涉及到网络传输就一定涉及到序列化,你不可能直接使用 JDK 自带的序列化吧!JDK 自带的序列化效率低并且有安全漏洞。 所以,你还要考虑使用哪种序列化协议,比较常用的有 hession2、kyro、protostuff。
- 动态代理 : 另外,动态代理也是需要的。因为 RPC 的主要目的就是让我们调用远程方法像调用本地方法一样简单,使用动态代理可以屏蔽远程方法调用的细节比如网络传输。也就是说当你调用远程方法的时候,实际会通过代理对象来传输网络请求,不然的话,怎么可能直接就调用到远程方法呢?
- 负载均衡 :负载均衡也是需要的。为啥?举个例子我们的系统中的某个服务的访问量特别大,我们将这个服务部署在了多台服务器上,当客户端发起请求的时候,多台服务器都可以处理这个请求。那么,如何正确选择处理该请求的服务器就很关键。假如,你就要一台服务器来处理该服务的请求,那该服务部署在多台服务器的意义就不复存在了。负载均衡就是为了避免单个服务器响应同一请求,容易造成服务器宕机、崩溃等问题,我们从负载均衡的这四个字就能明显感受到它的意义。
- ......
项目基本情况和可优化点
为了循序渐进,最初的是时候,我是基于传统的 BIO 的方式 Socket 进行网络传输,然后利用 JDK 自带的序列化机制 来实现这个 RPC 框架的。后面,我对原始版本进行了优化,已完成的优化点和可以完成的优化点我都列在了下面 。
为什么要把可优化点列出来? 主要是想给哪些希望优化这个 RPC 框架的小伙伴一点思路。欢迎大家 fork 本仓库,然后自己进行优化。
使用 Netty(基于 NIO)替代 BIO 实现网络传输;
使用开源的序列化机制 Kyro(也可以用其它的)替代 JDK 自带的序列化机制;
使用 Zookeeper 管理相关服务地址信息
Netty 重用 Channel 避免重复连接服务端
使用
CompletableFuture
包装接受客户端返回结果(之前的实现是通过AttributeMap
绑定到 Channel 上实现的) 详见:使用 CompletableFuture 优化接受服务提供端返回结果增加 Netty 心跳机制 : 保证客户端和服务端的连接不被断掉,避免重连。
客户端调用远程服务的时候进行负载均衡 :调用服务的时候,从很多服务地址中根据相应的负载均衡算法选取一个服务地址。ps:目前只实现了随机负载均衡算法。
处理一个接口有多个类实现的情况 :对服务分组,发布服务的时候增加一个 group 参数即可。
集成 Spring 通过注解注册服务
增加服务版本号 :建议使用两位数字版本,如:1.0,通常在接口不兼容时版本号才需要升级。为什么要增加服务版本号?为后续不兼容升级提供可能,比如服务接口增加方法,或服务模型增加字段,可向后兼容,删除方法或删除字段,将不兼容,枚举类型新增字段也不兼容,需通过变更版本号升级。
对 SPI 机制的运用
增加可配置比如序列化方式、注册中心的实现方式,避免硬编码 :通过 API 配置,后续集成 Spring 的话建议使用配置文件的方式进行配置
使用注解进行服务消费
客户端与服务端通信协议(数据包结构)重新设计
,可以将原有的
RpcRequest
和
RpcReuqest
对象作为消息体,然后增加如下字段(可以参考:《Netty 入门实战小册》和 Dubbo 框架对这块的设计):
魔数 : 通常是 4 个字节。这个魔数主要是为了筛选来到服务端的数据包,有了这个魔数之后,服务端首先取出前面四个字节进行比对,能够在第一时间识别出这个数据包并非是遵循自定义协议的,也就是无效数据包,为了安全考虑可以直接关闭连接以节省资源。
序列化器编号 :标识序列化的方式,比如是使用 Java 自带的序列化,还是 json,kyro 等序列化方式。
消息体长度 : 运行时计算出来。
......
编写测试为重构代码提供信心
项目模块概览
运行项目
导入项目
fork 项目到自己的仓库,然后克隆项目到自己的本地:git clone git@github.com:username/guide-rpc-framework.git
,使用 IDEA 打开,等待项目初始化完成。
初始化 git hooks
这一步主要是为了在 commit 代码之前,跑 Check Style,保证代码格式没问题,如果有问题的话就不能提交。
以下演示的是 Mac/Linux 对应的操作,Window 用户需要手动将
config/git-hooks
目录下的pre-commit
文件拷贝到 项目下的.git/hooks/
目录。
执行下面这些命令:
➜ guide-rpc-framework git:(master) ✗ chmod +x ./init.sh
➜ guide-rpc-framework git:(master) ✗ ./init.sh
init.sh
这个脚本的主要作用是将 git commit 钩子拷贝到项目下的 .git/hooks/
目录,这样你每次 commit 的时候就会执行了。
CheckStyle 插件下载和配置
IntelliJ IDEA-> Preferences->Plugins->搜索下载 CheckStyle 插件,然后按照如下方式进行配置。
配置完成之后,按照如下方式使用这个插件!
下载运行 zookeeper
这里使用 Docker 来下载安装。
下载:
docker pull zookeeper:3.5.8
运行:
docker run -d --name zookeeper -p 2181:2181 zookeeper:3.5.8
使用
服务提供端
实现接口:
@Slf4j
@RpcService(group = "test1", version = "version1")
public class HelloServiceImpl implements HelloService {
static {
System.out.println("HelloServiceImpl被创建");
}
@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被创建");
}
@Override
public String hello(Hello hello) {
log.info("HelloServiceImpl2收到: {}.", hello.getMessage());
String result = "Hello description is " + hello.getDescription();
log.info("HelloServiceImpl2返回: {}.", result);
return result;
}
}
发布服务(使用 Netty 进行传输):
/**
* 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();
}
}
服务消费端
ClientTransport rpcClient = new NettyClientTransport();
RpcServiceProperties rpcServiceProperties = RpcServiceProperties.builder()
.group("test1").version("version1").build();
RpcClientProxy rpcClientProxy = new RpcClientProxy(rpcClient, rpcServiceProperties);
HelloService helloService = rpcClientProxy.getProxy(HelloService.class);
String hello = helloService.hello(new Hello("111", "222"));
相关问题
为什么要造这个轮子?Dubbo 不香么?
写这个 RPC 框架主要是为了通过造轮子的方式来学习,检验自己对于自己所掌握的知识的运用。
实现一个简单的 RPC 框架实际是比较容易的,不过,相比于手写 AOP 和 IoC 还是要难一点点,前提是你搞懂了 RPC 的基本原理。
我之前从理论层面在我的知识星球分享过如何实现一个 RPC。不过理论层面的东西只是支撑,你看懂了理论可能只能糊弄住面试官。咱程序员这一行还是最需要动手能力,即使你是架构师级别的人物。当你动手去实践某个东西,将理论付诸实践的时候,你就会发现有很多坑等着你。
大家在实际项目上还是要尽量少造轮子,有优秀的框架之后尽量就去用,Dubbo 在各个方面做的都比较好和完善。
如果我要自己写的话,需要提前了解哪些知识
Java :
- 动态代理机制;
- 序列化机制以及各种序列化框架的对比,比如 hession2、kyro、protostuff。
- 线程池的使用;
CompletableFuture
的使用- ......
Netty :
- 使用 Netty 进行网络传输;
ByteBuf
介绍- Netty 粘包拆包
- Netty 长连接和心跳机制
Zookeeper :
- 基本概念;
- 数据结构;
- 如何使用 Netflix 公司开源的 zookeeper 客户端框架 Curator 进行增删改查;
我手写了一个RPC框架。成功帮助读者斩获字节、阿里等大厂offer。的更多相关文章
- 手写简易版RPC框架基于Socket
什么是RPC框架? RPC就是远程调用过程,实现各个服务间的通信,像调用本地服务一样. RPC有什么优点? - 提高服务的拓展性,解耦.- 开发人员可以针对模块开发,互不影响.- 提升系统的可维护性及 ...
- 手写一个RPC框架
一.前言 前段时间看到一篇不错的文章<看了这篇你就会手写RPC框架了>,于是便来了兴趣对着实现了一遍,后面觉得还有很多优化的地方便对其进行了改进. 主要改动点如下: 除了Java序列化协议 ...
- 从0 开始手写一个 RPC 框架,轻松搞定!
Java技术栈 www.javastack.cn 优秀的Java技术公众号 来源:juejin.im/post/5c4481a4f265da613438aec3 之前在 RPC框架底层到底什么原理得知 ...
- 纯手写SpringMVC到SpringBoot框架项目实战
引言 Spring Boot其设计目的是用来简化新Spring应用的初始搭建以及开发过程.该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置. 通过这种方式,springboot ...
- 为sproto手写了一个python parser
这是sproto系列文章的第三篇,可以参考前面的<为sproto添加python绑定>.<为python-sproto添加map支持>. sproto是云风设计的序列化协议,用 ...
- servlet(1) - 手写第一个servlet程序 - 小易Java笔记
声明:如tomcat的安装目录为D:\Java\tomcat6,下面要根据tomcat的安装目录而定 1. 建立程序的文件结构 ==>找到tomcat的安装目录,在webapps目录下新建一个名 ...
- 手撸了一个HTTP框架:支持Sprng MVC、IOC、AOP,拦截器,配置文件读取...
https://github.com/Snailclimb/jsoncat :仿 Spring Boot 但不同于 Spring Boot 的一个轻量级的 HTTP 框架 距离上一次给小伙伴们汇报简易 ...
- 手写mini版MVC框架
目录 1, Springmvc基本原理流程 2,注解开发 编写测试代码: 目录结构: 3,编写自定义DispatcherServlet中的初始化流程: 3.1 加载配置文件 3.2 扫描相关的类,扫描 ...
- 学习写简单的RPC框架demo
学习实现一个简单的RPC框架. 工程主要目录分级结构: rpc-common: 公共基础包,能力提供包 rpc-provider: 服务提供者 rpc-consumer:服务消费者 rpc-servi ...
- Neeto-Vue:我为了记笔记,手写了一个为知笔记客户端
构建自托管的笔记系统 这两年各种笔记系统快速井喷,好像谁都能来掺一脚,app store随便搜索一个关键字就会有一大堆的结果,从老牌的印象笔记,Bear,MWeb,有道云笔记再到新星专注笔记之类,从买 ...
随机推荐
- 最被低估的Python绘图库!Matlplotlib 超强实力鉴赏
最被低估的Python绘图库!Matlplotlib 超强实力鉴赏 Matplotlib Matplotlib 是一个 Python 的 2D绘图库,它以各种硬拷贝格式和跨平台的交互式环境生成出版质量 ...
- Kafka--简介,部署
kafka官网:https://kafka.apache.org/documentation/ 本文kafka版本:3.1.0 一.简介 Kafka是一种高吞吐量的分布式发布订阅消息系统,它可以处理消 ...
- 库函数 | C++17 std::filesystem文件系统 用法指北
本文将针对常用的场景,对 std::filesystem 的使用逐一进行验证: 判断文件夹是否存在 创建单层目录 逐级创建多层目录 创建多级目录 当前文件路径 创建文件"from.dat&q ...
- 【每日一题】20.K-th Number (二分 + 尺取)
关于此题,我们分析一下: 一个区间第k大的数不小于x的条件是什么? 答案就是一个区间内不小于x的数的个数不小于k 那么,我们就会发现,我们其实并不需要知道每个数的值,实际上对我们有用的只有每个数与x的 ...
- Kafka的Rebalance机制可能造成的影响及解决方案
一.kafka的rebalance机制在Kafka中,当有新消费者加入或者订阅的Topic数发生变化时,会触发Rebalance(再均衡:在同一个消费者组当中,分区的所有权从一个消费者转移到另外一个消 ...
- 上海丨阿里云 Serverless 技术实战营邀你来玩!
活动简介 本次沙龙深度探讨 "Serverless 在中国企业的落地和开发者实操" 主题,我们特别邀请了来自阿里云 一线技术专家,分享当前 Serverless 趋势和落地实践过程 ...
- 11、SpringBoot-mybatis-plus-druid多源数据事务
系列导航 springBoot项目打jar包 1.springboot工程新建(单模块) 2.springboot创建多模块工程 3.springboot连接数据库 4.SpringBoot连接数据库 ...
- arguments使用
arguments在函数中可以使用,表示包含了当前函数调用时传入的实参. arguments通过索引值获取实参,索引值从开始,按顺序依次表示函数调用时传过来的实参. 1 // arguments的使用 ...
- Liunx常用操作(八)-sed命令详细说明
一.sed简介 sed是一种流编编器,它是文本处理中非常中的工具,能够完美的配合正则表达式便用,功物能不同凡响. 处理时,把当前处理的行存储在临时缓冲区中,称为"模式空间"( oa ...
- 如何将一个JAR包添加到Java应用程序的Boot Classpath中?
1. 在启动脚本中使用-bootstrap或-Xbootclasspath选项 这两个选项的使用方式如下: -bootstrap选项: java -bootstrap /path/to/your.ja ...