这篇文章在两年前第一次接触RMI时就想写。但当时百度了许多资料,都觉得讲的太深理解不来。最近学习分布式系统相关的知识时,好好的把RMI原理再学了一遍,希望可以写出一篇简单通俗易懂的RMI基本原理解析出来,帮助一下当初和我一样的朋友。
注:本篇文章不涉及源码解析,只讲解最简单的两个问题:
RMI到底是个什么东西?
它又是怎么实现的?
定义
讲解之前,首先弄懂三个名词和它们之间的关系:分布式,RPC,RMI
1. 分布式:
简单来说,就是一个系统大了以后,单个服务器肯定性能不够,所以进行拆分。将原有的一个复杂业务分拆多个子业务,部署在不同的服务器上,相互进行调用。
关于分布式和集群我以前一直有点混淆,这里引用 知乎:分布式与集群的区别是什么?上一位大神的讲解:
小饭店原来只有一个厨师,切菜洗菜备料炒菜全干。后来客人多了,厨房一个厨师忙不过来,又请了个厨师,两个厨师都能炒一样的菜,这两个厨师的关系是集群。为了让厨师专心炒菜,把菜做到极致,又请了个配菜师负责切菜,备菜,备料,厨师和配菜师的关系是分布式,一个配菜师也忙不过来了,又请了个配菜师,两个配菜师关系是集群
2. RPC(Remote Process Call)
即远程进程调用,一个服务器的进程调用另外一个服务器上的进程,这也是分布式应用实现的基础。通过RPC,分布式应用之间互相调用,给整个系统的处理能力和吞吐量带来了近似无限制提升的可能。
3. RMI(Remote Method Invocation)
即远程方法调用,一个Java虚拟机的对象调用运行在另一个Java虚拟机上的对象的方法。
RMI
是RPC
的一种实现方案,基于的是TCP协议
;还有基于HTTP协议
实现的RPC
,例如Hessian
第一个问题:RMI到底是什么?
定义里面已经讲解了部分,RMI是分布式应用之间调用的一种手段。
再具体一点,现在有一个场景,应用A里面有个方法Method1,想要调用应用B里面的方法Method2,怎么办?答案:用RMI。
下面给出RMI调用的Demo代码:
其中,应用B为RMI服务端,提供HelloImpl
类的sayHello
方法给应用A调用
/*
* 任何远程对象都必须直接或间接实现接口Remote 。它的方法才可被远程调用。
*/
public interface IHello extends Remote {
public String sayHello(String name) throws java.rmi.RemoteException;
}
/*
* 远程对象必须实现java.rmi.server.UniCastRemoteObject类,这样才能保证客户端访问获得远程对象时,
* 该远程对象将会把自身的一个拷贝以Socket的形式传输给客户端,此时客户端所获得的这个拷贝称为“存根”,
* 而服务器端本身已存在的远程对象则称之为“骨架”。其实此时的存根是客户端的一个代理,用于与服务器端的通信,
* 而骨架也可认为是服务器端的一个代理,用于接收客户端的请求之后调用远程方法来响应客户端的请求。
*/
/* java.rmi.server.UnicastRemoteObject构造函数中将生成stub和skeleton */
public class HelloImpl extends UnicastRemoteObject implements IHello {
// 这个实现必须有一个显式的构造函数,并且要抛出一个RemoteException异常
protected HelloImpl() throws RemoteException {
super();
}
private static final long serialVersionUID = 4077329331699640331L;
public String sayHello(String name) throws RemoteException {
return "Hello " + this.name + " ^_^ ";
}
}
public class HelloServer {
public static void main(String[] args) {
try {
IHello hello = new HelloImpl(); /* 生成stub和skeleton,并返回stub代理引用 */
/* 本地创建并启动RMI Service,被创建的Registry服务将在指定的端口上侦听到来的请求
* 实际上,RMI Service本身也是一个RMI应用,我们也可以从远端获取Registry:
* public interface Registry extends Remote;
* public static Registry getRegistry(String host, int port) throws RemoteException;
*/
LocateRegistry.createRegistry(4099);
/* 将stub代理绑定到Registry服务的URL上 */
java.rmi.Naming.rebind("rmi://localhost:4099/hello", hello);
System.out.print("Ready");
} catch (Exception e) {
e.printStackTrace();
}
}
}
应用A为RMI客户端,通过RMI调用应用B中HelloImpl
类的sayHello
方法
/*
* 任何远程对象都必须直接或间接实现接口Remote 。它的方法才可被远程调用。
*/
public interface IHello extends Remote {
public String sayHello(String name) throws java.rmi.RemoteException;
}
public class Hello_RMI_Client {
public static void main(String[] args) {
try {
/* 从RMI Registry中请求stub
* 如果RMI Service就在本地机器上,URL就是:rmi://localhost:1099/hello
* 否则,URL就是:rmi://RMIService_IP:1099/hello
*/
IHello hello = (IHello) Naming.lookup("rmi://localhost:4099/hello");
/* 通过stub调用远程接口实现 */
System.out.println(hello.sayHello("zhangxianxin"));
} catch (Exception e) {
e.printStackTrace();
}
}
}/**output:
Hello zhangxianxin ^_^
**/
第二个问题:RMI是怎么实现的?
RMI的实现思路
我们先思考一个问题,如果让我们自己用Java
实现RMI
,要怎么写?
一种思路是:应用A调用应用B的方法时,我们可以将A
调用B
的方法的信息封装起来,通过Socket
通信传到B
的监听端口,B
收到信息后进行解析,获取出调用方法信息后调用对应的方法,然后将方法的返回信息再通过Socket
通讯返回给A
。
这种思路其实就是RMI的大致实现。
RMI的框架
现在我们来更细节的看下RMI的框架:
RMI框架中有五个“对象”:
- 调用服务的客户对象
- Stub(客户辅助对象)
- 提供服务的服务对象
- Skeleton(服务辅助对象)
- RMI Registry
从图中可以看成,当应用A的客户对象想要调用应用B的服务对象中的方法时,找的是Stub这个客户辅助对象。而应用B的服务对象中的方法被调用时,也是通过Skeleton这个服务辅助对象。
Stub和Skeleton
那么Stub和Skeleton这两个辅助对象到底是什么呢?搞清楚了这两个概念你就差不多懂RMI了。
前面我们讲过,RMI的实现思路其实就是把调用方法的信息通过Socket传到应用B去,然后应用B解析方法信息进行调用。那么这个Socket传输和解析的过程在哪里呢?答案就是Stub和Skeleton了。
Stub和Skeleton就是用来负责Socket通信的。它们都是由RMI自动生成的,下面给个代码例子:
public interface Person {
public int getAge() throws Throwable;
public String getName() throws Throwable;
}
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();
}
}
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);
}
}
}
RMI Registry
那RMI Registry呢?
RMI Registry也可以看成一个应用,它是Java内部自己的应用。主要用于查找对象对应的“Stub”。
/* 将stub代理绑定到Registry服务的URL上 */
java.rmi.Naming.rebind("rmi://localhost:1099/hello", hello);
服务端通过上面的代码,会将服务对象hello的Stub绑定在Registry的服务上面
IHello hello = (IHello) Naming.lookup("rmi://localhost:1099/hello");
客户端通过Registry查找出对应的Stub(注意,此处返回的其实是IHelloImpl的Stub对象),然后客户端就可以通过该Stub对象和服务端通信,调用对应的方法了
这个过程如下图:
本文部分内容参考:
java RMI原理详解
深究Java中的RMI底层原理