JavaRMI入门详细

143 阅读11分钟

Java RMI入门

定义:

**RMI: **

远程方法调用(Remote Method Invocation) ,它支持存储在不同地址空间的程序级对象之间彼此进行通信,实现远程对象之间的无缝远程调用。

Java RMI:

1、用于不同虚拟机之间的通信

2、这些虚拟机可以在不同的主机上、也可以在同一个主机上;

3、一个虚拟机中的对象调用另一个虚拟上中的对象的方法,只不过是允许被远程调用的对象要通过一些标志加以标识

  • 优点:避免重复造轮子;
  • 缺点:调用过程很慢,而且该过程是不可靠的,容易发生不可预料的错误,比如网络错误等;

从方法调用角度来看,RMI要解决的问题

是让客户端对远程方法的调用可以相当于对本地方法的调用而屏蔽其中关于远程通信的内容,即使在远程上,也和在本地上是一样的。

从客户端-服务器模型来看,客户端程序直接调用服务端,两者之间是通过JRMP( Java Remote Method Protocol)协议通信,这个协议类似于HTTP协议,规定了客户端和服务端通信要满足的规范。

实际上,客户端只与代表远程主机中对象的Stub对象进行通信,丝毫不知道Server的存在。

客户端只是调用Stub对象中的本地方法,Stub对象是一个本地对象,它实现了远程对象向外暴露的接口,也即它的方法和远程对象暴露的方法的签名是相同的。

客户端认为它是在调用远程对象的方法,实际上是调用Stub对象中的方法。可以理解为Stub对象是远程对象在本地的一个代理,当客户端调用方法的时候,Stub对象会将调用通过网络传递给远程对象。

java 1.2之前,与Stub对象直接对话的是Skeleton对象,在Stub对象将调用传递给Skeleton的过程中,其实这个过程是通过JRMP协议实现转化的,通过这个协议将调用从一个虚拟机转到另一个虚拟机。

java 1.2之后,与Stub对象直接对话的是Server程序,不再是Skeleton对象了。

所以从逻辑上来看,数据是在Client和Server之间横向流动的,但是实际上是从Client到Stub,然后从Skeleton到Server这样纵向流动的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UfWEQVoB-1640164643251)(Java网络复习.assets/SouthEast)]

RMI远程调用步骤

RMI交互图

在这里插入图片描述

存根和骨干网的具体通信过程

在这里插入图片描述

方法调用从客户对象-经-存根(stub)、远程引用层(Remote Reference Layer)和传输层(Transport Layer)向下,传递给主机,然后再次经传输层,向上穿过远程调用层和骨干网(Skeleton),到达服务器对象。

存根扮演着远程服务器对象代理的角色,使该对象可被客户激活。

远程调用层处理语义、管理单一或多重对象的通信,决定调用是应发往一个服务器还是多个。
传输层管理实际的连接,并且追踪可以接受方法调用的远程对象。
骨干网完成对服务器对象实际的方法调用,并获取返回值。
返回值向下经远程引用层、服务器端的传输层传递回客户端,再向上经传输层和远程调用层返回。最后,存根获得返回值。

RMI由3个部分构成

第一个是rmiregistry(JDK提供的一个可以独立运行的程序,在bin目录下)

第二个是server端的程序,对外提供远程对象

第三个是client端的程序,想要调用远程对象的方法。

步骤

首先,先启动rmiregistry服务,启动时可以指定服务监听的端口,也可以使用默认的端口(1099)。

其次,server端在本地先实例化一个提供服务的实现类,然后通过RMI提供的Naming/Context/Registry(下面实例用的Registry)等类的bind或rebind方法将刚才实例化好的实现类注册到rmiregistry上并对外暴露一个名称。

最后,client端通过本地的接口和一个已知的名称(即rmiregistry暴露出的名称)再使用RMI提供的Naming/Context/Registry等类的lookup方法从RMIService那拿到实现类。这样虽然本地没有这个类的实现类,但所有的方法都在接口里了,便可以实现远程调用对象的方法了。

数据传递

Java程序中引用类型(不包括基本类型)的参数传递是按引用传递的,对于在同一个虚拟机中的传递时是没有问题的,因为的参数的引用对应的是同一个内存空间,在分布式系统中,由于对象不存在于同一个内存空间,虚拟机A的对象引用对于虚拟机B没有任何意义,那么怎么解决这个问题呢?

第一种

将引用传递更改为值传递也就是将对象序列化为字节,然后使用该字节的副本在客户端和服务器之间传递,而且一个虚拟机中对该值的修改不会影响到其他主机中的数据;

但是对象的序列化也有一个问题,就是对象的嵌套引用就会造成序列化的嵌套,这必然会导致数据量的激增,因此我们需要有选择进行序列化。

在Java中一个对象如果能够被序列化,需要满足下面两个条件之一:
–是Java的基本类型;
–实现java.io.Serializable接口(String类即实现了该接口);

对于容器类,如果其中的对象是可以序列化的,那么该容器也是可以序列化的;
可序列化的子类也是可以序列化的;

第二种:

使用引用传递,每当远程主机调用本地主机方法时,该调用还要通过本地主机查询该引用对应的对象,在任何一台机器上的改变都会影响原始主机上的数据,因为这个对象是共享的;

RMI中的参数传递和结果返回可以使用的三种机制(取决于数据类型):

​ 简单类型:按值传递,直接传递数据拷贝;
​ 远程对象引用(实现了Remote接口):以远程对象的引用传递;
​ 远程对象引用(未实现Remote接口):按值传递,通过序列化对象传递副本,本身不允许序列化的对象不允许传递给远程方法;

在调用远程对象的方法之前需要一个远程对象的引用,如何获得这个远程对象的引用在RMI中是一个关键的问题,如果将远程对象的发现类比于IP地址的发现可能比较好理解一些。

平常我们上网是通过域名来定位一个网站,实际上网络是通过IP地址来定位网站,因此其中就存在一个映射的过程,域名系统(DNS)就是为了这个目的出现的,在域名系统中通过域名来查找对应的IP地址来访问对应的服务器。

对应的,IP地址在这里就相当于远程对象的引用,而DNS则相当于一个注册表(Registry)

而域名在RMI中就相当于远程对象的标识符,客户端通过提供远程对象的标识符访问注册表,来得到远程对象的引用。这个标识符是类似URL地址格式的,它要满足的规范如下:

该名称是URL形式的,类似于http的URL,schema是rmi;
格式类似于rmi://host:port/name,host指明注册表运行的注解,port表明接收调用的端口,name是一个标识该对象的简单名称
主机和端口都是可选的,如果省略主机,则默认运行在本地;如果端口也省略,则默认端口是1099;

入门实战

基本

实现RMI所需的API几乎都在:

java.rmi:提供客户端需要的类、接口和异常;
java.rmi.server:提供服务端需要的类、接口和异常;
java.rmi.registry:提供注册表的创建以及查找和命名远程对象的类、接口和异常;

其实在RMI中的客户端和服务端并没有绝对的界限,与Web应用中的客户端和服务器还是有区别的。这两者其实是平等的,客户端可以为服务端提供远程调用的方法,这时候,原来的客户端就是服务器端。

实战:客户端调用服务器端的对象更新User的信息

1、entity

该对象必须实现Serializable接口,否则在调用过程中,会抛出NotSerializableException异常

public class User implements Serializable {
    // 服务端 客户端的serialVersionUID字段数据要保持一致
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;
    ... 
2、远程调用对象

远程接口

/**
 * Author:甲粒子
 * Date: 2021/12/22
 * Description:远程接口,该接口需要继承Remote接口,并且接口中的方法全都要抛出RemoteException异常
 */
public interface IUpdateUser extends Remote {
    User updateUser(User u) throws RemoteException;
}

接口实现

import com.zbz.rmi.entity.User;
import com.zbz.rmi.interfaceImpl.IUpdateUser;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
/**
 * Author:甲粒子
 * Date: 2021/12/22
 * Description:远程接口实现类,必须继承UnicastRemoteObject
 * (继承RemoteServer->继承RemoteObject->实现Remote,Serializable),
 * 只有继承UnicastRemoteObject类,才表明其可以作为远程对象,被注册到注册表中供客户端远程调用
 * (补充:客户端lookup找到的对象,只是该远程对象的Stub(存根对象),
 * 而服务端的对象有一个对应的骨架Skeleton(用于接收客户端stub的请求,以及调用真实的对象)对应,
 * Stub是远程对象的客户端代理,Skeleton是远程对象的服务端代理,
 * 他们之间协作完成客户端与服务器之间的方法调用时的通信。)
 */
public class UpdateUserImpl extends UnicastRemoteObject implements IUpdateUser {
    private static final long serialVersionUID = 1L;

    /**
     * 因为UnicastRemoteObject的构造方法抛出了RemoteException异常,因此这里默认的构造方法必须写,
     * 也必须声明抛出RemoteException异常
     *
     * @throws RemoteException
     */

    public UpdateUserImpl() throws RemoteException {
    }

    @Override
    public User updateUser(User u) throws RemoteException {
        System.out.println("-----客户端发送的user-----"+u.toString());
        //更新u
        u.setName(u.getName()+"_更新");
        u.setAge(u.getAge()*2);
        return u;
    }
}
3、Server
import java.net.MalformedURLException;
import java.rmi.AlreadyBoundException;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;

/**
 * Author:甲粒子
 * Date: 2021/12/22
 * Description:服务端启动类,用于创建远程对象注册表以及注册远程对象
 */
public class Server {
    public static void main(String[] args) throws Exception {
        try{
            // 本地主机上的远程对象注册表Registry的实例,
            // 并指定端口为8888,这一步必不可少(Java默认端口是1099)
            LocateRegistry.createRegistry(8888);
            //创建一个远程对象
            IUpdateUser rUpdateUser=new UpdateUserImpl();
            //把远程对象注册到RMI注册服务器上,命名为 rUpdate
            //绑定的URL标准格式为:rmi://host:port/name(其中协议名可以省略,下面两种写法都是正确的)
            // Naming.bind("//localhost:8888/rUpdate",rUpdateUser);
            Naming.bind("rmi://localhost:8888/rUpdate",rUpdateUser);
            System.out.println("------------远程对象IUpdateUser注册成功,等待客户端调用...");
        }catch (RemoteException e) {
            System.out.println("创建远程对象发生异常!");
            e.printStackTrace();
        } catch (AlreadyBoundException e) {
            System.out.println("发生重复绑定对象异常!");
            e.printStackTrace();
        } catch (MalformedURLException e) {
            System.out.println("发生URL畸形异常!");
            e.printStackTrace();
        }

    }
}
4、Client
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;

/**
 * Author:甲粒子
 * Date: 2021/12/22
 * Description:该类为客户端启动类,用于在注册表中查找远程对象实现远程方法调用,
 */
public class Client {
    public static void main(String[] args) {
        try{
            // 在RMI服务注册表中查找名称为rUpdate的对象,并调用其上的方法
            IUpdateUser rUpdateUser = (IUpdateUser) Naming.lookup("rmi://localhost:8888/rUpdate");
            //构造User对象,测试远程对象传输
            User user = new User("甲粒子",20);
            System.out.println("-------------- 服务端返回的的user为" + rUpdateUser.updateUser(user).toString());
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (NotBoundException e) {
            e.printStackTrace();
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }
}
5、运行结果

在这里插入图片描述

在这里插入图片描述

说明:

Java 1.4及 以前的版本中需要手动建立Stub对象,通过运行rmic命令来生成远程对象实现类的Stub对象,但是在Java 1.5之后可以通过动态代理来完成,不再需要这个过程了。

核心代码

Naming.bind("rmi://localhost:8888/rUpdate",rUpdateUser); ,通过一个名称rUpdate映射到该远程对象的引用,客户端通过该名称获取该远程对象的引用。

Naming.lookup(...)获取该远程对象的引用。这个方法通过一个指定的名称来获取,该名称必须与远程对象服务器绑定的名称一致。可以通过Naming.list(...)方法列出所有可用的远程对象。

客户端的引用类型的serialVersionUID字段要与服务器端的对象保持一致