如需转载,请根据 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 许可,附上本文作者及链接。
本文作者: 执笔成念
作者昵称: zbcn
本文链接: https://1363653611.github.io/zbcn.github.io/2021/02/19/socket_05RMI/
RMI 远程过程调用
- Java的RMI远程调用是指,一个JVM中的代码可以通过网络实现远程调用另一个JVM的某个方法。
- RMI是Remote Method Invocation的缩写。
- 要实现RMI,服务器和客户端必须共享同一个接口
- Java的RMI规定此接口必须派生自
java.rmi.Remote
,并在每个方法声明抛出RemoteException
。
实现
- 编写 接口
1 | /** |
2 | * 世界时钟 |
3 | * <br/> |
4 | * @author zbcn8 |
5 | * @since 2020/9/30 15:05 |
6 | */ |
7 | public interface WorldClock extends Remote { |
8 | |
9 | LocalDateTime getLocalDateTime(String zoneId) throws RemoteException; |
10 | } |
- 编写实现类
1 | /** |
2 | * 世界时钟 |
3 | * <br/> |
4 | * @author zbcn8 |
5 | * @since 2020/9/30 15:11 |
6 | */ |
7 | public class WorldClockService implements WorldClock { |
8 | |
9 | public LocalDateTime getLocalDateTime(String zoneId) throws RemoteException { |
10 | return LocalDateTime.now(ZoneId.of(zoneId)).withNano(0); |
11 | } |
12 | } |
- 编写服务提供方
服务器端的服务相关代码就编写完毕。我们需要通过Java RMI提供的一系列底层支持接口,把上面编写的服务以RMI的形式暴露在网络上,客户端才能调用:
1 | /** |
2 | * rmi 服务提供者 |
3 | */ |
4 | public class RmiServer { |
5 | |
6 | public static void main(String[] args) { |
7 | System.out.println("create World clock remote service..."); |
8 | // 实例化一个WorldClock: |
9 | WorldClock worldClock = new WorldClockService(); |
10 | // 将此服务转换为远程服务接口: |
11 | try { |
12 | WorldClock skeleton = (WorldClock) UnicastRemoteObject.exportObject(worldClock, 0); |
13 | // 将RMI服务注册到1099端口: |
14 | Registry registry = LocateRegistry.createRegistry(1099); |
15 | // 注册此服务,服务名为"WorldClock": |
16 | registry.rebind("WorldClock", skeleton); |
17 | } catch (RemoteException e) { |
18 | e.printStackTrace(); |
19 | } |
20 | } |
21 | } |
上述代码主要目的是通过RMI提供的相关类,将我们自己的WorldClock
实例注册到RMI服务上。RMI的默认端口是1099
,最后一步注册服务时通过rebind()
指定服务名称为"WorldClock"
。
- 客户端
RMI要求服务器和客户端共享同一个接口,因此我们要把WorldClock.java
这个接口文件复制到客户端,然后在客户端实现RMI调用:
1 | /** |
2 | * rmi 客户端: 使用时,需要将 接口和 客户端复制到 对应的项目.例如 demos 项目下的 com.zbcn.socket.rmi.RmiClient |
3 | * <br/> |
4 | * @author zbcn8 |
5 | * @since 2020/9/30 15:19 |
6 | */ |
7 | public class RmiClient { |
8 | |
9 | public static void main(String[] args) { |
10 | try { |
11 | // 连接到服务器localhost,端口1099: |
12 | Registry registry = LocateRegistry.getRegistry("localhost", 1099); |
13 | // 查找名称为"WorldClock"的服务并强制转型为WorldClock接口: |
14 | WorldClock worldClock = (WorldClock) registry.lookup("WorldClock"); |
15 | // 正常调用接口方法: |
16 | LocalDateTime now = worldClock.getLocalDateTime("Asia/Shanghai"); |
17 | // 打印调用结果: |
18 | System.out.println(now); |
19 | } catch (RemoteException e) { |
20 | e.printStackTrace(); |
21 | } catch (NotBoundException e) { |
22 | e.printStackTrace(); |
23 | } |
24 | } |
25 | } |
先运行服务器,再运行客户端。从运行结果可知,因为客户端只有接口,并没有实现类,因此,客户端获得的接口方法返回值实际上是通过网络从服务器端获取的。
整个过程实际上非常简单,对客户端来说,客户端持有的
WorldClock
接口实际上对应了一个“实现类”,它是由Registry
内部动态生成的,并负责把方法调用通过网络传递到服务器端。服务器端接收网络调用的服务并不是我们自己编写的
WorldClockService
,而是Registry
自动生成的代码。我们把客户端的“实现类”称为stub
,而服务器端的网络服务类称为skeleton
,它会真正调用服务器端的WorldClockService
,获取结果,然后把结果通过网络传递给客户端。整个过程由RMI底层负责实现序列化和反序列化:
Java的RMI严重依赖序列化和反序列化,而这种情况下可能会造成严重的安全漏洞,因为Java的序列化和反序列化不但涉及到数据,还涉及到二进制的字节码,即使使用白名单机制也很难保证100%排除恶意构造的字节码。因此,使用RMI时,双方必须是内网互相信任的机器,不要把1099端口暴露在公网上作为对外服务.
此外,Java的RMI调用机制决定了双方必须是Java程序,其他语言很难调用Java的RMI。如果要使用不同语言进行RPC调用,可以选择更通用的协议,例如gRPC。
总结
- Java提供了RMI实现远程方法调用;
- RMI通过自动生成stub和skeleton实现网络调用,客户端只需要查找服务并获得接口实例,服务器端只需要编写实现类并注册为服务;
- RMI的序列化和反序列化可能会造成安全漏洞,因此调用双方必须是内网互相信任的机器,不要把1099端口暴露在公网上作为对外服务。