RPC与Zookeeper注册中心的简单实现

时间:2023-03-09 17:34:42
RPC与Zookeeper注册中心的简单实现

  接着之前的RPC实现:https://www.cnblogs.com/wuzhenzhao/p/9962250.html RPC框架的简单实现,基于这个小程序,在我学习完Zookeeper之后如何将注册中心与RPC调用结合起来。直接进入正题

  我这边用到的 curator 客户端工具的依赖是:版本太高不兼容的话会报异常

<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>2.5.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>2.5.0</version>
</dependency>

  承接上文,我们之前在服务发布的时候是直接写死一个地址发布的,现在需要结合注册中心 ,那么我们势必要在发布服务的时候在指定的 zookeeper服务上面注册我们的节点,基于 zookeeper 临时节点的特性,一旦服务 down机,那么这个节点也会消失,同时会有事件触发,添加一个注册服务:

服务端:

public interface IRegisterCenter {

    /**
* 注册服务名称和服务地址
* @param serviceName
* @param serviceAddress
*/
void register(String serviceName,String serviceAddress);
}

  实现:注册服务的实现就是往zookeeper 的指点根节点下插入一个临时节点,如果根节点不存在则先创建,由于 curator 做了很好的实现,这里先用他来做实现。

public class RegisterCenterImpl implements IRegisterCenter {

    //zk链接地址
public final static String CONNNECTION_STR = "192.168.1.101:2181"; //注册根节点
public final static String ZK_REGISTER_PATH = "/registrys"; private CuratorFramework curatorFramework; {// 这段代码无非是连接服务器,自己看着写在哪里把
curatorFramework = CuratorFrameworkFactory.builder().
connectString(CONNNECTION_STR). //
sessionTimeoutMs().
retryPolicy(new ExponentialBackoffRetry(,
)).build();
curatorFramework.start();
} @Override
public void register(String serviceName, String serviceAddress) {
//注册相应的服务
String servicePath = ZK_REGISTER_PATH + "/" + serviceName; try {
//判断 /registrys/product-service是否存在,不存在则创建
if (curatorFramework.checkExists().forPath(servicePath) == null) {
curatorFramework.create().creatingParentsIfNeeded().
withMode(CreateMode.PERSISTENT).forPath(servicePath, "".getBytes());
}
// 组装节点地址
String addressPath = servicePath + "/" + serviceAddress;
String rsNode = curatorFramework.create().withMode(CreateMode.EPHEMERAL).
forPath(addressPath, "".getBytes());
System.out.println("服务注册成功:" + rsNode); } catch (Exception e) {
e.printStackTrace();
}
}
}

  有了注册服务我们需要进入服务发布的类里面进行一些修改,由于服务的地址及端口都注册到注册中心上了,我们需要增加注册中心属性,便已获取相关信息。

public class RpcServer {
//创建一个线程池
private static final ExecutorService executorService= Executors.newCachedThreadPool(); private IRegisterCenter registerCenter; //注册中心
private String serviceAddress; //服务发布地址 // 存放服务名称和服务对象之间的关系
Map<String,Object> handlerMap=new HashMap<>(); public RpcServer(IRegisterCenter registerCenter, String serviceAddress) {
this.registerCenter = registerCenter;
this.serviceAddress = serviceAddress;
} /**
* 绑定服务名称和服务对象
* @param services
*/
public void bind(Object... services){
for(Object service:services){// 这里为了获取对应服务的类名,我们这里定义了一个注解来实现 代码请看下面
RpcAnnotation annotation=service.getClass().getAnnotation(RpcAnnotation.class);
String serviceName=annotation.value().getName();
handlerMap.put(serviceName,service);//绑定服务接口名称对应的服务
}
} public void publisher(){
ServerSocket serverSocket=null;
try{
String[] addrs=serviceAddress.split(":");//这个时候服务的ip port 都是从这个注册地址上获取
serverSocket=new ServerSocket(Integer.parseInt(addrs[])); //启动一个服务监听
// handlerMap 可能存放多个发布服务,我这里演示的是一个
for(String interfaceName:handlerMap.keySet()){
registerCenter.register(interfaceName,serviceAddress);
System.out.println("注册服务成功:"+interfaceName+"->"+serviceAddress);
} while(true){ //循环监听
Socket socket=serverSocket.accept(); //监听服务
//通过线程池去处理请求
executorService.execute(new ProcessorHandler(socket,handlerMap));
}
} catch (IOException e) {
e.printStackTrace();
}finally {
if(serverSocket!=null){
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
} }
}
}

  我们可以采用注解的方式把这个服务注册上去,注解类:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface RpcAnnotation {
/**
* 对外发布的服务的接口地址
* @return
*/
Class<?> value();
// 暂时没用 可以处理版本
String version() default "";
}

  然后再对应需要发布的服务类实现上加上注解:

public interface HelloService {
String sayHello(String msg);
}
@RpcAnnotation(HelloService.class)
public class HelloServiceImpl implements HelloService {
@Override
public String sayHello(String msg) {
return " RPC Hello, " + msg;
}
}

  上面监听服务请求的处理也发生了改变,之前传入的是单独的service 现在需要把绑定的 handlerMap 带过去。 接下来看 ProcessorHandler :

public class ProcessorHandler implements Runnable {
private Socket socket; Map<String, Object> handlerMap;// 现在需要从map里获取需要发布的绑定服务 public ProcessorHandler(Socket socket, Map<String, Object> handlerMap) {
this.socket = socket;
this.handlerMap = handlerMap;
} @Override
public void run() {
//处理请求
ObjectInputStream inputStream = null;
try {
//获取客户端的输入流
inputStream = new ObjectInputStream(socket.getInputStream());
//反序列化远程传输的对象RpcRequest
RpcRequest request = (RpcRequest) inputStream.readObject();
Object result = invoke(request); //通过反射去调用本地的方法 //通过输出流讲结果输出给客户端
ObjectOutputStream outputStream = new ObjectOutputStream(socket.getOutputStream());
outputStream.writeObject(result);
outputStream.flush();
inputStream.close();
outputStream.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
} private Object invoke(RpcRequest request) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
//以下均为反射操作,目的是通过反射调用服务
Object[] args = request.getParameters();
Class<?>[] types = new Class[args.length];
for (int i = ; i < args.length; i++) {
types[i] = args[i].getClass();
}
//从handlerMap中,根据客户端请求的地址,去拿到响应的服务,通过反射发起调用
Object service = handlerMap.get(request.getClassName());
Method method = service.getClass().getMethod(request.getMethodName(), types);
return method.invoke(service, args);
}
}

  然后发布服务:

public static void main(String[] args) throws IOException {
HelloService helloService=new HelloServiceImpl();
IRegisterCenter registerCenter=new RegisterCenterImpl(); RpcServer rpcServer=new RpcServer(registerCenter,"127.0.0.1:8080");
rpcServer.bind(helloService);
rpcServer.publisher();
System.in.read();
}

  输出:

服务注册成功:/registrys/com.wuzz.demo.rpc.server.HelloService/127.0.0.1:
注册服务成功:com.wuzz.demo.rpc.server.HelloService->127.0.0.1:

  对应的zk服务器上也创建了对应的节点:

RPC与Zookeeper注册中心的简单实现

客户端:

  对于客户端需要从远程调用服务,现在是需要从注册中心先去获取该服务类锁对应的再注册中心注册的服务地址及端口,再去发起请求,并且对指定到 路径进行监听,那么客户端需要定义一个服务的发现服务:

public interface IServiceDiscovery {
/**
* 根据请求的服务地址,获得对应的调用地址
* @param serviceName
* @return
*/
String discover(String serviceName);
}

  实现:

public class ServiceDiscoveryImpl implements IServiceDiscovery {
// 从指定节点下获取的子节点列表
List<String> repos = new ArrayList<>();
// 服务器连接地址,就是服务端的 ZkConfig.CONNNECTION_STR
private String address; private CuratorFramework curatorFramework; //注册根节点
public final static String ZK_REGISTER_PATH = "/registrys"; // 为了方便测试,这里直接再构造里面启动连接
public ServiceDiscoveryImpl(String address) {
this.address = address; curatorFramework = CuratorFrameworkFactory.builder().
connectString(address).
sessionTimeoutMs().
retryPolicy(new ExponentialBackoffRetry(,
)).build();
curatorFramework.start();
} @Override // 本质就是获取指定服务节点下的子节点
public String discover(String serviceName) {
String path = ZK_REGISTER_PATH + "/" + serviceName;
try {
repos = curatorFramework.getChildren().forPath(path); } catch (Exception e) {
throw new RuntimeException("获取子节点异常:" + e);
}
//动态发现服务节点的变化
registerWatcher(path); //简单负载均衡机制
if (repos == null || repos.size() == ) {
return null;
}
if (repos.size() == ) {
return repos.get();
} int len = repos.size();
Random random = new Random();
return repos.get(random.nextInt(len));//返回调用的服务地址
} // 这里是之前提到的用 curator 客户端进行时间注册的操作
private void registerWatcher(final String path) {
PathChildrenCache childrenCache = new PathChildrenCache
(curatorFramework, path, true); PathChildrenCacheListener pathChildrenCacheListener = new PathChildrenCacheListener() {
@Override
public void childEvent(CuratorFramework curatorFramework, PathChildrenCacheEvent pathChildrenCacheEvent) throws Exception {
repos = curatorFramework.getChildren().forPath(path);
}
};
childrenCache.getListenable().addListener(pathChildrenCacheListener);
try {
childrenCache.start();
} catch (Exception e) {
throw new RuntimeException("注册PatchChild Watcher 异常" + e);
}
}
}

  接下去由于客户端是通过动态代理去获取远程对象,由于原来参数为 IP Port ,现在需要通过注册中心去拿:

public class RpcClientProxy {

    // 服务发现
private IServiceDiscovery serviceDiscovery; public RpcClientProxy(IServiceDiscovery serviceDiscovery) {
this.serviceDiscovery = serviceDiscovery;
} /**
* 创建客户端的远程代理。通过远程代理进行访问
* @param interfaceCls
* @param <T>
* @return
*/
public <T> T clientProxy(final Class<T> interfaceCls){
//使用到了动态代理。
return (T) Proxy.newProxyInstance(interfaceCls.getClassLoader(),
new Class[]{interfaceCls},new RemoteInvocationHandler(serviceDiscovery));
}
}

  对应的RemoteInvocationHandler:

public class RemoteInvocationHandler implements InvocationHandler {
private IServiceDiscovery serviceDiscovery; public RemoteInvocationHandler(IServiceDiscovery serviceDiscovery) {
this.serviceDiscovery = serviceDiscovery;
} @Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//组装请求
RpcRequest request = new RpcRequest();
request.setClassName(method.getDeclaringClass().getName());
request.setMethodName(method.getName());
request.setParameters(args); String serviceAddress = serviceDiscovery.discover(request.getClassName()); //根据接口名称得到对应的服务地址
//通过tcp传输协议进行传输
TCPTransport tcpTransport = new TCPTransport(serviceAddress);
//发送请求
return tcpTransport.send(request);
}
}

  由于 IP Port 都是来自注册中心的一个服务节点下的子节点的信息,而zookeeper服务器上存储的是该服务的全路径名称,在其之下的直接点才是真的IP/PORT的信息,所以我们通过这个request.getClassName()获取服务的地址,这里的传输方法也要进行修改:仅仅是修改了 IP/PORT的来源。

public class TCPTransport {
private String serviceAddress; public TCPTransport(String serviceAddress) {
this.serviceAddress=serviceAddress;
} //创建一个socket连接
private Socket newSocket(){
System.out.println("创建一个新的连接");
Socket socket;
try{
String[] arrs=serviceAddress.split(":");
socket=new Socket(arrs[],Integer.parseInt(arrs[]));
return socket;
}catch (Exception e){
throw new RuntimeException("连接建立失败");
}
} public Object send(RpcRequest request){
Socket socket=null;
try {
socket = newSocket();
//获取输出流,将客户端需要调用的远程方法参数request发送给
ObjectOutputStream outputStream=new ObjectOutputStream
(socket.getOutputStream());
outputStream.writeObject(request);
outputStream.flush();
//获取输入流,得到服务端的返回结果
ObjectInputStream inputStream=new ObjectInputStream
(socket.getInputStream());
Object result=inputStream.readObject();
inputStream.close();
outputStream.close();
return result; }catch (Exception e ){
throw new RuntimeException("发起远程调用异常:",e);
}finally {
if(socket!=null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}

  启动客户端:

public static void main(String[] args) throws InterruptedException {
IServiceDiscovery serviceDiscovery = new
ServiceDiscoveryImpl("192.168.1.101:2181"); RpcClientProxy rpcClientProxy = new RpcClientProxy(serviceDiscovery); HelloService hello = rpcClientProxy.clientProxy(HelloService.class);
System.out.println(hello.sayHello("wuzz"));
}

  输出:

创建一个新的连接
RPC Hello , wuzz

  这样就完成了注册中心的简单结合,这里还可以通过注解里的 version实现版本的管理,以及如果想要实现负载的效果,由于客户端已经实现了负载的简单实现,只需要启动两个服务端,向zk注册,然后启动客户端去调用即可。