简介
Java RMI 指的是远程方法调用 (Remote Method Invocation)。它是一种机制,能够让在某个 Java 虚拟机上的对象调用另一个 Java 虚拟机中的对象上的方法。
在Java中,只要一个类继承了java.rmi.Remote
接口,即可成为存在于服务器端的远程对象,供客户端访问并提供一定的服务。
RMI框架
RMI框架封装了所有底层通信细节,并且解决了编组、分布式垃圾收集、安全检查和并发性等通用问题,开发人员只需专注于开发与特定问题领域相关的各种本地对象和远程对象即可。
Stub和Skeleton
RMI框架采用代理来负责客户与远程对象之间通过Socket进行通信的细节。RMI框架为远程对象分别生成了客户端代理和服务器端代理,位于客户端的代理类称为Stub,位于服务器端的代理类称为Skeleton。stub(存根)和skeleton( 骨架 ) 在RMI中充当代理角色,在现实开发中主要是用来隐藏系统和网络的的差异, 这一部分的功能在RMI开发中对程序员是透明的。Stub为客户端编码远程命令并把他们发送到服务器。而Skeleton则是把远程命令解码,调用服务端的远程对象的方法,把结果在编码发给stub,然后stub再解码返回调用结果给客户端。
实现RMI步骤
- 定义一个远程接口,此接口需要继承
Remote
- 开发远程接口的实现类
- 创建一个
server
并把远程对象注册到端口 - 创建一个
client
查找远程对象,调用远程方法
RMI实现
首先我们定义一个远程接口Hello.java
1 | package rmi.server; |
接着我们定义一个实现类HelloImpl.java
1 | package rmi.server; |
这里需要注意的是,实现类必须要继承UnicastRemoteObject
类,客户端访问获得远程对象时,远程对象才会把自身的一个拷贝以Socket
的形式传输给客户端,这个拷贝也就是Stub
,也可以叫做”存根”,这个”Stub”可以看作是远程对象在本地的一个代理,其中包含了远程对象的具体信息,客户端可以通过这个代理与服务端进行交互。
最后实现该类的远程接口中的sayHello()
方法
1 | package rmi.server; |
以上就是server
端的配置
接着我们实现client
端的配置,客户端只需要一个连接程序,即可实现远程调用
1 | package rmi.client; |
RMI执行过程
我们写好的RMI文件目录结构如下
1 | rmi |
编译
1 | java rmi/server/*.java |
1 | java rmi/client/*.java |
生成Stub存根
然后将服务端生成的Stub
存根复制到客户端目录下,最后我们RMI文件目录结构如下
1 | caoyifan@MacBookPro src % tree rmi |
启动RMI服务端
启动RMI客户端
可以看到,我们已经成功调用服务端的sayHello()
方法
抓包分析
我们可以通过Wireshark进行进一步的分析,在客户端请求的过程中,有两次完整的数据交互。可以通过tcp.stream eq 会话序号
划分
tcp.stream eq 17
一开始是TCP
三次握手
接着是RMI代理的确认工作,这里RMI代理返回了客户端的IP地址和端口,用于确认要进行的RMI服务是否是RMI客户端,如果RMI客户端做出响应,则代表RMI客户端需要RMI服务
随后是RMI客户端的确认工作,经过RMI客户端和RMI代理的确认之后,初始化工作就完成了
初始化工作完成后,RMI客户端开始请求RMI服务端,这一过程通过RMI Call
完成
这里我们具体看一下请求的数据包,首先在Wireshark中追踪数据流,之后以RAW格式显示数据
可以明显的看到ac ed 00 05
特征码,我们可以利用SerializationDumper工具进行解析
1 | caoyifan@MacBookPro ~ % java -jar SerializationDumper-v1.13.jar 50aced00057722000000000000000000000000000000000000000000000000000344154dc9d4e63bdf74000568656c6c6f73720019726d692e7365727665722e48656c6c6f496d706c5f537475620000000000000002020000707872001a6a6176612e726d692e7365727665722e52656d6f746553747562e9fedcc98be1651a020000707872001c6a6176612e726d692e7365727665722e52656d6f74654f626a656374d361b4910c61331e0300007078707732000a556e696361737452656600093132372e302e302e310000e2ab53b1d296bd7e099cac7c1a220000017d73a2f78e80010078 |
之后是ReturnData
的数据包
1 | caoyifan@MacBookPro ~ % java -jar SerializationDumper-v1.13.jar 51aced0005770f01353650820000017d739f868f800873720019726d692e7365727665722e48656c6c6f496d706c5f537475620000000000000002020000707872001a6a6176612e726d692e7365727665722e52656d6f746553747562e9fedcc98be1651a020000707872001c6a6176612e726d692e7365727665722e52656d6f74654f626a656374d361b4910c61331e0300007078707732000a556e696361737452656600093132372e302e302e310000e25c35c1c19f1dabc88b353650820000017d739f868f80010178 |
在ReturnData
数据包就包含了RMI服务端的IP和端口
之后RMI客户端就可以直接去访问RMI服务端上对应类的方法。
tcp.stream eq 18
一上来还是先进行三次握手
之后是RMI服务端和RMI客户端的一个验证过程,服务端询问客户端是否是127.0.0.1
,如果客户端返回响应,则代表客户端需要RMI服务
客户端在做出回应同时,继续请求127.0.0.1
,这一步是调用java.rmi
包中的一些类
之后完成了RMI Call
和ReturnData
的过程
从Raw
数据包中可以看出
这一步完成之后,RMI客户端会将参数传输给RMI服务端,服务端会在本地执行后返回结果。
服务端会将参数带入sayHello()
函数执行,并将结果返回给客户端
最后进行TCP
四次挥手并结束此次调用流程。