在编码前,需要跟大家说一下,整个项目是按照一个一个功能模块叠加实现的,由于文章排版不适合放大块代码,文章里我会截取最关键的代码给大家讲解,想要获取完整的代码,可以去 Github 上下载,已经正式开源了。

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

注意:源码可能会更新,记得拉取最新的。

需求分析:服务注册和发现

rpc 项目要实现的第一个功能模块就是:服务注册和发现,这个功能也是整个框架非常核心和关键的。

我们的 rpc 项目不用于生成环境,造个轮子嘛,只需要实现最基础的功能即可:

  • 服务实例注册自己的元数据到注册中心,元数据包括:实例 ip、端口、接口描述等;
  • 客户端实例想要调用服务端接口会先连接注册中心,发现待调用的服务端实例;
  • 拿到多个服务端实例后,客户端会根据负载均衡算法选择一个合适的实例进行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 等。