RMI 介绍
RMI全称是 Remote Method Invocation,远程方法调用。从这个名字就可以看出,他的目标和RPC其实 是类似的,是让某个Java虚拟机上的对象调⽤另⼀个Java虚拟机中对象上的方法,只不过RMI是Java独 有的⼀种机制。
RMI 服务演示
示意图
一个简单的RMI服务可以由一下内容组成。
- 定义继承 java.rmi.Remote 的接口
- 定义实现上述接口的类
- 创建服务端将 远程对象的类 注册到 registry 中并绑定到一个地址
- 创建客户端连接远程 RMI 服务,到的对应实现接口的类对象
通过 RMI 远程执行的方法还是执行在 RMI 服务器上的,客户端接收方法执行的返回结果。
本地搭建演示
定义了三个包 client
,registry
,server
registry 包
这个包里定义了一个接口和实现这个接口的类,这个接口要求 client 和 server都要导入, client 通过 rmi 服务得到的对象返回的就是实现了改接口的类对象。
RemoteMethod.java
package registry;
import java.rmi.Remote;
import java.rmi.RemoteException;
//定义接口,该接口必须继承 java.rmi.Remote 接口
public interface RemoteMethod extends Remote {
//自定义要远程调用的方法,这些方法必须有抛出 RemoteException 异常
public String sayHello() throws RemoteException;
public int calcAdd(int num1, int num2) throws RemoteException;
}
RemoteObj.java
package registry;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
//要远程实现的类,该类要实现对应的接口,并且要继承 UnicastRemoteObj 类
public class RemoteObj extends UnicastRemoteObject implements RemoteMethod {
public RemoteObj() throws RemoteException {
super();
}
@Override
public String sayHello() throws RemoteException {
return "hello RMI";
}
@Override
public int calcAdd(int num1, int num2) throws RemoteException {
return num1 + num2;
}
}
server 包
该包用于创建 RMI 服务器,并将 registry 包中的类对象注册到 registry
仓库中,使得客户端能够调用该对象实现的接口中对应的方法。
CreateRmiServer.java
package server;
import registry.RemoteObj;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
public class CreateRmiServer {
RemoteObj obj;
int port;
public CreateRmiServer(int port) throws RemoteException {
//实例化远程对象
this.obj = new RemoteObj();
this.port = port;
}
public void start() throws Exception {
//创建 registry 并绑定端口
LocateRegistry.createRegistry(this.port);
//将实例化的远程对象绑定到一个地址
Naming.bind("rmi://127.0.0.1:" + this.port + "/execMethod", this.obj);
System.out.println("Run in port " + this.port);
}
}
Server.java
RMI 服务器主方法,用于启动
package server;
public class Server {
public static void main(String[] args) throws Exception {
new CreateRmiServer(1099).start();
}
}
client 包
创建客户端用于访问 RMI 服务器
CreateClient.java
客户端要导入 要调用的方法的接口,通过 RMI 服务器远程得到的对象是实现了这个接口的对象,这时接口的重要性就体现出来了,告诉客户端你可以调用的方法有哪些,从而正常调用方法。
package client;
import registry.RemoteMethod;
import registry.RemoteObj;
import java.rmi.Naming;
public class CreateClient {
int port;
public CreateClient(int port) {
this.port = port;
}
public RemoteMethod getObj() throws Exception {
//返回一个 实现了对应接口的对象 (接口中对应着一些方法)
return (RemoteMethod) Naming.lookup("rmi://127.0.0.1:" + this.port + "/execMethod");
}
}
Client.java
package client;
import registry.RemoteMethod;
public class Client {
public static void main(String[] args) throws Exception {
//创建客户端对象
CreateClient client = new CreateClient(1099);
//获得远程对象
RemoteMethod obj = client.getObj();
//调用远程方法执行
String hello = obj.sayHello();
int addRes = obj.calcAdd(1, 1);
System.out.println("this is client");
System.out.println(hello);
System.out.println(addRes);
}
}
编译运行 Server.java
和 Client.java
服务端
客户端
成功调用 sayHello
和 calcAdd
方法
RMI通信过程分析
使用 wireshark 抓下包
这是完整的通信过程,我们可以发现,整个过程进行了两次TCP握手,也就是我们实际建立了两次 TCP连接。
第⼀次建立TCP连接是连接远端 127.0.0.1
的 1099
端口 ,就是我们在代码里看到的端口,⼆ 者进行沟通后,客户端向远端发送了⼀个 “Call” 消息,远端回复了⼀个 “ReturnData” 消息,然后客户端新建了⼀ 个TCP连接,连到远端的 2802
端口。
为什么客户端会连接33769端口呢?
细细阅读数据包我们会发现,在”ReturnData”这个包中,返回了目标的IP地址 172.28.128.1
,其 后跟的⼀个字节 \x00\x00\x0a\xf2
,刚好就是整数 2802 的网络序列:
0000 02 00 00 00 45 00 01 65 b9 a3 40 00 80 06 00 00 ....E..e..@.....
0010 7f 00 00 01 7f 00 00 01 04 4b 18 d1 76 8f 8c d5 .........K..v...
0020 6b cd 58 cb 50 18 20 0f d3 98 00 00 51 ac ed 00 k.X.P. .....Q...
0030 05 77 0f 01 94 17 4f 3e 00 00 01 83 cb e7 b7 f5 .w....O>........
0040 80 11 73 7d 00 00 00 02 00 0f 6a 61 76 61 2e 72 ..s}......java.r
0050 6d 69 2e 52 65 6d 6f 74 65 00 15 72 65 67 69 73 mi.Remote..regis
0060 74 72 79 2e 52 65 6d 6f 74 65 4d 65 74 68 6f 64 try.RemoteMethod
0070 70 78 72 00 17 6a 61 76 61 2e 6c 61 6e 67 2e 72 pxr..java.lang.r
0080 65 66 6c 65 63 74 2e 50 72 6f 78 79 e1 27 da 20 eflect.Proxy.'.
0090 cc 10 43 cb 02 00 01 4c 00 01 68 74 00 25 4c 6a ..C....L..ht.%Lj
00a0 61 76 61 2f 6c 61 6e 67 2f 72 65 66 6c 65 63 74 ava/lang/reflect
00b0 2f 49 6e 76 6f 63 61 74 69 6f 6e 48 61 6e 64 6c /InvocationHandl
00c0 65 72 3b 70 78 70 73 72 00 2d 6a 61 76 61 2e 72 er;pxpsr.-java.r
00d0 6d 69 2e 73 65 72 76 65 72 2e 52 65 6d 6f 74 65 mi.server.Remote
00e0 4f 62 6a 65 63 74 49 6e 76 6f 63 61 74 69 6f 6e ObjectInvocation
00f0 48 61 6e 64 6c 65 72 00 00 00 00 00 00 00 02 02 Handler.........
0100 00 00 70 78 72 00 1c 6a 61 76 61 2e 72 6d 69 2e ..pxr..java.rmi.
0110 73 65 72 76 65 72 2e 52 65 6d 6f 74 65 4f 62 6a server.RemoteObj
0120 65 63 74 d3 61 b4 91 0c 61 33 1e 03 00 00 70 78 ect.a...a3....px
0130 70 77 35 00 0a 55 6e 69 63 61 73 74 52 65 66 00 pw5..UnicastRef.
0140 0c 31 37 32 2e 32 38 2e 31 32 38 2e 31 00 00 0a .172.28.128.1...
0150 f2 be 5d ce 36 ed b5 55 22 94 17 4f 3e 00 00 01 ..].6..U"..O>...
0160 83 cb e7 b7 f5 80 01 01 78 ........x
同时分析这个数据包不难发现这是 java 反序列化的数据,从 \xAC\xED
开始往后就是 java 反序列化的数据了,IP和端口是这个对象的⼀部分。
所以整个过程,就是客户端先连接Registry,并在其中寻找Name是execMethod的对象,这个对应数据流中的Call消息
然后Registry返回⼀个序列化的数据,这个就是找到的 RemoteMethod
的对象,这个对应 数据流中的ReturnData消息;客户端反序列化该对象,发现该对象是⼀个远程对象,地址在 172.28.128.1:2802
,于是再与这个地址建立TCP连接;在这个新的连接中,才执行真正远程方法调用,也就是执行 sayHello()
关系图如下:
RMI Registry就像⼀个网关,他自己是不会执行远程方法的,但RMI Server可以在上⾯注册⼀个Name
到对象的绑定关系;RMI Client通过Name向RMI Registry查询,得到这个绑定关系,然后再连接RMI
Server;最后,远程方法实际上在RMI Server上调用。