乐趣区

关于java:我手写了一个RPC框架成功帮助读者斩获字节阿里等大厂offer

本着开源精力,本我的项目 README 曾经同步了英文版本。另外,我的项目的源代码的正文大部分也批改为了英文。

如访问速度不佳,可放在 Gitee 地址:https://gitee.com/SnailClimb/…。如果要提交 issue 或者 pr 的话,请在 Github 提交:https://github.com/Snailclimb…。

相干我的项目:

  1. Netty 从入门到实战 : https://github.com/Snailclimb/netty-practical-tutorial
  2. 「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 框架的思路:

  1. 注册核心 :注册核心首先是要有的,举荐应用 Zookeeper。注册核心负责服务地址的注册与查找,相当于目录服务。服务端启动的时候将服务名称及其对应的地址(ip+port) 注册到注册核心,服务生产端依据服务名称找到对应的服务地址。有了服务地址之后,服务生产端就能够通过网络申请服务端了。
  2. 网络传输:既然要调用近程的办法就要发申请,申请中至多要蕴含你调用的类名、办法名以及相干参数吧!举荐基于 NIO 的 Netty 框架。
  3. 序列化:既然波及到网络传输就肯定波及到序列化,你不可能间接应用 JDK 自带的序列化吧!JDK 自带的序列化效率低并且有安全漏洞。所以,你还要思考应用哪种序列化协定,比拟罕用的有 hession2、kyro、protostuff。
  4. 动静代理:另外,动静代理也是须要的。因为 RPC 的次要目标就是让咱们调用近程办法像调用本地办法一样简略,应用动静代理能够屏蔽近程办法调用的细节比方网络传输。也就是说当你调用近程办法的时候,理论会通过代理对象来传输网络申请,不然的话,怎么可能间接就调用到近程办法呢?
  5. 负载平衡:负载平衡也是须要的。为啥?举个例子咱们的零碎中的某个服务的访问量特地大,咱们将这个服务部署在了多台服务器上,当客户端发动申请的时候,多台服务器都能够解决这个申请。那么,如何正确抉择解决该申请的服务器就很要害。如果,你就要一台服务器来解决该服务的申请,那该服务部署在多台服务器的意义就不复存在了。负载平衡就是为了防止单个服务器响应同一申请,容易造成服务器宕机、解体等问题,咱们从负载平衡的这四个字就能显著感触到它的意义。
  6. ……

我的项目根本状况和可优化点

为了循序渐进,最后的是时候,我是基于传统的 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

  1. 动静代理机制;
  2. 序列化机制以及各种序列化框架的比照,比方 hession2、kyro、protostuff。
  3. 线程池的应用;
  4. CompletableFuture 的应用
  5. ……

Netty

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

Zookeeper :

  1. 基本概念;
  2. 数据结构;
  3. 如何应用 Netflix 公司开源的 zookeeper 客户端框架 Curator 进行增删改查;
退出移动版