定义
RMI(Remote Method Invoke远程方法调用),为远程方法调用,是允许运行在一个Java虚拟机的对象调用运行在另一个Java虚拟机上的对象的方法。 这两个虚拟机可以是运行在相同计算机上的不同进程中,也可以是运行在网络上的不同计算机中。
Java RMI:(Java Remote Method Invocation)是Java编程语言里,一种用于实现远程过程调用的应用程序编程接口,它使客户机上运行的程序可以调用远程服务器上的对象,使Java编程人员能够在网络环境中分布操作。只要一个类继承了java.rmi.Remote接口,就可以成为存在于服务器端的远程对象,给客户端提供服务。其中,Remote接口用于标识其方法可以从非本地虚拟机上调用,任何远程对象都必须间接或直接实现该接口。
简单例子
让我们先来看一个简单的例子:
import java.net.MalformedURLException;
import java.rmi.AlreadyBoundException;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
public class HelloServer {
public HelloServer() {
}
public static void main(String[] args) {
new HelloServer().start();
}
private void start() {
try {
IHello hello = new IHelloImpl();
// 创建注册表对象
LocateRegistry.createRegistry(10006);
// banding远程对象到RIMI注册表上 rmi协议可以缺省
Naming.bind("rmi://localhost:10006/RHello", hello);
System.out.println("启动远程RMI注册服务");
} catch (RemoteException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (MalformedURLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (AlreadyBoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface IHello extends Remote {
/**
* 远程调用的接口 必须抛出 RemoteException
* */
public String say(String msg)throws RemoteException;
}
import java.rmi.Naming;
import java.util.concurrent.ConcurrentHashMap;
public class IHelloClient {
public static void main(String[] args)throws Exception {
IHello hello=(IHello) Naming.lookup("//127.0.0.1:10006/RHello");
System.out.println(hello.say("你好"));
}
}
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class IHelloImpl extends UnicastRemoteObject implements IHello {
/**
* 因为UnicastRemoteObject的构造方法抛出了RemoteException异常,因此这里默认的构造方法必须写,必须声明抛出RemoteException异常
*
* @throws RemoteException
*/
public IHelloImpl() throws RemoteException {
super();
// TODO Auto-generated constructor stub
}
public String say(String msg) {
// TODO Auto-generated method stub
System.out.println("server .."+msg);
return "远程接口调用:" + msg;
}
}
先运行server,在运行client:
例子很简单,我们在client端调用了hello.say(),服务端执行具体的实现。
RMI是怎么实现的?
如果让我们来实现类似rmi的效果,该怎么做呢?可以想到的步骤是:
- 编写服务端服务,并将其通过某个服务机的端口暴露出去供客户端调用。
- 编写客户端程序,客户端通过指定服务所在的主机和端口号、将请求封装并序列化,最终通过网络协议发送到服务端。
- 服务端解析和反序列化请求,调用服务端上的服务,将结果序列化并返回给客户端。
- 客户端接收并反序列化服务端返回的结果,反馈给用户。
这是大致的流程,我们不难想到,RMI本质上就是帮我们封装了这些细节,比如连接的建立和释放,序列化和反序列化等,RMI的具体流程如下:
这里涉及几个概念:
- Stub和Skeleton:这两个的身份是一致的,都是作为代理的存在。客户端的称作Stub,服务端的称作Skeleton。要做到对程序员屏蔽远程方法调用的细节,这两个代理是必不可少的,包括网络连接等细节。
- Registry:顾名思义,可以认为Registry是一个“注册所”,提供了服务名到服务的映射。客户端只需要知道服务名即可调用对应的服务。
RMI工作原理
方法调用从客户对象经占位程序(Stub)、远程引用层(Remote Reference Layer)和传输层(Transport Layer)向下,传递给主机,然后再次经传 输层,向上穿过远程调用层和骨干网(Skeleton),到达服务器对象。 占位程序扮演着远程服务器对象的代理的角色。 远程引用层处理语义、管理单一或多重对象的通信,决定调用是应发往一个服务器还是多个。传输层管理实际的连接,并且追踪可以接受方法调用的远程对象。服务器端的骨干网完成对服务器对象实际的方法调用,并获取返回值。返回值向下经远程引用层、服务器端的传输层传递回客户端,再向上经传输层和远程调用层返回。最后,占位程序获得返回值
要完成以上步骤需要有以下几个步骤:
1、 生成一个远程接口
2、 实现远程对象(服务器端程序)
3、 生成占位程序和骨干网(服务器端程序)
4、 编写服务器程序
5、 编写客户程序
6、 注册远程对象
7、 启动远程对象
RMI由3个部分构成,第一个是RMIService即JDK提供的一个可以独立运行的程序(bin目录下的rmiregistry),第二个是RMIServer即我们自己编写的一个java项目,这个项目对外提供服务。第三个是RMIClient即我们自己编写的另外一个java项目,这个项目远程使用RMIServer提供的服务。
首先,RMIService必须先启动并开始监听对应的端口。
其次,RMIServer将自己提供的服务的实现类注册到RMIService上,并指定一个访问的路径(或者说名称)供RMIClient使用。
最后,RMIClient使用事先知道(或和RMIServer约定好)的路径(或名称)到RMIService上去寻找这个服务,并使用这个服务在本地的接口调用服务的具体方法。
通俗的讲完了再稍微技术的讲下:
- 在一个JVM中启动rmiregistry服务,启动时可以指定服务监听的端口,也可以使用默认的端口。
- RMIServer在本地先实例化一个提供服务的实现类,然后通过RMI提供的Naming,Context,Registry等类的bind或rebind方法将刚才实例化好的实现类注册到RMIService上并对外暴露一个名称。
- RMIClient通过本地的接口和一个已知的名称(即RMIServer暴露出的名称)再使用RMI提供的Naming,Context,Registry等类的lookup方法从RMIService那拿到实现类。这样虽然本地没有这个类的实现类,但所有的方法都在接口里了,想怎么调就怎么调吧。
值得注意的是理论上讲RMIService,RMIServer,RMIClient可以部署到3个不同的JVM中,这个时候的执行的顺序是RMIService---RMIServer—RMIClient。另外也可以由RMIServer来启动RMIService这时候执行的顺序是RMIServer—RMIService—RMIClient。实际应用中很少有单独提供一个RMIService服务器,开发的时候可以使用Registry类在RMIServer中启动RMIService。
当客户端通过RMI注册表找到一个远程接口的时候,所得到的其实是远程接口的一个动态代理对象。当客户端调用其中的方法的时候,方法的参数对象会在序列化之后,传输到服务器端。服务器端接收到之后,进行反序列化得到参数对象。并使用这些参数对象,在服务器端调用实际的方法。调用的返回值Java对象经过序列化之后,再发送回客户端。客户端再经过反序列化之后得到Java对象,返回给调用者。这中间的序列化过程对于使用者来说是透明的,由动态代理对象自动完成。除了序列化之外,RMI还使用了动态类加载技术。当需要进行反序列化的时候,如果该对象的类定义在当前JVM中没有找到,RMI会尝试从远端下载所需的类文件定义。可以在RMI程序启动的时候,通过JVM参数java.rmi.server.codebase来指定动态下载Java类文件的URL。
简单实现stub和skeleton
明白了RMI应用的原理后,可以自行实现一个stub和skeleton程序,进一步探索RMI的代理访问原理。
RMI的本质就是实现在不同JVM之间的调用,它的实现方法就是在两个JVM中各开一个Stub和Skeleton,二者通过socket通信来实现参数和返回值的传递。
- 定义一个Person的接口,其中有两个business method, getAge() 和getName()
Person代码:
public interface Person {
public int getAge() throws Throwable;
public String getName() throws Throwable;
}
- Stub的实现
Person_Stub代码:
import java.io.ObjectOutputStream;
import java.io.ObjectInputStream;
import java.net.Socket;
public class Person_Stub implements Person {
private Socket socket;
public Person_Stub() throws Throwable {
// connect to skeleton
socket = new Socket("computer_name", 9000);
}
public int getAge() throws Throwable {
// pass method name to skeleton
ObjectOutputStream outStream =
new ObjectOutputStream(socket.getOutputStream());
outStream.writeObject("age");
outStream.flush();
ObjectInputStream inStream =
new ObjectInputStream(socket.getInputStream());
return inStream.readInt();
}
public String getName() throws Throwable {
// pass method name to skeleton
ObjectOutputStream outStream =
new ObjectOutputStream(socket.getOutputStream());
outStream.writeObject("name");
outStream.flush();
ObjectInputStream inStream =
new ObjectInputStream(socket.getInputStream());
return (String)inStream.readObject();
}
}
Person_Stub是建立socket连接,并向Skeleton发请求,然后通过Skeleton调用PersonServer的方法,最后接收返回的结果。
- 骨架(Skeleton)的实现
Person_Skeleton代码:
import java.io.ObjectOutputStream;
import java.io.ObjectInputStream;
import java.net.Socket;
import java.net.ServerSocket;
public class Person_Skeleton extends Thread {
private PersonServer myServer;
public Person_Skeleton(PersonServer server) {
// get reference of object server
this.myServer = server;
}
public void run() {
try {
// new socket at port 9000
ServerSocket serverSocket = new ServerSocket(9000);
// accept stub's request
Socket socket = serverSocket.accept();
while (socket != null) {
// get stub's request
ObjectInputStream inStream =
new ObjectInputStream(socket.getInputStream());
String method = (String)inStream.readObject();
// check method name
if (method.equals("age")) {
// execute object server's business method
int age = myServer.getAge();
ObjectOutputStream outStream =
new ObjectOutputStream(socket.getOutputStream());
// return result to stub
outStream.writeInt(age);
outStream.flush();
}
if(method.equals("name")) {
// execute object server's business method
String name = myServer.getName();
ObjectOutputStream outStream =
new ObjectOutputStream(socket.getOutputStream());
// return result to stub
outStream.writeObject(name);
outStream.flush();
}
}
} catch(Throwable t) {
t.printStackTrace();
System.exit(0);
}
}
}
Skeleton类 extends from Thread,它长驻在后台运行,随时接收client发过来的request。并根据发送过来的key去调用相应的business method。
- Person的实现PersonServer类
PersonServer代码:
public class PersonServer implements Person {
private int age;
private String name;
public PersonServer(String name, int age) {
this.age = age;
this.name = name;
}
public int getAge() {
return age;
}
public String getName() {
return name;
}
public static void main(String args []) {
// new object server
PersonServer person = new PersonServer("Richard", 34);
Person_Skeleton skel = new Person_Skeleton(person);
skel.start();
}
}
PersonServer中对Person的接口进行了真正的实现,创建PersonServer实例对象,并启动Person_Skeleton服务。
- Client的实现
PersonClient 代码:
public class PersonClient {
public static void main(String [] args) {
try {
Person person = new Person_Stub();
int age = person.getAge();
String name = person.getName();
System.out.println(name + " is " + age + " years old");
} catch(Throwable t) {
t.printStackTrace();
}
}
}
Client(PersonClient)的本质是,它要知道Person接口的定义,并实例一个Person_Stub,通过Stub来调用business method,至于Stub怎么去和Server沟通,Client就不用管了。
这里只是简单的模拟了实现,rmi的实际实现和上述差别很大,但是这个可以作为简单的入门理解rmi的基本原理。