幾行代碼搞定 RPC 服務註冊和發現
在編碼前,需要跟大家說一下,整個項目是按照一個一個功能模塊疊加實現的,由於文章排版不適合放大塊代碼,文章裏我會截取最關鍵的代碼給大家講解,想要獲取完整的代碼,可以去 Github 上下載,已經正式開源了。
easy-rpc 開源地址:
https://github.com/CoderLeixiaoshuai/easy-rpc注意:源碼可能會更新,記得拉取最新的。
需求分析:服務註冊和發現
rpc 項目要實現的第一個功能模塊就是:服務註冊和發現
,這個功能也是整個框架非常核心和關鍵的。
關於服務註冊發現的介紹和原理,可以看這篇文章:《10 張圖搞懂服務註冊發現機制》
我們的 rpc 項目不用於生成環境,造個輪子嘛,只需要實現最基礎的功能即可:
-
服務實例註冊自己的元數據到註冊中心,元數據包括:實例 ip、端口、接口描述等;
-
客戶端實例想要調用服務端接口會先連接註冊中心,發現待調用的服務端實例;
-
拿到多個服務端實例後,客戶端會根據負載均衡算法選擇一個合適的實例進行 RPC 調用。
需求很明確了,下面開始寫代碼。
引入三方依賴
市面上靠譜的註冊中心還是很多的,這次打算同時兼容兩種註冊中心:Zookeeper 和 Nacos,是不是很良心?!在使用前需要先引入以下依賴。
與 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