幾行代碼搞定RPC服務(wù)注冊(cè)和發(fā)現(xiàn)
前兩篇文章我們把環(huán)境和項(xiàng)目骨干搭建好了之后,從這篇文章開始就進(jìn)入項(xiàng)目編碼階段了。
在編碼前,需要跟大家說一下,整個(gè)項(xiàng)目是按照一個(gè)一個(gè)功能模塊疊加實(shí)現(xiàn)的,由于文章排版不適合放大塊代碼,文章里我會(huì)截取最關(guān)鍵的代碼給大家講解,想要獲取完整的代碼,可以去 Github 上下載,已經(jīng)正式開源了。
easy-rpc 開源地址:
https://github.com/CoderLeixiaoshuai/easy-rpc注意:源碼可能會(huì)更新,記得拉取最新的。
需求分析:服務(wù)注冊(cè)和發(fā)現(xiàn)
rpc 項(xiàng)目要實(shí)現(xiàn)的第一個(gè)功能模塊就是:服務(wù)注冊(cè)和發(fā)現(xiàn),這個(gè)功能也是整個(gè)框架非常核心和關(guān)鍵的。
關(guān)于服務(wù)注冊(cè)發(fā)現(xiàn)的介紹和原理,可以看這篇文章:《10 張圖搞懂服務(wù)注冊(cè)發(fā)現(xiàn)機(jī)制》
我們的 rpc 項(xiàng)目不用于生成環(huán)境,造個(gè)輪子嘛,只需要實(shí)現(xiàn)最基礎(chǔ)的功能即可:
服務(wù)實(shí)例注冊(cè)自己的元數(shù)據(jù)到注冊(cè)中心,元數(shù)據(jù)包括:實(shí)例 ip、端口、接口描述等; 客戶端實(shí)例想要調(diào)用服務(wù)端接口會(huì)先連接注冊(cè)中心,發(fā)現(xiàn)待調(diào)用的服務(wù)端實(shí)例; 拿到多個(gè)服務(wù)端實(shí)例后,客戶端會(huì)根據(jù)負(fù)載均衡算法選擇一個(gè)合適的實(shí)例進(jìn)行RPC調(diào)用。
需求很明確了,下面開始寫代碼。
引入三方依賴
市面上靠譜的注冊(cè)中心還是很多的,這次打算同時(shí)兼容兩種注冊(cè)中心:Zookeeper 和 Nacos,是不是很良心?!在使用前需要先引入以下依賴。
與 Zookeeper 交互可以引入對(duì)應(yīng)的 SDK,zkclient 是個(gè)不錯(cuò)的選擇;JSON 序列化和反序列化可以引入 fastjson,雖然經(jīng)常爆漏洞,但是國(guó)產(chǎn)還是得支持下:
<!-- 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>
服務(wù)端實(shí)現(xiàn)服務(wù)注冊(cè)
服務(wù)注冊(cè)和發(fā)現(xiàn)分為兩塊功能:服務(wù)端注冊(cè)和客戶端發(fā)現(xiàn),我們先來實(shí)現(xiàn)服務(wù)端注冊(cè)功能。
定義服務(wù)注冊(cè)接口
在日常的工作或者學(xué)習(xí)編碼過程中,我們一定要習(xí)慣面向接口編程,這樣做有助于增強(qiáng)代碼可擴(kuò)展性。
根據(jù)前面的需求描述,服務(wù)注冊(cè)只需要干一件事情:服務(wù)注冊(cè),我們可以定義一個(gè)接口:ServiceRegistry,接口中定義一個(gè)方法:register,代碼如下:
public interface ServiceRegistry {
/**
* 注冊(cè)服務(wù)信息
*
* @param serviceInfo 待注冊(cè)的服務(wù)
* @throws Exception 異常
*/
void register(ServiceInfo serviceInfo) throws Exception;
}
服務(wù)向注冊(cè)中心注冊(cè),注冊(cè)的內(nèi)容定義一個(gè)類ServiceInfo來封裝。
/**
* 服務(wù)名稱
*/
private String serviceName;
/**
* ip 地址
*/
private String ip;
/**
* 端口號(hào)
*/
private Integer port;
/**
* class 對(duì)象
*
*/
private Class<?> clazz;
/**
* bean 對(duì)象
*/
private Object obj;
// 省略 get set 方法……
}
Zookeeper 實(shí)現(xiàn)服務(wù)注冊(cè)
我們嘗試用 Zookeeper 來實(shí)現(xiàn)服務(wù)注冊(cè)功能,先新建一個(gè)類實(shí)現(xiàn)前面定義好的服務(wù)注冊(cè)接口:
public class ZookeeperServiceRegistry implements ServiceRegistry {
}
接下來重寫register方法,主要功能包括調(diào)用 Zookeeper 接口創(chuàng)建服務(wù)節(jié)點(diǎn)和實(shí)例節(jié)點(diǎn)。其中服務(wù)節(jié)點(diǎn)是一個(gè)永久節(jié)點(diǎn),只用創(chuàng)建一次;實(shí)例節(jié)點(diǎn)是臨時(shí)節(jié)點(diǎn),如果實(shí)例故障下線,實(shí)例節(jié)點(diǎn)會(huì)自動(dòng)刪除。
// ZookeeperServiceRegistry.java
@Override
public void register(ServiceInfo serviceInfo) throws Exception {
logger.info("Registering service: {}", serviceInfo);
// 創(chuàng)建 ZK 永久節(jié)點(diǎn)(服務(wù)節(jié)點(diǎn))
String servicePath = "/com/leixiaoshuai/easyrpc/" + serviceInfo.getServiceName() + "/service";
if (!zkClient.exists(servicePath)) {
zkClient.createPersistent(servicePath, true);
}
// 創(chuàng)建 ZK 臨時(shí)節(jié)點(diǎn)(實(shí)例節(jié)點(diǎn))
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);
}
代碼非常簡(jiǎn)單,大家看注釋就能懂了。
Nacos 實(shí)現(xiàn)服務(wù)注冊(cè)
除了使用 Zookeeper 來實(shí)現(xiàn),我們還可以使用 Nacos,跟上面一樣我們還是先建一個(gè)類:
public class NacosServiceRegistry implements ServiceRegistry {
}
接著編寫構(gòu)造方法,NacosServiceRegistry 類被實(shí)例化之后 Nacos 客戶端也要連接上 Nacos 服務(wù)端。
// NacosServiceRegistry.java
public NacosServiceRegistry(String serverList) throws NacosException {
// 使用工廠類創(chuàng)建注冊(cè)中心對(duì)象,構(gòu)造參數(shù)為 Nacos Server 的 ip 地址,連接 Nacos 服務(wù)器
naming = NamingFactory.createNamingService(serverList);
// 打印 Nacos Server 的運(yùn)行狀態(tài)
logger.info("Nacos server status: {}", naming.getServerStatus());
}
獲得NamingService類的實(shí)例對(duì)象后,就可以調(diào)用實(shí)例注冊(cè)接口完成服務(wù)注冊(cè)了。
// NacosServiceRegistry.java
@Override
public void register(ServiceInfo serviceInfo) throws Exception {
// 注冊(cè)當(dāng)前服務(wù)實(shí)例
naming.registerInstance(serviceInfo.getServiceName(), buildInstance(serviceInfo));
}
private Instance buildInstance(ServiceInfo serviceInfo) {
// 將實(shí)例信息注冊(cè)到 Nacos 中心
Instance instance = new Instance();
instance.setIp(serviceInfo.getIp());
instance.setPort(serviceInfo.getPort());
// TODO add more metadata
return instance;
}
注意:NamingService 類提供了很多有用的方法,大家可自行進(jìn)行嘗試。
客戶端實(shí)現(xiàn)服務(wù)發(fā)現(xiàn)
定義服務(wù)發(fā)現(xiàn)接口
前面已經(jīng)將服務(wù)實(shí)例注冊(cè)到Zookeeper 或者 Nacos 服務(wù)端,現(xiàn)在客戶端想要調(diào)用服務(wù)端首先得獲得服務(wù)端實(shí)例列表,這個(gè)過程其實(shí)就是服務(wù)發(fā)現(xiàn)。
我們先定義一個(gè)抽象的接口,這個(gè)接口主要的功能就是定義一個(gè)獲取服務(wù)實(shí)例的接口:
public interface ServiceDiscovery {
/**
* 通過服務(wù)名稱隨機(jī)選擇一個(gè)健康的實(shí)例
* @param serviceName 服務(wù)名稱
* @return 實(shí)例對(duì)象
*/
InstanceInfo selectOneInstance(String serviceName);
}
隨機(jī)挑選一個(gè)實(shí)例是為了模擬負(fù)載均衡,盡量使請(qǐng)求均勻分配到各實(shí)例上。
Zookeeper 實(shí)現(xiàn)服務(wù)發(fā)現(xiàn)
前面使用 Zookeeper 實(shí)現(xiàn)了服務(wù)注冊(cè)功能,這里我們?cè)儆?Zookeeper 來實(shí)現(xiàn)服務(wù)發(fā)現(xiàn)功能,先定義一個(gè)類實(shí)現(xiàn) ServiceDiscovery 接口:
public class ZookeeperServiceDiscovery implements ServiceDiscovery {
}
下面實(shí)現(xiàn)核心方法:selectOneInstance
Zookeeper 內(nèi)部是一個(gè)樹形的節(jié)點(diǎn),通過查找一個(gè)指定節(jié)點(diǎn)的所有子節(jié)點(diǎn)即可獲得服務(wù)實(shí)例列表。拿到服務(wù)實(shí)例列表后如何挑選出一個(gè)實(shí)例呢?這里就可以引入負(fù)載均衡算法了。
// 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 {
// 將服務(wù)信息經(jīng)過 URL 解碼后反序列化為對(duì)象
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();
}
注意:當(dāng)前項(xiàng)目?jī)H僅用于學(xué)習(xí)用,這里沒有引入復(fù)雜的負(fù)載均衡算法,有興趣的同學(xué)可自行補(bǔ)充,歡迎提交 MR 貢獻(xiàn)代碼。
Nacos 實(shí)現(xiàn)服務(wù)發(fā)現(xiàn)
最后來到 Nacos 的實(shí)現(xiàn),話不多說先定義一個(gè)類:
public class NacosServiceDiscovery implements ServiceDiscovery {
}
同樣也需要實(shí)現(xiàn)核心方法:selectOneInstance。Nacos 的實(shí)現(xiàn)就比較簡(jiǎn)單了,因?yàn)?Nacos 官方提供的 SDK 功能太強(qiáng)大了,我們直接調(diào)用對(duì)應(yīng)的接口就可以了,Nacos 根據(jù)算法會(huì)隨機(jī)挑選一個(gè)健康的實(shí)例,我們不用關(guān)注細(xì)節(jié)。
// ZookeeperServiceDiscovery.java
@Override
public InstanceInfo selectOneInstance(String serviceName) {
Instance instance;
try {
// 調(diào)用 nacos 提供的接口,隨機(jī)挑選一個(gè)服務(wù)實(shí)例,負(fù)載均衡的算法依賴 nacos 的實(shí)現(xiàn)
instance = namingService.selectOneHealthyInstance(serviceName);
} catch (NacosException e) {
logger.error("Nacos exception", e);
return null;
}
// 封裝實(shí)例對(duì)象返回
InstanceInfo instanceInfo = new InstanceInfo();
instanceInfo.setServiceName(instance.getServiceName());
instanceInfo.setIp(instance.getIp());
instanceInfo.setPort(instance.getPort());
return instanceInfo;
}
源碼清單
服務(wù)注冊(cè)和發(fā)現(xiàn)所用到的源碼清單如下:
├── 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
小結(jié)
本文以較少的代碼實(shí)現(xiàn)了 RPC 框架實(shí)現(xiàn)服務(wù)注冊(cè)發(fā)現(xiàn)功能,相信大家對(duì)這個(gè)流程已經(jīng)全面掌握了。
客戶端與服務(wù)端通信的前提是需要知道對(duì)方的 ip 和端口,服務(wù)注冊(cè)就是將自己的元信息(ip、端口等)注冊(cè)到注冊(cè)中心(Registry),這樣客戶端就可以從注冊(cè)中心(Registry)獲取自己"感興趣"的服務(wù)實(shí)例了。
服務(wù)注冊(cè)和發(fā)現(xiàn)機(jī)制可以通過一些中間件來輔助實(shí)現(xiàn),如比較流行的:Zookeeper或者 Nacos 等。
-- END --
推薦學(xué)習(xí):
好了,今天的技術(shù)文就到這里了。我是雷小帥,一個(gè)死磕技術(shù)的理工男,如果本文對(duì)你有幫助,麻煩點(diǎn)贊、分享、在看支持一下,非常感謝~
你的支持就是我前進(jìn)的動(dòng)力!
下期見!
