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获取远程方法的相关信息并且调用
交互过程
- 首先,启动RMI Registry服务,启动时可以指定服务监听的端口,也可以使用默认的端口(1099);
- 其次,Server端在本地先实例化一个提供服务的实现类,然后通过RMI提供的Naming/Context/Registry等类的
bind
或rebind
方法将刚才实例化好的实现类注册到RMI Registry上并对外暴露一个名称; - 最后,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):注销对象,取消对象与名字的绑定;