java-RMI远程调用

什么是RMI

Java RMI是专为Java环境设计的远程方法调用机制,是一种用于实现远程调用(RPC,Remote Procedure Call)的Java API,能直接传输序列化后的Java对象和分布式垃圾收集。它的实现依赖于JVM,因此它支持从一个JVM到另一个JVM的调用。

在Java RMI中,远程服务器实现具体的Java方法并提供接口,客户端本地仅需根据接口类的定义,提供相应的参数即可调用远程方法,其中对象是通过序列化方式进行编码传输的。所以平时说的反序列化漏洞的利用经常是涉及到RMI,就是这个意思。

RMI依赖的通信协议为JRMP(Java Remote Message Protocol,Java远程消息交换协议),该协议是为Java定制的,要求服务端与客户端都必须是Java编写的。

RMI的模式与交互过程

设计模式

RMI的设计模式中,主要包括以下三个部分的角色:

  • Registry:提供服务注册与服务获取。即Server端向Registry注册服务,比如地址、端口等一些信息,Client端从Registry获取远程对象的一些信息,如地址、端口等,然后进行远程调用。
  • Server:远程方法的提供者,并向Registry注册自身提供的服务
  • Client:远程方法的消费者,从Registry获取远程方法的相关信息并且调用

交互过程

  1. 首先,启动RMI Registry服务,启动时可以指定服务监听的端口,也可以使用默认的端口(1099);
  2. 其次,Server端在本地先实例化一个提供服务的实现类,然后通过RMI提供的Naming/Context/Registry等类的bindrebind方法将刚才实例化好的实现类注册到RMI Registry上并对外暴露一个名称;
  3. 最后,Client端通过本地的接口和一个已知的名称(即RMI Registry暴露出的名称),使用RMI提供的Naming/Context/Registry等类的lookup方法从RMI Service那拿到实现类。这样虽然本地没有这个类的实现类,但所有的方法都在接口里了,便可以实现远程调用对象的方法了;

远程对象

远程对象是存在于服务端以供客户端调用的对象。任何可以被远程调用的对象都必须实现 java.rmi.Remote 接口,远程对象的实现类必须继承UnicastRemoteObject类。如果不继承UnicastRemoteObject类,则需要手工初始化远程对象,在远程对象的构造方法的调用UnicastRemoteObject.exportObject()静态方法。这个远程对象中可能有很多个函数,但是只有在远程接口中声明的函数才能被远程调用,其他的公共函数只能在本地的JVM中使用。

使用远程方法调用,必然会涉及参数的传递和执行结果的返回。参数或者返回值可以是基本数据类型,当然也有可能是对象的引用。所以这些需要被传输的对象必须可以被序列化,这要求相应的类必须实现java.io.Serializable接口,并且客户端的serialVersionUID字段要与服务器端保持一致。

流程原理

从RMI设计角度来讲,基本分为三层架构模式来实现RMI,分别为RMI服务端,RMI客户端和RMI注册中心。

客户端

存根/桩(Stub):远程对象在客户端上的代理;
远程引用层(Remote Reference Layer):解析并执行远程引用协议;
传输层(Transport):发送调用、传递远程方法参数、接收远程方法执行结果

服务端

骨架(Skeleton):读取客户端传递的方法参数,调用服务器方的实际对象方法, 并接收方法执行后的返回值;
远程引用层(Remote Reference Layer):处理远程引用后向骨架发送远程方法调用;
传输层(Transport):监听客户端的入站连接,接收并转发调用到远程引用层。

注册表(Registry)

以URL形式注册远程对象,并向客户端回复对远程对象的引用。

流程原理图

编写RMI步骤

定义服务端供远程调用的类

在此之前先定义一个可序列化的Model层的用户类,其实例可放置于服务端进行远程调用:

import java.io.Serializable;

public class PersonEntity implements Serializable {
    private int id;
    private String name;
    private int age;

    public void setId(int id) {
        this.id = id;
    }

    public int getId() {
        return id;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public int getAge() {
        return age;
    }
}

定义一个远程接口

远程接口必须继承java.rmi.Remote接口,且抛出RemoteException错误:

import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.List;

public interface PersonService extends Remote {
    public List<PersonEntity> GetList() throws RemoteException;
}

开发接口的实现类

建立PersonServiceImpl实现远程接口,注意此为远程对象实现类,需要继承UnicastRemoteObject(如果不继承UnicastRemoteObject类,则需要手工初始化远程对象,在远程对象的构造方法的调用UnicastRemoteObject.exportObject()静态方法):

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.LinkedList;
import java.util.List;

public class PersonServiceImpl extends UnicastRemoteObject implements PersonService {
    public PersonServiceImpl() throws RemoteException {
        super();
    // TODO Auto-generated constructor stub
    }

    @Override
    public List<PersonEntity> GetList() throws RemoteException {
    // TODO Auto-generated method stub
        System.out.println("Get Person Start!");
        List<PersonEntity> personList = new LinkedList<PersonEntity>();

        PersonEntity person1 = new PersonEntity();
        person1.setAge(3);
        person1.setId(0);
        person1.setName("small_fish");
        personList.add(person1);

        PersonEntity person2 = new PersonEntity();
        person2.setAge(18);
        person2.setId(1);
        person2.setName("lalalaxiaoyuren");
        personList.add(person2);

        return personList;
    }
}

创建Server和Registry

其实Server和Registry可以单独运行创建,其中Registry可通过代码启动也可通过rmiregistry命令启动,这里只进行简单的演示,将Server和Registry的创建、对象绑定注册表等都写到一块,且Registry直接代码启动:

import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

public class Program {
    public static void main(String[] args) {
        try {
            PersonService personService=new PersonServiceImpl();
            //注册通讯端口
            LocateRegistry.createRegistry(6600);
            //注册通讯路径
            Naming.rebind("rmi://127.0.0.1:6600/PersonService", personService);
            System.out.println("Service Start!");
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

创建客户端并查找调用远程方法

这里我们通过Naming.lookup()来查找RMI Server端的远程对象并获取到本地客户端环境中输出出来:

import java.rmi.Naming;
import java.util.List;

public class Client {
    public static void main(String[] args){
        try{
            //调用远程对象,注意RMI路径与接口必须与服务器配置一致
            PersonService personService=(PersonService) Naming.lookup("rmi://127.0.0.1:6600/PersonService");
            List<PersonEntity> personList=personService.GetList();
            for(PersonEntity person:personList){
                System.out.println("ID:"+person.getId()+" Age:"+person.getAge()+" Name:"+person.getName());
            }
        }catch(Exception ex){
            ex.printStackTrace();
        }
    }
}

函数说明

  • bind(String name, Object obj):注册对象,把对象和一个名字name绑定,这里的name其实就是URL格式。如果改名字已经与其他对象绑定,则抛出NameAlreadyBoundException错误;
  • rebind(String name, Object obj):注册对象,把对象和一个名字name绑定。如果改名字已经与其他对象绑定,不会抛出NameAlreadyBoundException错误,而是把当前参数obj指定的对象覆盖原先的对象;
  • lookup(String name):查找对象,返回与参数name指定的名字所绑定的对象;
  • unbind(String name):注销对象,取消对象与名字的绑定;
posted @ 2022-03-10 10:18  lalalaxiaoyuren  阅读(302)  评论(0编辑  收藏  举报