我手寫了一個RPC框架。成功幫助讀者斬獲字節、阿里等大廠offer。


本着開源精神,本項目README已經同步了英文版本。另外,項目的源代碼的注釋大部分也修改為了英文。

如訪問速度不佳,可放在 Gitee 地址:https://gitee.com/SnailClimb/guide-rpc-framework 。如果要提交 issue 或者 pr 的話,請在 Github 提交:https://github.com/Snailclimb/guide-rpc-framework

相關項目:

  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 目前的架構 :

img

服務提供端 Server 向注冊中心注冊服務,服務消費者 Client 通過注冊中心拿到服務相關信息,然后再通過網絡請求服務提供端 Server。

作為 RPC 框架領域的佼佼者Dubbo的架構如下圖所示,和我們上面畫的大體也是差不多的。

img

一般情況下, RPC 框架不僅要提供服務發現功能,還要提供負載均衡、容錯等功能,這樣的 RPC 框架才算真正合格的。

簡單說一下設計一個最基本的 RPC 框架的思路:

img

  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 等序列化方式。

  • 消息體長度 : 運行時計算出來。

  • ......

  • 編寫測試為重構代碼提供信心

項目模塊概覽

img

運行項目

導入項目

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 插件,然后按照如下方式進行配置。

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 進行增刪改查;


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM