在编码前,需要跟大家说一下,整个项目是按照一个一个功能模块叠加实现的,由于文章排版不适合放大块代码,文章里我会截取最关键的代码给大家讲解,想要获取完整的代码,可以去 Github 上下载,已经正式开源了。
easy-rpc 开源地址:
https://github.com/CoderLeixiaoshuai/easy-rpc注意:源码可能会更新,记得拉取最新的。
需求分析:服务注册和发现
rpc 项目要实现的第一个功能模块就是:服务注册和发现
,这个功能也是整个框架非常核心和关键的。
我们的 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 等。