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

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

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

相關項目:程序員

  1. Netty 從入門到實戰 : https://github.com/Snailclimb/netty-practical-tutorial
  2. 「Java學習+面試指南」一份涵蓋大部分Java程序員所須要掌握的核心知識。: https://github.com/Snailclimb/JavaGuide

前言

雖然說 RPC 的原理實際不難,可是,本身在實現的過程當中本身也遇到了不少問題。guide-rpc-framework 目前只實現了 RPC 框架最基本的功能,一些可優化點都在下面提到了,有興趣的小夥伴能夠自行完善。github

經過這個簡易的輪子,你能夠學到 RPC 的底層原理和原理以及各類 Java 編碼實踐的運用。面試

你甚至能夠把 guide-rpc-framework 當作你的畢設/項目經驗的選擇,這是很是不錯!對比其餘求職者的項目經驗都是各類系統,造輪子確定是更加能贏得面試官的青睞。算法

若是你要將 guide-rpc-framework 當作你的畢設/項目經驗的話,我但願你必定要搞懂,而不是直接複製粘貼個人思想。你能夠 fork 個人項目,而後進行優化。若是你以爲的優化是有價值的話,你能夠提交 PR 給我,我會盡快處理。docker

介紹

guide-rpc-framework 是一款基於 Netty+Kyro+Zookeeper 實現的 RPC 框架。代碼註釋詳細,結構清晰,而且集成了 Check Style 規範代碼結構,很是適合閱讀和學習。apache

因爲 Guide哥自身精力和能力有限,若是你們以爲有須要改進和完善的地方的話,歡迎 fork 本項目,而後 clone 到本地,在本地修改後提交 PR 給我,我會在第一時間 Review 你的代碼。json

咱們先從一個基本的 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 自帶的序列化效率低而且有安全漏洞。 因此,你還要考慮使用哪一種序列化協議,比較經常使用的有 hession二、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. 序列化機制以及各類序列化框架的對比,好比 hession二、kyro、protostuff。
  3. 線程池的使用;
  4. CompletableFuture 的使用
  5. ......

Netty

  1. 使用 Netty 進行網絡傳輸;
  2. ByteBuf 介紹
  3. Netty 粘包拆包
  4. Netty 長鏈接和心跳機制

Zookeeper :

  1. 基本概念;
  2. 數據結構;
  3. 如何使用 Netflix 公司開源的 zookeeper 客戶端框架 Curator 進行增刪改查;
相關文章
相關標籤/搜索