接上一篇《​​10.手写rpc框架-代码实现(三)​​》
上一篇我们编写了rpc-server客户端工程,其作用是处理客户端发送过来的RPC请求,使用Handler处理客户端请求,获取相关的服务类调用的结果,使用Netty反馈回客户端。
前面我们的客户端rpc-client会通过注册中心rpc-registry-zookeeper获取服务端注册到其中的服务信息,而服务端rpc-server会将相关的服务信息注册到注册中心,以便于客户端发现并获取信息调用。
下面我们就来编写注册中心rpc-registry-zookeeper,来实现服务的注册和发现。

(作者:黄勇)

在MyEclipse新建名为rpc-registry的maven工程,该工程是注册中心的父级工程,无论使用何种注册中心,均以该工程的Server接口规范来开发:

【RPC高性能框架总结】11.手写rpc框架-代码实现(四)_zkClient

【RPC高性能框架总结】11.手写rpc框架-代码实现(四)_getChildren_02

【RPC高性能框架总结】11.手写rpc框架-代码实现(四)_zkClient_03


新建成功后,由于该工程只是接口规范,不需要任何的实现,所以POM文件中无需引入其它依赖:

<project xmlns="http://maven.apache.org/POM/4.0.0" 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>
<artifactId>rpc-registry</artifactId>

<parent>
<groupId>com.xxx.rpc</groupId>
<artifactId>rpc-framework</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>

<build/>
</project>

然后我们开始编写代码,首先在src/main/java中创建com.xxx.rpc.registry包下的ServiceRegistry、ServiceDiscovery,这两个类分别是服务注册和服务发现的接口定义:

package com.xxx.rpc.registry;

//服务注册接口
public interface ServiceRegistry {

/**
* 注册服务名称与服务地址
*
* @param serviceName 服务名称
* @param serviceAddress 服务地址
* */
void register(String serviceName,String serviceAddress);
}
package com.xxx.rpc.registry;

//服务发现接口
public interface ServiceDiscovery {

/**
* 根据服务名称查找服务地址
*
* @param serviceName 服务名称
* @return 服务地址
* */
String discover(String serviceName);
}

编写完父级工程,下面就要来编写注册中心的实现工程了;

在MyEclipse新建名为rpc-registry-zookeeper的maven工程,该工程就是以zookeeper作为注册中心,来实现服务的注册与发现功能:

【RPC高性能框架总结】11.手写rpc框架-代码实现(四)_zkClient

【RPC高性能框架总结】11.手写rpc框架-代码实现(四)_getChildren_02

【RPC高性能框架总结】11.手写rpc框架-代码实现(四)_ZooKeeper_06


新建成功之后,在POM中引入依赖,除了需要rpc-common工程的公共依赖,rpc-registry父级工程的接口定义,还需要log4j日志输出,zookeeper基本依赖,zookeeper客户端依赖zkClient,所以我们引入以下依赖:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>com.xxx.rpc</groupId>
<artifactId>rpc-framework</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>

<artifactId>rpc-registry-zookeeper</artifactId>

<dependencies>
<!-- SLF4J -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</dependency>
<!-- ZooKeeper -->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</dependency>
<!-- ZkClient -->
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
</dependency>
<!-- RPC Common -->
<dependency>
<groupId>com.xxx.rpc</groupId>
<artifactId>rpc-common</artifactId>
<version>${project.version}</version>
</dependency>
<!-- RPC Registry -->
<dependency>
<groupId>com.xxx.rpc</groupId>
<artifactId>rpc-registry</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>

</project>

然后我们开始编写代码,首先在src/main/java中创建com.xxx.rpc.registry.zookeeper包下的Constant、ZooKeeperServiceRegistry和ZooKeeperServiceDiscovery类,其中Constant用来定义Zookeeper相关的常量,ZooKeeperServiceRegistry用来实现RPC服务的注册,ZooKeeperServiceDiscovery用来实现RPC服务调用地址的发现。首先是Constant接口,定义了一些Zookeeper连接的常量信息:

package com.xxx.rpc.registry.zookeeper;

//常量
public interface Constant {

int ZK_SESSION_TIMEOUT = 5000;//会话超时时间
int ZK_CONNECTION_TIMEOUT = 1000;//连接超时时间

String ZK_REGISTRY_PATH = "/registry";//注册地址
}

在该接口体中,定义了三个常量,一个是zookeeper的会话超时时间,一个是连接超时时间,最后一个是zookeeper的注册目录地址。接着是ZooKeeperServiceRegistry,我们实现父级工程中的ServiceRegistry接口,实现其register方法,来编写注册服务的逻辑:

package com.xxx.rpc.registry.zookeeper;

import org.I0Itec.zkclient.ZkClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.xxx.rpc.registry.ServiceRegistry;

//基于Zookeeper的服务注册接口实现
public class ZooKeeperServiceRegistry implements ServiceRegistry{

//日志对象
private static final Logger LOGGER = LoggerFactory.getLogger(ZooKeeperServiceRegistry.class);

private final ZkClient zkClient;//zookeeper连接客户端

//构造方法,通过传入的zookeeper服务地址,创建zookeeper客户端
public ZooKeeperServiceRegistry(String zkAddress){
//创建Zookeeper客户端
zkClient = new ZkClient(zkAddress,Constant.ZK_SESSION_TIMEOUT,Constant.ZK_CONNECTION_TIMEOUT);
LOGGER.debug("connect zookeeper");
}

@Override
public void register(String serviceName, String serviceAddress) {
//创建registry节点(持久)
String registryPath = Constant.ZK_REGISTRY_PATH;
if(!zkClient.exists(registryPath)){
//判断节点是否存在,节点不存在时,创建相关节点
zkClient.createPersistent(registryPath);//创建持久化节点
LOGGER.debug("create registry node:{}",registryPath);
}
//创建service节点(持久)
String servicePath = registryPath + "/" + serviceName;
if(!zkClient.exists(servicePath)){
zkClient.createPersistent(servicePath);//创建持久化节点
LOGGER.debug("create service node:{}",servicePath);
}
//创建address节点(临时)
String addressPath = servicePath + "/address-";
String addressNode = zkClient.createEphemeralSequential(addressPath, serviceAddress);
LOGGER.debug("create address node:{}",addressNode);
}

}

ZooKeeperServiceRegistry类的作用主要是注册RPC服务地址,在该类中,引入了zkClient对象进行zookeeper的操作。在ZooKeeperServiceRegistry类的构造方法中,引入了zookeeper服务地址,用来创建可以连接zookeeper的zkClient对象。然后实现ServiceRegistry接口的register方法,将传进来的服务名称serviceName以及服务地址serviceAddress进行处理,首先是创建一个永久的registry节点,以后所有注册进来的服务地址,都位于该节点下;然后创建service节点,所有有关该service的服务信息都位于该节点下;最后是服务的地址节点,是一个临时节点,该服务的具体调用地址,位于该节点下。接着是ZooKeeperServiceDiscovery,我们实现父级工程中的ServiceDiscovery接口,实现其discovery方法,来编写发现服务的逻辑:

package com.xxx.rpc.registry.zookeeper;

import java.util.List;

import org.I0Itec.zkclient.ZkClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.xxx.rpc.common.utils.CollectionUtil;
import com.xxx.rpc.registry.ServiceDiscovery;

import io.netty.util.internal.ThreadLocalRandom;

//基于zookeeper的服务发现接口实现
public class ZooKeeperServiceDiscovery implements ServiceDiscovery{

//日志对象
private static final Logger LOGGER = LoggerFactory.getLogger(ZooKeeperServiceDiscovery.class);

private String zkAddress;//zookeeper地址

public ZooKeeperServiceDiscovery(String zkAddress){
this.zkAddress = zkAddress;
}

@Override
public String discover(String serviceName) {
//创建zookeeper客户端
ZkClient zkClient = new ZkClient(zkAddress,Constant.ZK_SESSION_TIMEOUT,Constant.ZK_CONNECTION_TIMEOUT);
LOGGER.debug("connect zookeeper");

try {
//获取service节点
String servicePath = Constant.ZK_REGISTRY_PATH + "/" + serviceName;
if(!zkClient.exists(servicePath)){
//如果service不存在,则报错
throw new RuntimeException(String.format("can not find any service node on path: %s", servicePath));
}
//获取service节点下的所有子节点
List<String> addressList = zkClient.getChildren(servicePath);
if (CollectionUtil.isEmpty(addressList)) {
//如果service节点下的所有子节点为空,则报错
throw new RuntimeException(String.format("can not find any address node on path: %s", servicePath));
}

//获取address节点
String address;
int size = addressList.size();
if(size==1){
//如果只有一个地址,则获取改地址
address = addressList.get(0);
LOGGER.debug("get only address node: {}", address);
}else{
//如果存在多个地址,则随机获取改地址
address = addressList.get(ThreadLocalRandom.current().nextInt(size));
LOGGER.debug("get random address node: {}", address);
}
//获取address节点的值
String addressPath = servicePath + "/" + address;
return zkClient.readData(addressPath);
} finally{
zkClient.close();//关闭zookeeper连接
}
}

}

ZooKeeperServiceDiscovery类的作用主要是发现RPC服务地址,在该类中,引入了zkAddress地址用于创建zookeeper连接客户端zkClient的操作。然后实现ServiceDiscovery接口的discovery方法,获取传进来的服务名称serviceName,然后创建zkClient对象,进行服务发现操作。首先获取service节点,然后获取相关service节点下的所有子节点,这些子节点保存了各个服务端(单体或集群)暴露出来的服务地址,如果子节点只有一个,证明服务端是单体服务,只需返回这一个节点即可;如果子节点有很多,则证明服务端是集群服务,此时的选择策略是随机选择一个节点,然后使用客户端对象,将该节点的服务地址信息(serviceAddress)读取出来即可。

至此,我们将rpc-registry-zookeeper注册中心端编写完成。下一篇我们来编写业务端的客户端与服务端,对接之前的rpc-client以及rpc-server,来实现具体的业务逻辑服务的远程调用。