JAVA语言快速掌握zookeeper RMI 的使用
小标 2019-01-09 来源 : 阅读 777 评论 0

摘要:本文主要向大家介绍了JAVA语言快速掌握zookeeper RMI 的使用,通过具体的内容向大家展示,希望对大家学习JAVA语言有所帮助。

本文主要向大家介绍了JAVA语言快速掌握zookeeper RMI 的使用,通过具体的内容向大家展示,希望对大家学习JAVA语言有所帮助。

在 Java 世界里,有一种技术可以实现“跨虚拟机”的调用,它就是 RMI(Remote Method Invocation,远程方法调用)。例如,服务A 在 JVM1 中运行,服务B 在 JVM2 中运行,服务A 与 服务B 可相互进行远程调用,就像调用本地方法一样,这就是 RMI。在分布式系统中,我们使用 RMI 技术可轻松将 服务提供者(Service Provider)与 服务消费者(Service Consumer)进行分离,充分体现组件之间的弱耦合,系统架构更易于扩展。

 

 本文先从通过一个最简单的 RMI 服务与调用示例,让读者快速掌握 RMI 的使用方法,然后指出 RMI 的局限性,最后笔者对此问题提供了一种简单的解决方案,即使用 ZooKeeper 轻松解决 RMI 调用过程中所涉及的问题。

 

下面我们就从一个最简单的 RMI 示例开始吧!

   


1 发布 RMI 服务

发布一个 RMI 服务,我们只需做三件事情:


定义一个 RMI 接口

编写 RMI 接口的实现类

通过 JNDI 发布 RMI 服务

1.1 定义一个 RMI 接口

RMI 接口实际上还是一个普通的 Java 接口,只是 RMI 接口必须继承 java.rmi.Remote,此外,每个 RMI 接口的方法必须声明抛出一个 java.rmi.RemoteException 异常,就像下面这样:


package demo.zookeeper.rmi.common;

 

import java.rmi.Remote;

import java.rmi.RemoteException;

 

public interface HelloService extends Remote {

 

    String sayHello(String name) throws RemoteException;

}

   


继承了 Remote 接口,实际上是让 JVM 得知该接口是需要用于远程调用的,抛出了 RemoteException 是为了让调用 RMI 服务的程序捕获这个异常。毕竟远程调用过程中,什么奇怪的事情都会发生(比如:断网)。需要说明的是,RemoteException 是一个“受检异常”,在调用的时候必须使用 try...catch... 自行处理。

 

1.2 编写 RMI 接口的实现类

实现以上的 HelloService 是一件非常简单的事情,但需要注意的是,我们必须让实现类继承 java.rmi.server.UnicastRemoteObject 类,此外,必须提供一个构造器,并且构造器必须抛出 java.rmi.RemoteException 异常。我们既然使用 JVM 提供的这套 RMI 框架,那么就必须按照这个要求来实现,否则是无法成功发布 RMI 服务的,一句话:我们得按规矩出牌!


package demo.zookeeper.rmi.server;


import demo.zookeeper.rmi.common.HelloService;

import java.rmi.RemoteException;

import java.rmi.server.UnicastRemoteObject;


public class HelloServiceImpl extends UnicastRemoteObject implements HelloService {


protected HelloServiceImpl() throws RemoteException {

}

 

@Override

public String sayHello(String name) throws RemoteException {

    return String.format("Hello %s", name);

}

   


}

为了满足 RMI 框架的要求,我们确实做了很多额外的工作(继承了 UnicastRemoteObject 类,抛出了 RemoteException 异常),但这些工作阻止不了我们发布 RMI 服务的决心!我们可以通过 JVM 提供的 JNDI(Java Naming and Directory Interface,Java 命名与目录接口)这个 API 轻松发布 RMI 服务。


1.3 通过 JNDI 发布 RMI 服务

发布 RMI 服务,我们需要告诉 JNDI 三个基本信息:1. 域名或 IP 地址(host)、2. 端口号(port)、3. 服务名(service),它们构成了 RMI 协议的 URL(或称为“RMI 地址”):


rmi://:/

如果我们是在本地发布 RMI 服务,那么 host 就是“localhost”。此外,RMI 默认的 port 是“1099”,我们也可以自行设置 port 的值(只要不与其它端口冲突即可)。service 实际上是一个基于同一 host 与 port 下唯一的服务名,我们不妨使用 Java 完全类名来表示吧,这样也比较容易保证 RMI 地址的唯一性。


对于我们的示例而言,RMI 地址为:


rmi://localhost:1099/demo.zookeeper.rmi.server.HelloServiceImpl

我们只需简单提供一个 main() 方法就能发布 RMI 服务,就像下面这样:




package demo.zookeeper.rmi.server;


import java.rmi.Naming;

import java.rmi.registry.LocateRegistry;


public class RmiServer {


public static void main(String[] args) throws Exception {

    int port = 1099;

    String url = "rmi://localhost:1099/demo.zookeeper.rmi.server.HelloServiceImpl";

    LocateRegistry.createRegistry(port);

    Naming.rebind(url, new HelloServiceImpl());

}

   


}

需要注意的是,我们通过 LocateRegistry.createRegistry() 方法在 JNDI 中创建一个注册表,只需提供一个 RMI 端口号即可。此外,通过 Naming.rebind() 方法绑定 RMI 地址与 RMI 服务实现类,这里使用了 rebind() 方法,它相当于先后调用 Naming 的 unbind() 与 bind() 方法,只是使用 rebind() 方法来得更加痛快而已,所以我们选择了它。


运行这个 main() 方法,RMI 服务就会自动发布,剩下要做的就是写一个 RMI 客户端来调用已发布的 RMI 服务。


2 调用 RMI 服务

同样我们也使用一个 main() 方法来调用 RMI 服务,相比发布而言,调用会更加简单,我们只需要知道两个东西:1. RMI 请求路径、2. RMI 接口(一定不需要 RMI 实现类,否则就是本地调用了)。数行代码就能调用刚才发布的 RMI 服务,就像下面这样:




package demo.zookeeper.rmi.client;


import demo.zookeeper.rmi.common.HelloService;

import java.rmi.Naming;


public class RmiClient {


public static void main(String[] args) throws Exception {

    String url = "rmi://localhost:1099/demo.zookeeper.rmi.server.HelloServiceImpl";

    HelloService helloService = (HelloService) Naming.lookup(url);

    String result = helloService.sayHello("Jack");

    System.out.println(result);

}

  

}

当我们运行以上 main() 方法,在控制台中看到“Hello Jack”输出,就表明 RMI 调用成功。


3 RMI 服务的局限性

可见,借助 JNDI 这个所谓的命名与目录服务,我们成功地发布并调用了 RMI 服务。实际上,JNDI 就是一个注册表,服务端将服务对象放入到注册表中,客户端从注册表中获取服务对象。在服务端我们发布了 RMI 服务,并在 JNDI 中进行了注册,此时就在服务端创建了一个 Skeleton(骨架),当客户端第一次成功连接 JNDI 并获取远程服务对象后,立马就在本地创建了一个 Stub(存根),远程通信实际上是通过 Skeleton 与 Stub 来完成的,数据是基于 TCP/IP 协议,在“传输层”上发送的。毋庸置疑,理论上 RMI 一定比 WebService 要快,毕竟 WebService 是基于 HTTP 的,而 HTTP 所携带的数据是通过“应用层”来传输的,传输层较应用层更为底层,越底层越快。


既然 RMI 比 WebService 快,使用起来也方便,那么为什么我们有时候还要用 WebService 呢?


其实原因很简单,WebService 可以实现跨语言系统之间的调用,而 RMI 只能实现 Java 系统之间的调用。也就是说,RMI 的跨平台性不如 WebService 好,假如我们的系统都是用 Java 开发的,那么当然首选就是 RMI 服务了。


貌似 RMI 确实挺优秀的,除了不能跨平台以外,还有那些问题呢?


笔者认为有两点局限性:


RMI 使用了 Java 默认的序列化方式,对于性能要求比较高的系统,可能需要使用其它序列化方案来解决(例如:Protobuf)。

RMI 服务在运行时难免会存在出故障,例如,如果 RMI 服务无法连接了,就会导致客户端无法响应的现象。

在一般的情况下,Java 默认的序列化方式确实已经足以满足我们的要求了,如果性能方面如果不是问题的话,我们需要解决的实际上是第二点,也就是说,让使系统具备 HA(High Availability,高可用性)。


4 使用 ZooKeeper 提供高可用的 RMI 服务

ZooKeeper 是 Hadoop 的一个子项目,用于解决分布式系统之间的数据一致性问题。如果读者尚不了解 ZooKeeper 的工作原理与使用方法,可以通过以下链接来了解:


ZooKeeper 官网

分布式服务框架 ZooKeeper – 管理分布式环境中的数据

本文假设读者已经对 ZooKeeper 有一定了解的前提下,对 RMI 的高可用性问题提供一个简单的解决方案。


要想解决 RMI 服务的高可用性问题,我们需要利用 ZooKeeper 充当一个 服务注册表(Service Registry),让多个 服务提供者(Service Provider)形成一个集群,让 服务消费者(Service Consumer)通过服务注册表获取具体的服务访问地址(也就是 RMI 服务地址)去访问具体的服务提供者。如下图所示:


服务注册表


需要注意的是,服务注册表并不是 Load Balancer(负载均衡器),提供的不是“反向代理”服务,而是“服务注册”与“心跳检测”功能。


利用服务注册表来注册 RMI 地址,这个很好理解,那么“心跳检测”又如何理解呢?说白了就是通过服务中心定时向各个服务提供者发送一个请求(实际上建立的是一个 Socket 长连接),如果长期没有响应,服务中心就认为该服务提供者已经“挂了”,只会从还“活着”的服务提供者中选出一个做为当前的服务提供者。


也许读者会考虑到,服务中心可能会出现单点故障,如果服务注册表都坏掉了,整个系统也就瘫痪了。看来要想实现这个架构,必须保证服务中心也具备高可用性。


ZooKeeper 正好能够满足我们上面提到的所有需求。


使用 ZooKeeper 的临时性 ZNode 来存放服务提供者的 RMI 地址,一旦与服务提供者的 Session 中断,会自动清除相应的 ZNode。

让服务消费者去监听这些 ZNode,一旦发现 ZNode 的数据(RMI 地址)有变化,就会重新获取一份有效数据的拷贝。

ZooKeeper 与生俱来的集群能力(例如:数据同步与领导选举特性),可以确保服务注册表的高可用性。

4.1 服务提供者

需要编写一个 ServiceProvider 类,来发布 RMI 服务,并将 RMI 地址注册到 ZooKeeper 中(实际存放在 ZNode 上)。




package demo.zookeeper.rmi.server;


import demo.zookeeper.rmi.common.Constant;

import java.io.IOException;

import java.net.MalformedURLException;

import java.rmi.Naming;

import java.rmi.Remote;

import java.rmi.RemoteException;

import java.rmi.registry.LocateRegistry;

import java.util.concurrent.CountDownLatch;

import org.apache.zookeeper.CreateMode;

import org.apache.zookeeper.KeeperException;

import org.apache.zookeeper.WatchedEvent;

import org.apache.zookeeper.Watcher;

import org.apache.zookeeper.ZooDefs;

import org.apache.zookeeper.ZooKeeper;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;


public class ServiceProvider {

 

// 用于等待 SyncConnected 事件触发后继续执行当前线程

private CountDownLatch latch = new CountDownLatch(1);

 

// 发布 RMI 服务并注册 RMI 地址到 ZooKeeper 中

public void publish(Remote remote, String host, int port) {

    String url = publishService(remote, host, port); // 发布 RMI 服务并返回 RMI 地址

    if (url != null) {

        ZooKeeper zk = connectServer(); // 连接 ZooKeeper 服务器并获取 ZooKeeper 对象

        if (zk != null) {

            createNode(zk, url); // 创建 ZNode 并将 RMI 地址放入 ZNode 上

        }

    }

}

 

// 发布 RMI 服务

private String publishService(Remote remote, String host, int port) {

    String url = null;

    try {

        url = String.format("rmi://%s:%d/%s", host, port, remote.getClass().getName());

        LocateRegistry.createRegistry(port);

        Naming.rebind(url, remote);

        LOGGER.debug("publish rmi service (url: {})", url);

    } catch (RemoteException | MalformedURLException e) {

        LOGGER.error("", e);

    }

    return url;

}

 

// 连接 ZooKeeper 服务器

private ZooKeeper connectServer() {

    ZooKeeper zk = null;

    try {

        zk = new ZooKeeper(Constant.ZK_CONNECTION_STRING, Constant.ZK_SESSION_TIMEOUT, new Watcher() {

            @Override

            public void process(WatchedEvent event) {

                if (event.getState() == Event.KeeperState.SyncConnected) {

                    latch.countDown(); // 唤醒当前正在执行的线程

                }

            }

        });

        latch.await(); // 使当前线程处于等待状态

    } catch (IOException | InterruptedException e) {

        LOGGER.error("", e);

    }

    return zk;

}

 

// 创建 ZNode

private void createNode(ZooKeeper zk, String url) {

    try {

        byte[] data = url.getBytes();

        String path = zk.create(Constant.ZK_PROVIDER_PATH, data, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL); // 创建一个临时性且有序的 ZNode

        LOGGER.debug("create zookeeper node ({} => {})", path, url);

    } catch (KeeperException | InterruptedException e) {

        LOGGER.error("", e);

        

   

本文由职坐标整理并发布,希望对同学们有所帮助。了解更多详情请关注编程语言JAVA频道!

本文由 @小标 发布于职坐标。未经许可,禁止转载。
喜欢 | 1 不喜欢 | 0
看完这篇文章有何感觉?已经有1人表态,100%的人喜欢 快给朋友分享吧~
评论(0)
后参与评论

您输入的评论内容中包含违禁敏感词

我知道了

助您圆梦职场 匹配合适岗位
验证码手机号,获得海同独家IT培训资料
选择就业方向:
人工智能物联网
大数据开发/分析
人工智能Python
Java全栈开发
WEB前端+H5

请输入正确的手机号码

请输入正确的验证码

获取验证码

您今天的短信下发次数太多了,明天再试试吧!

提交

我们会在第一时间安排职业规划师联系您!

您也可以联系我们的职业规划师咨询:

小职老师的微信号:z_zhizuobiao
小职老师的微信号:z_zhizuobiao

版权所有 职坐标-一站式IT培训就业服务领导者 沪ICP备13042190号-4
上海海同信息科技有限公司 Copyright ©2015 www.zhizuobiao.com,All Rights Reserved.
 沪公网安备 31011502005948号    

©2015 www.zhizuobiao.com All Rights Reserved

208小时内训课程