RPC Demo(二) 基于 Zookeeper 的服务发现

本文介绍了如何在RPC系统中使用Zookeeper作为服务发现组件。首先,解释了改进的必要性,即避免在客户端调用时手动输入服务器地址。接着详细阐述了编码思路,包括服务端如何将Provider注册到Zookeeper,以及客户端如何从Zookeeper获取并更新Provider信息,同时监听Provider的变化。此外,还展示了服务端和客户端的关键代码片段。

RPC Demo(二) 基于 Zookeeper 的服务发现


简介

    基于上篇的:RPC Demo(一) Netty RPC Demo 实现

    第二部分来实现使用Zookeeper作为服务注册中心,去掉在RPC调用中的显示传参

    完整项目工程地址:RpcDemoJava

改进说明

    在客户端调用中,我们需要显示的传入后端服务器的地址,这样显的有些不方便,代码大致如下:

UserService userService = jdk.create(UserService.class, "http://localhost:8080/");

    利用Zookeeper作为注册中心,客户端可以从Zookeeper中获取接口实现的服务器相关地址,就不必再显式传入地址了,改进后大致如下:

UserService userService = jdk.create(UserService.class);

编码思路

    进过调研和思考,实现的思路和步骤大致如下:

  • 1.服务端将Provider注册到Zookeeper中
  • 2.客户端拉取所有的Provider信息到本地,建立接口(Consumer)和Provider列表的映射关系
  • 3.客户端能监听服务端Provider的增删改查,同步到客户端,便于删除和更新变化后的Provider信息
  • 4.客户端反射调用时从Provider列表中获取相关url地址,进行访问,返回结果

    需要在本地启动一个zk,使用docker即可,相关命令如下:

# 拉取ZK镜像启动ZK,后面的三个命令是基于运行了这个命令后的
docker run -dit --name zk -p 2181:2181 zookeeper
# 查看ZK运行日志
docker logs -f zk
# 重启ZK
docker restart zk
# 启动ZK
docker start zk
# 停止ZK
docker stop zk

Provider信息结构约定

    我们约定一个Provider信息如下:

@Data
public class ProviderInfo {
   
   

    /**
     * Provider ID:ZK注册后会生成一个ID
     * Client 获取Provider列表时,将此ID设置为获取的ZK生成的ID
     */
    String id;

    /**
     * Provider对应的后端服务器地址
     */
    String url;

    /**
     * 标签:用于简单路由
     */
    List<String> tags;

    /**
     * 权重:用于加权负载均衡
     */
    Integer weight;

    public ProviderInfo() {
   
   }

    public ProviderInfo(String id, String url, List<String> tags, int weight) {
   
   
        this.id = id;
        this.url = url;
        this.tags = tags;
        this.weight = weight;
    }
}

1.服务端将Provider注册到Zookeeper中

    首先,我们要为各个接口的实现指定Provider名称、分组、版本、标签、权重,这里我们使用注解进行实现

/**
 * RPC provider service 初始化注解
 *
 * group,version,targs 都有默认值,是为了兼容以前的版本
 *
 * @author lw1243925457
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ProviderService {
   
   

    /**
     * 对应 API 接口名称
     * @return API service
     */
    String service();

    /**
     * 分组
     * @return group
     */
    String group() default "default";

    /**
     * version
     * @return version
     */
    String version() default "default";

    /**
     * tags:用于简单路由
     * 多个标签使用逗号分隔
     * @return tags
     */
    String tags() default "";

    /**
     * 权重:用于加权负载均衡
     * @return
     */
    int weight() default 1;
}

    接下来,借鉴Mybatis的设置包扫描路径的思路,写一个通过扫描指定包路径下的所有的class,获取class后判断其是否是Provider(有相应的注解),如果是,提取信息,注册到ZK
中,大致的代码如下:

/**
 * 提供RPC Provider 的初始化
 * 初始化实例放入 Map 中,方便后续的获取
 *
 * @author lw1243925457
 */
@Slf4j
public class ProviderServiceManagement {
   
   

    /**
     * 通过服务名、分组、版本作为key,确实接口实现类的实例
     * service:group:version --> class
     */
    private static Map<String, Object> proxyMap = new HashMap<>();

    /**
     * 初始化:通过扫描包路径,获取所有实现类,将其注册到ZK中
     * 获取实现类上的Provider注解,获取服务名、分组、版本
     * 调用ZK服务注册,将Provider注册到ZK中
     * @param packageName 接口实现类的包路径
     * @param port 服务监听的端口
     * @throws Exception exception
     */
    public static void init(String packageName, int port) throws Exception {
   
   
        System.out.println("\n-------- Loader Rpc Provider class start ----------------------\n");

        DiscoveryServer serviceRegister = new DiscoveryServer();

        Class[] classes = getClasses(packageName);
        for (Class c: classes) {
   
   
            ProviderService annotation = (ProviderService) c.getAnnotation(ProviderService.class);
            if (annotation == null) {
   
   
                continue;
            }
            String group = annotation.group();
            String version = annotation.version();
            List<String> tags = Arrays.asList(annotation.tags().split(","));
            String provider = Joiner.on(":").join(annotation.service(), group, version);
            int weight = annotation.weight();

            proxyMap.put(provider, c.newInstance());

            serviceRegister.registerService(annotation.service(), group, version, port, tags, weight);

            log.info("load provider class: " + annotation.service() + ":" + group + ":" + version + " :: " + c.getName());
        }
        System.out.println("\n-------- Loader Rpc Provider class end ----------------------\n");
    }

    /**
     * Scans all classes accessible from the context class loader which belong to the given package and subpackages.
     *
     * @param packageName The base package
     * @return The classes
     * @throws ClassNotFoundException exception
     * @throws IOException exception
     */
    private static Class[] getClasses(String packageName) throws ClassNotFoundException, IOException {
   
   
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        assert classLoader != null;
        String path = packageName.replace('.', '/');
        Enumeration<URL> resources = classLoader.getResources(path);
        List<File> dirs = new ArrayList<>();
        while (resources.hasMoreElements()) {
   
   
            URL resource = resources.nextElement();
            dirs.add(new File(resource.getFile()));
        }
        ArrayList<Class> classes = new ArrayList<>();
        for (File directory : dirs) {
   
   
            classes.addAll(findClasses(directory
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值