幾行代碼搞定 RPC 服務註冊和發現

在編碼前,需要跟大家說一下,整個項目是按照一個一個功能模塊疊加實現的,由於文章排版不適合放大塊代碼,文章裏我會截取最關鍵的代碼給大家講解,想要獲取完整的代碼,可以去 Github 上下載,已經正式開源了。

easy-rpc 開源地址:
https://github.com/CoderLeixiaoshuai/easy-rpc

注意:源碼可能會更新,記得拉取最新的。

需求分析:服務註冊和發現

rpc 項目要實現的第一個功能模塊就是:服務註冊和發現,這個功能也是整個框架非常核心和關鍵的。

關於服務註冊發現的介紹和原理,可以看這篇文章:《10 張圖搞懂服務註冊發現機制

我們的 rpc 項目不用於生成環境,造個輪子嘛,只需要實現最基礎的功能即可:

需求很明確了,下面開始寫代碼。

引入三方依賴

市面上靠譜的註冊中心還是很多的,這次打算同時兼容兩種註冊中心:ZookeeperNacos,是不是很良心?!在使用前需要先引入以下依賴。

與 Zookeeper 交互可以引入對應的 SDK,zkclient 是個不錯的選擇;JSON 序列化和反序列化可以引入 fastjson,雖然經常爆漏洞,但是國產還是得支持下:

<!-- Zookeeper 客戶端 -->
<dependency>
    <groupId>com.101tec</groupId>
    <artifactId>zkclient</artifactId>
    <version>0.10</version>
</dependency>
<!--Json 序列化反序列-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.80</version>
</dependency>

至於 Nacos,可以直接引入官方提供的 SDK:nacos-client:

<dependency>
  <groupId>com.alibaba.nacos</groupId>
  <artifactId>nacos-client</artifactId>
  <version>2.0.3</version>
</dependency>

服務端實現服務註冊

服務註冊和發現分爲兩塊功能:服務端註冊和客戶端發現,我們先來實現服務端註冊功能。

定義服務註冊接口

在日常的工作或者學習編碼過程中,我們一定要習慣面向接口編程,這樣做有助於增強代碼可擴展性。

根據前面的需求描述,服務註冊只需要幹一件事情:服務註冊,我們可以定義一個接口:ServiceRegistry,接口中定義一個方法:register,代碼如下:

public interface ServiceRegistry {
    /**
     * 註冊服務信息
     *
     * @param serviceInfo 待註冊的服務
     * @throws Exception 異常
     */
    void register(ServiceInfo serviceInfo) throws Exception;

}

服務向註冊中心註冊,註冊的內容定義一個類ServiceInfo來封裝。

    /**
     * 服務名稱
     */
    private String serviceName;

    /**
     * ip 地址
     */
    private String ip;

    /**
     * 端口號
     */
    private Integer port;

    /**
     * class 對象
     *
     */
    private Class<?> clazz;

    /**
     * bean 對象
     */
    private Object obj;
  
    // 省略 get set 方法……
}

Zookeeper 實現服務註冊

我們嘗試用 Zookeeper 來實現服務註冊功能,先新建一個類實現前面定義好的服務註冊接口:

public class ZookeeperServiceRegistry implements ServiceRegistry {

}

接下來重寫register方法,主要功能包括調用 Zookeeper 接口創建服務節點和實例節點。其中服務節點是一個永久節點,只用創建一次;實例節點是臨時節點,如果實例故障下線,實例節點會自動刪除。

// ZookeeperServiceRegistry.java

    @Override
    public void register(ServiceInfo serviceInfo) throws Exception {
        logger.info("Registering service: {}", serviceInfo);

        // 創建 ZK 永久節點(服務節點)
        String servicePath = "/com/leixiaoshuai/easyrpc/" + serviceInfo.getServiceName() + "/service";
        if (!zkClient.exists(servicePath)) {
            zkClient.createPersistent(servicePath, true);
        }

        // 創建 ZK 臨時節點(實例節點)
        String uri = JSON.toJSONString(serviceInfo);
        uri = URLEncoder.encode(uri, "UTF-8");
        String uriPath = servicePath + "/" + uri;
        if (zkClient.exists(uriPath)) {
            zkClient.delete(uriPath);
        }
        zkClient.createEphemeral(uriPath);
    }

代碼非常簡單,大家看註釋就能懂了。

Nacos 實現服務註冊

除了使用 Zookeeper 來實現,我們還可以使用 Nacos,跟上面一樣我們還是先建一個類:

public class NacosServiceRegistry implements ServiceRegistry {

}

接着編寫構造方法,NacosServiceRegistry 類被實例化之後 Nacos 客戶端也要連接上 Nacos 服務端。

// NacosServiceRegistry.java

public NacosServiceRegistry(String serverList) throws NacosException {
    // 使用工廠類創建註冊中心對象,構造參數爲 Nacos Server 的 ip 地址,連接 Nacos 服務器
    naming = NamingFactory.createNamingService(serverList);
    // 打印 Nacos Server 的運行狀態
    logger.info("Nacos server status: {}", naming.getServerStatus());
}

獲得 NamingService 類的實例對象後,就可以調用實例註冊接口完成服務註冊了。

// NacosServiceRegistry.java

@Override
public void register(ServiceInfo serviceInfo) throws Exception {
    // 註冊當前服務實例
    naming.registerInstance(serviceInfo.getServiceName(), buildInstance(serviceInfo));
}

private Instance buildInstance(ServiceInfo serviceInfo) {
    // 將實例信息註冊到 Nacos 中心
    Instance instance = new Instance();
    instance.setIp(serviceInfo.getIp());
    instance.setPort(serviceInfo.getPort());
    // TODO add more metadata
    return instance;
}

注意:NamingService 類提供了很多有用的方法,大家可自行進行嘗試。

客戶端實現服務發現

定義服務發現接口

前面已經將服務實例註冊到 Zookeeper 或者 Nacos 服務端,現在客戶端想要調用服務端首先得獲得服務端實例列表,這個過程其實就是服務發現

我們先定義一個抽象的接口,這個接口主要的功能就是定義一個獲取服務實例的接口:

public interface ServiceDiscovery {

    /**
     * 通過服務名稱隨機選擇一個健康的實例
     * @param serviceName 服務名稱
     * @return 實例對象
     */
    InstanceInfo selectOneInstance(String serviceName);

}

隨機挑選一個實例是爲了模擬負載均衡,儘量使請求均勻分配到各實例上。

Zookeeper 實現服務發現

前面使用 Zookeeper 實現了服務註冊功能,這裏我們再用 Zookeeper 來實現服務發現功能,先定義一個類實現 ServiceDiscovery 接口:

public class ZookeeperServiceDiscovery implements ServiceDiscovery {

}

下面實現核心方法:selectOneInstance

Zookeeper 內部是一個樹形的節點,通過查找一個指定節點的所有子節點即可獲得服務實例列表。拿到服務實例列表後如何挑選出一個實例呢?這裏就可以引入負載均衡算法了。

// ZookeeperServiceDiscovery.java

@Override
public InstanceInfo selectOneInstance(String serviceName) {
    String servicePath = "/com/leixiaoshuai/easyrpc/" + serviceName + "/service";
    final List<String> childrenNodes = zkClient.getChildren(servicePath);

    return Optional.ofNullable(childrenNodes)
            .orElse(new ArrayList<>())
            .stream()
            .map(node -> {
                try {
                    // 將服務信息經過 URL 解碼後反序列化爲對象
                    String serviceInstanceJson = URLDecoder.decode(node, "UTF-8");
                    return JSON.parseObject(serviceInstanceJson, InstanceInfo.class);
                } catch (UnsupportedEncodingException e) {
                    logger.error("Fail to decode", e);
                }
                return null;
            }).filter(Objects::nonNull).findAny().get();
}

注意:當前項目僅僅用於學習用,這裏沒有引入複雜的負載均衡算法,有興趣的同學可自行補充,歡迎提交 MR 貢獻代碼。

Nacos 實現服務發現

最後來到 Nacos 的實現,話不多說先定義一個類:

public class NacosServiceDiscovery implements ServiceDiscovery {
}

同樣也需要實現核心方法:selectOneInstance。Nacos 的實現就比較簡單了,因爲 Nacos 官方提供的 SDK 功能太強大了,我們直接調用對應的接口就可以了,Nacos 根據算法會隨機挑選一個健康的實例,我們不用關注細節。

// ZookeeperServiceDiscovery.java

@Override
public InstanceInfo selectOneInstance(String serviceName) {
    Instance instance;
    try {
        // 調用 nacos 提供的接口,隨機挑選一個服務實例,負載均衡的算法依賴 nacos 的實現
        instance = namingService.selectOneHealthyInstance(serviceName);
    } catch (NacosException e) {
        logger.error("Nacos exception", e);
        return null;
    }

    // 封裝實例對象返回
    InstanceInfo instanceInfo = new InstanceInfo();
    instanceInfo.setServiceName(instance.getServiceName());
    instanceInfo.setIp(instance.getIp());
    instanceInfo.setPort(instance.getPort());
    return instanceInfo;
}

源碼清單

服務註冊和發現所用到的源碼清單如下:

├── easy-rpc-example
├── easy-rpc-spring-boot-starter
│   ├── pom.xml
│   ├── src
│   │   └── main
│   │       ├── java
│   │       │   └── com
│   │       │       └── leixiaoshuai
│   │       │           └── easyrpc
│   │       │               ├── client
│   │       │               │   ├── ClientProxyFactory.java
│   │       │               │   ├── discovery
│   │       │               │   │   ├── NacosServiceDiscovery.java
│   │       │               │   │   ├── ServiceDiscovery.java
│   │       │               │   │   └── ZookeeperServiceDiscovery.java
│   │       │               ├── common
│   │       │               │   └── InstanceInfo.java
│   │       │               └── server
│   │       │                   └── registry
│   │       │                       ├── NacosServiceRegistry.java
│   │       │                       ├── ServiceRegistry.java
│   │       │                       └── ZookeeperServiceRegistry.java

完整的源碼可以自行去 Github 上取:

https://github.com/CoderLeixiaoshuai/easy-rpc

小結

本文以較少的代碼實現了 RPC 框架實現服務註冊發現功能,相信大家對這個流程已經全面掌握了。

客戶端與服務端通信的前提是需要知道對方的 ip 和端口,服務註冊就是將自己的元信息(ip、端口等)註冊到註冊中心(Registry),這樣客戶端就可以從註冊中心(Registry)獲取自己 "感興趣" 的服務實例了。

服務註冊和發現機制可以通過一些中間件來輔助實現,如比較流行的:Zookeeper 或者 Nacos 等。

愛笑的架構師 死磕技術,熱愛生活!

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/gmC1kUHm7DxVkNjNsxeO7Q