这可不是目录
- 1.RMI原理与说明
- 1.1含义
- 1.2流程
- 1.3rmi的简单实现
- 1.4RMI的局限性
- 2.zookeeper实现RMI服务(高可用、HA)
- 2.1实现原理
- 2.2高可用分析
- 2.3zookeeper实现
- 2.3.1代码分析
- 2.3.2公共部分
- 2.3.3服务端
- 2.3.4客户端
- 2.3.5运行与部署
- 2.3.6效果展示与说明
1.RMI原理与说明
1.1含义
远程方法调用
仅适用于JAVA
RMI是一种用于实现远程过程调用的应用程序编程接口。它使客户机上运行的程序可以调用远程服务器上的对象。远程方法调用特性使Java编程人员能够在网络环境中分布操作。RMI全部的宗旨就是尽可能简化远程接口对象的使用。RMI(Remote Method Invocation)远程方法调用是一种计算机之间利用远程对象互相调用实现双方通讯的一种通讯机制。使用这种机制,某一台计算机上的对象可以调用另外一台计算机上的对象来获取远程数据。
1.2流程
1.3rmi的简单实现
客户端:RmiClient.java
package com.rmi.client;
import com.rmi.common.HelloService;
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.Remote;
import java.rmi.RemoteException;
public class RmiClient {
public static void main(String[] args) throws MalformedURLException, NotBoundException, RemoteException {
System.out.println("rmi client running");
//定义url
String url ="rmi://127.0.0.1:1099/com.rmi.server.HelloServiceImpl";
//寻找发布的服务
Remote lookup = Naming.lookup(url);
//强制类型转换
HelloService helloService = (HelloService) lookup;
//调用目标方法
String result = helloService.sayHello("wunaiieq");
System.out.println("result:"+result);
}
}
远程接口:HelloService.java
package com.rmi.common;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface HelloService extends Remote {
String sayHello(String name) throws RemoteException;
}
远程接口的实现类:HelloServiceImpl.java
package com.rmi.server;
import com.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 "Hello"+name;
}
}
rmi服务:RmiServer
package com.rmi.server;
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
public class RmiServer {
public static void main(String[] args) throws Exception{
//定义发布RMi服务的端口
int port = 1099;
String url = "rmi://127.0.0.1:1099/com.rmi.server.HelloServiceImpl";
//注册服务:相当于在JNDI中创建了一个注册表
LocateRegistry.createRegistry(port);
//绑定服务:将RMI服务的实现类对象和url绑定
Naming.rebind(url,new HelloServiceImpl());
}
}
运行说明
- 先启动RmiServer,注册远程对象
- 再启动RmiCllient,查找此对象,并调用远程方法
1.4RMI的局限性
- 只能使用Java,不支持跨语言
- RMI使用了Java默认的序列化方式,对于要求较高的系统,可能需要其他的序列化方案进行解决(Protobuf)
- RMI服务在运行时可能会出现单点故障的问题,因此需要配置实现高可用HA
2.zookeeper实现RMI服务(高可用、HA)
2.1实现原理
服务注册与发现:RMI服务端在启动后,可以在ZooKeeper上注册一个临时节点(Ephemeral Node),并将自己的服务地址写入该节点。客户端在需要调用RMI服务时,可以监听ZooKeeper上的这些临时节点,以获取服务地址。由于ZooKeeper会监控这些节点的状态,一旦服务端节点宕机或断开连接,对应的临时节点就会被自动删除。客户端在感知到这一变化后,可以重新获取有效的服务地址,从而实现了服务的自动发现和故障切换。
负载均衡:ZooKeeper还可以作为服务注册中心,为多个RMI服务端实例提供统一的注册和发现接口。客户端在调用RMI服务时,可以通过ZooKeeper获取到多个服务端的地址,并根据一定的策略(如轮询、随机等)选择一个进行调用。这样可以实现负载的均衡分配,避免单个服务端过载。注意,此处只能实现相对均衡,原则上不等同于负载均衡服务器
服务状态监控:ZooKeeper可以监控RMI服务端的状态信息(如CPU使用率、内存占用率等),并将这些信息反馈给客户端或系统管理员。当发现某个服务端状态异常时,可以及时采取措施(如重启服务、扩展资源等)来恢复服务的正常运行。
zookeeper在此处相当于一个注册表
2.2高可用分析
1. rmi服务高可用
首先一个服务端只能运行于一台服务中心上,提供的同一个服务可以存在多个,这样当某一个服务宕机时,服务中心仍存在其他相同的服务。
因此,这样的同名服务,同时运行,但是端口不一致,客户端在调用这样的服务时,随机选取(自定义选取也可以)一个znode节点,调用rmi服务
2. 服务注册中心高可用
上述的服务已经保证了不会宕机,但服务中心仍存在宕机的可能。
因此配置zookeeper高可用,在每个zookeeper上运行相应的服务,以保证当一个服务中心宕机,仍然可以提供服务。
2.3zookeeper实现
2.3.1代码分析
将设置6个代码块,红框表示客户端程序,黄框表示服务器端,中间为公共部分
2.3.2公共部分
客户端和服务端应共同规定这个包中所有的接口
Constant.java
这个接口主要是规定一些常量,以便于服务端和客户端的使用
package com.zkrmi.common;
public interface Constant {
//zk集群的地址
String ZK_CONNECTION_STRING="192.168.80.111:2181,192.168.80.112:2181,192.168.80.113:2181";
//连接超时时间
int ZK_SESSION_TIMEOUT = 5000;
//服务列表对应的临时节点的parent节点
String ZK_REGISTRY_PATH="/registry";
//临时节点的路径,注意创建的是临时顺序节点,因此最后显示的是provider0,provider1....
String ZK_provider_PATH=ZK_REGISTRY_PATH+"/provider";
}
HelloService.java
远程接口,这是提前定义好,应当被客户端和服务端同时知晓的,共同规定的。
package com.zkrmi.common;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface HelloService extends Remote {
String sayHello(String name) throws RemoteException;
}
2.3.3服务端
任务1. 实现远程接口
任务2. 在zookeeper上注册远程方法服务
HelloServiceImpl.java
这串代码主要用于实现远程接口,没有什么特殊的点
package com.zkrmi.server;
import com.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 {
//注意,在不同的zk集群上,请修改以下说明,在实际生产中,不同节点上的实现类应保持一致
return "Hello_ZK112"+name;
}
}
ServiceProvider.java
在zookeeper上发布可以提供的服务,即远程对象,这里进行了封装,后续的调用将在主函数中进行。
package com.zkrmi.server;
import com.zkrmi.common.Constant;
import com.zookeeper.ZooKeeperFactory;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
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;
public class ServiceProvider {
private CountDownLatch latch = new CountDownLatch(1);
/**
* 发布RMI服务
*
* @param remote 远程对象,即HelloServiceImpl的实例
* @param host 192.168.80.113,zookeeper的地址
* @param port 11214,11215,11216,这个表示端口
* @return rmi地址 rmi://192.168.80.113:11214/com.rmi.server.HelloServiceImpl
*/
private String publishServer(Remote remote, String host, int port) {
String url = null;
try {
//设置发布服务的rmi地址 rmi://192.168.80.113:11214/com.rmi.server.HelloServiceImpl
url = String.format("rmi://%s:%d/%s", host, port, remote.getClass().getName());
//注册服务:相当于在JNDI中创建了一个注册表
LocateRegistry.createRegistry(port);
//绑定服务:将RMI服务的实现类对象和url绑定
Naming.rebind(url, remote);
} catch (RemoteException | MalformedURLException e) {
e.printStackTrace();
}
return url;
}
/**
* 创建临时节点
*
* @param zk:传入一个zookeeper对象
* @param url:url是临时节点的数据,表示rmi地址
*/
private void createNode(ZooKeeper zk, String url) {
try {
byte[] data = url.getBytes();
//创建一个临时有序的节点
String result = zk.create(Constant.ZK_provider_PATH, data, ZooDefs.
Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println("节点创建成功"+result);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* @param remote 调用时填入一个远程对象,这里使用remote进行标识,表示此对象可以从服务器端调用
* */
public void publish(Remote remote, String host, int port) throws Exception {
//调用publishServer发布rmi服务,获取rmi的地址
String url = publishServer(remote, host, port);
//连接zookeeper集群
if (url!=null){
ZooKeeper zk =ZooKeeperFactory.create(Constant.ZK_CONNECTION_STRING);
if (zk!=null){
createNode(zk,url);
}else {
System.out.println("zk==null,节点创建失败");
}
}else {
System.out.println("url==null,发布失败");
}
}
}
Server.java
作为服务器端的主类,值得说明的是,远程服务端的端口自拟即可(建议在1024到49151)
package com.zkrmi.server;
import com.rmi.common.HelloService;
import java.rmi.RemoteException;
//作为服务端的main类
public class Server {
public static void main(String[] args) throws Exception {
//zk节点
String host ="192.168.80.112";
//第一个端口
System.out.println("server 10100 start");
int port =Integer.parseInt("10100");
//创建服务的生产者对象
ServiceProvider provider0 = new ServiceProvider();
//创建远程对象,客户端将使用此对象进行远程方法调用
HelloService helloService =new HelloServiceImpl();
//发布rmi服务
provider0.publish(helloService,host,port);
//第二个端口
System.out.println("server 10101 start");
int port1 =Integer.parseInt("10101");
ServiceProvider provider1 = new ServiceProvider();
provider1.publish(helloService,host,port1);
}
}
2.3.4客户端
任务1:获取所有的rmi地址
任务2:当rmi地址更新,或者某个服务器异常时,需要重新获取
ServiceConsumer.java
客户端的方法支持,实现上述任务1和任务2的要求
package com.zkrmi.client;
import com.zkrmi.common.Constant;
import com.zookeeper.ZooKeeperFactory;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import java.rmi.ConnectException;
import java.rmi.Naming;
import java.rmi.Remote;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadLocalRandom;
public class ServiceConsumer {
private CountDownLatch latch =new CountDownLatch(1);
/**保存最新的rmi地址*/
private volatile List<String> urlList =new ArrayList<>();
/**构造器,用于观察/registry节点的所有子节点,并更新urlList*/
public ServiceConsumer() throws Exception {
ZooKeeper zk = ZooKeeperFactory.create(Constant.ZK_CONNECTION_STRING);
if (zk!=null){
watchNode(zk);
}
}
/**观察/registry节点下所有子节点是否有变化
* <br>初始化:构造器中调用一次,获取所有rmi地址
* <br>若有:重新调用此方法,更新rmi地址
* @param zk 用final定义,防止后续调用时获取的节点是新节点,监听不到变化
* */
private void watchNode(final ZooKeeper zk){
try {
//获取所有子节点名称,并设置监听
List<String> nodeList = zk.getChildren(Constant.ZK_REGISTRY_PATH, new Watcher() {
//当子节点发生变化时,再次调用这个watchNode方法
@Override
public void process(WatchedEvent event) {
if (event.getType()==Event.EventType.NodeChildrenChanged){
watchNode(zk);
}
}
});
List<String> dataList =new ArrayList<>();
//根据子节点名称进行遍历,获取所有子节点的数据,即rmi的地址
for (String node:nodeList){
byte[] data =zk.getData(Constant.ZK_REGISTRY_PATH+"/"+node,false,null);
dataList.add(new String(data));
}
//获取所有rmi地址,更新-->重新调用此方法-->再度获取
urlList =dataList;
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (KeeperException e) {
throw new RuntimeException(e);
}
}
/**查找rmi服务
* */
public <T extends Remote> T lookup(){
T service =null;
//由于前面的更新,因此urlList始终保持的是最新的
int size =urlList.size();
if (size>0){
String url;
if (size ==1){
url = urlList.get(0);
System.out.println("只获取到一个url:"+url);
}else {
url =urlList.get(ThreadLocalRandom.current().nextInt(size));
System.out.println("获取一个随机的url:"+url);
}
System.out.println("当前url:"+url);
service = lookupService(url);
}
return service;
}
@SuppressWarnings("unchecked")
private <T> T lookupService(String url){
T remote =null;
try {
remote=(T) Naming.lookup(url);
}catch (Exception e){
if (e instanceof ConnectException){
System.out.println("连接中断,重试");
if (urlList.size()!=0){
url=urlList.get(0);
return lookupService(url);
}
}
}
return remote;
}
}
Client.java
作为客户端的主类,实现远程方法调用
package com.zkrmi.client;
import com.rmi.common.HelloService;
public class Client {
public static void main(String[] args) throws Exception {
ServiceConsumer consumer =new ServiceConsumer();
while (true){
HelloService helloService =consumer.lookup();
String result =helloService.sayHello("wunaiieq");
System.out.println(result);
Thread.sleep(3000);
}
}
}
2.3.5运行与部署
- 建议部署于虚拟机上
- 为测试zookeeper效果,请将服务端部署于不同的zookeeper节点上
- 客户端可以运行于非zookeeper集群的主机
- 上述代码中缺少zookeeperFactory.java可以去博客中复制(不放这,太乱了)
打包
pom.xml
为实现如下效果,请打3个包,2个服务端,1个客户端,打包时修改pom.xml文件中的主函数
<?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>
<groupId>com.wunaiieq</groupId>
<artifactId>zookeeper02</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.7.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<!--声明-->
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.3.0</version>
<!--具体配置-->
<configuration>
<archive>
<manifest>
<!--jar包的执行入口-->
<mainClass>com.zkrmi.client.Client</mainClass>
</manifest>
</archive>
<descriptorRefs>
<!--描述符,此处为预定义的,表示创建一个包含项目所有依赖的可执行 JAR 文件;
允许自定义生成jar文件内容-->
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<!--执行配置-->
<executions>
<execution>
<!--执行配置ID,可修改-->
<id>make-assembly</id>
<!--执行的生命周期-->
<phase>package</phase>
<goals>
<!--执行的目标,single表示创建一个分发包-->
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
2.3.6效果展示与说明
112 , 113为连接到zookeeper集群的主机,作为服务中心
114为客户端,调用rmi服务
检查114的输出,可以看到,服务能正常调用
代码其他解释
zk集群中,113状态为leader,112follower,同时运行服务端jar包,均正常创建了rmi服务对象
ZK高可用
当一个zk节点宕机,另一个仍可以正常运行。保证服务不中断
服务高可用
由于服务的端口号不一致,因此当关闭一个服务时,仍存在rmi服务供客户端使用
客户端选择
在上述代码中,设置的时随机选择,这个不重要,自行设置即可