JavaSec-Remote Method Invocation

学习大纲

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
1. 关于RMI
1.1. RMI概述
1.2. RMI的三层架构
1.3. Stubs and Skeletons
1.4. RMI的运行机制
2. Remote Method Invocation
2.1. 远程对象
2.1.1. 继承UnicastRemoteObject的时候构造函数
2.1.1.1. tips
2.1.2. 没有继承UnicastRemoteObject的时候构造函数
2.1.3. IHello.java
2.1.3.1. tips
2.2. 对象调用过程
2.2.1. 本地对象的调用
2.2.2. 远程对象的调用
2.3. RMI Registry
2.3.1. RMI Registry的注册
2.3.2. RMI Registry的使用
3. RMI的简单实现
3.1. Server
3.1.1. 接口实现
3.1.2. 创建类用于远程调用
3.1.3. 实现调用
3.1.3.1. java.rmi.Naming
3.1.3.2. 注册
3.1.4. 服务器端完整代码
3.1.4.1. IHello.java
3.1.4.2. RMIServer.java
3.2. Client
3.2.1. 客户端完整代码
3.2.1.1. IHello.java
3.2.1.2. RMIClient.java
4. JRMP协议分析
4.1. 第一条TCP链接
4.2. 第二条TCP链接
4.3. 两条TCP链接分别断开

关于RMI

RMI概述

RMI-Remote Method Invocation顾名思义即为Java的远程方法调用;是基于注册中心和服务来进行实现;可以用于实现微服务。RMI用于构建分布式应用程序,RMI实现了Java程序之间跨JVM的远程通信。

它的功能是让Java的某一台虚拟机调用另外一台虚拟机中对象的方法,是Java独有的另一种机制。非常的灵活;再一次的为攻击者提供了方便。

在网络传输过程中,RMI的对象是通过序列化的方式进行编码传输的,这也就意味着RMI在接受到序列化编码的对象之后会进行反序列化

RMI的三层架构

RMI是由三层架构模式来实现的

  • Client-客户端:客户端调用服务端的方法
  • Server-服务端:远程方法调用的提供者,也是代码执行真正的地方。在执行结束之后会返回一个代码的执行结果给客户端
  • Registry-注册中心:用于客户端查询要调用的方法的引用;其本质是map(相当于字典)

Stubs and Skeletons

而为了屏蔽网络通信的复杂性时,RMI引入了两个概念,分别是Stubs(客户端存根)以及Skeletons(服务端骨架),当客户端试图调用一个远端的对象时,实际调用的是客户端本地的一个代理类(Proxy),这个代理类就称为Stub,而在调用远端的目标类之前也会经过一个对应的代理类Skeletons,它从Stub中接收远程方法调用并传递给真实的类。Stubs 以及 Skeletons 的调用对于 RMI 服务的使用者来讲是隐藏的,我们无需主动的去调用相关的方法。但实际的客户端和服务端的网络通信时通过 StubSkeleton 来实现的。

RMI的运行机制

调用的机制大概如下:

  • RMI客户端在调用远程方式是会先创建Stub(sun.rmi.registryImpl_Stub)
  • Stub会将Remote对象传递给远程引用层(java.rmi.server.RemoteRef)并创建java.rmi,server.RemoteCall(远程调用)对象
  • RMI客户端的远程引用层传输RemoteCall序列化后的请求信息通过Socket连接的方法传输到RMI服务端的远程引用层
  • RMI客户端远程引用层(sum.rmi.server.UnicasServerRef)收到请求会请求传输给Skeleton(sun.rmi.registery.RegisterImpl_Skel#diapatch)
  • Skeleton调用客户端请求:bindlistlookuprebindunbind,如果是lookup则查找RMI服务名绑定的接口对象,序列化该对象并通过RemoteCall传输到客户端。
  • RMI客户端反序列化服务端结果,获取远程对象的引用
  • RMI客户端调用远程方法,RMI服务端反射调用RMI服务实现类的对应方法并序列化执行结果返回给客户端。
  • RMI客户端反序列化RMI远程方法调用结果。

RMI Registry就像⼀个⽹关,他⾃⼰是不会执⾏远程⽅法的,但RMI Server可以在上⾯注册⼀个Name 到对象的绑定关系;RMI Client通过NameRMI Registry查询,得到这个绑定关系,然后再连接RMI Server;最后,远程⽅法实际上在RMI Server上调⽤。

Remote Method Invocation

远程对象

使用远程方法调用,必然会涉及参数的传递和执行结果的返回。参数或者返回值可以是基本数据类型,当然也有可能是对象的引用。所以这些需要被传输的对象必须可以被序列化,这要求相应的类必须实现java.io.Serializable接口,并且客户端的serialVersionUID字段要与服务器端保持一致。

任何可以被远程调用方法的对象必须继承java.rmi.Remote接口,远程对象的实现类必须继承UnicastRemoteObject类(这个类用于实现远程对象。一个类通过继承UnicastRemoteObject并实现一个或多个远程接口,可以成为一个远程服务对象,能够接收远程方法调用。)。如果不继承UnicastRemoteObject类,则需要手工初始化远程对象,在远程对象的构造方法的调用UnicastRemoteObject.exportObject()静态方法,如下:

java.rmi.RemoteExceptionRemoteException是一个受检查的异常,表示在RMI调用过程中可能发生的异常情况;当远程方法抛出RemoteException时它会被传递给客户端,以便客户端可以捕获处理该异常。在RMI中,远程接口的方法必须声明 可能抛出RemoteException

java.rmi.server.UnicastRemoteObjectUnicastRemoteObject是实现远程对象的基类,它提供了将对象导出为远程对象的功能。当一个类继承自UnicastRemoteObject并实现了一个远程接口时,该类的实例可以被注册和访问作为远程对象。

继承UnicastRemoteObject的时候构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package learn.rmi;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class RMIServer {

public class RMIHello extends UnicastRemoteObject implements IHello{
//定义了一个名为 RMIHello 的类,该类继承自 UnicastRemoteObject 类,并实现了 IHello 接口。
protected RMIHello() throws RemoteException{
super();
//定义了一个受保护的构造函数 RMIHello(),它声明可能抛出 RemoteException 异常,通过调用父类的UnicastRemoteObject的无参构造函数来实例化 RMIHello 对象。
}
@Override
public String sayHello(String name) throws RemoteException {
System.out.println("Hello World!");
return "hey";
//实现了 IHello 接口中的 sayHello 方法。该方法接收一个 name 参数,并返回一个字符串。在方法体内,它打印出 "Hello World!" 消息,并返回字符串 "hey"。
}
}

}

tips

此时上述代码定义了一个RMIHello类为远程对象,此时该远程对象继承了UnicastRemoteObject类并实现了IHello类。此时它的构造函数用于初始化对象并提供一个打印功能然后返回字符串。

没有继承UnicastRemoteObject的时候构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package learn.rmi;

import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIServer {
public static void main(String[] args) {
try {
IHello remoteObj = new RMIHello(); // 创建远程对象
IHello stub = (IHello) UnicastRemoteObject.exportObject(remoteObj, 0); // 导出远程对象
Registry registry = LocateRegistry.getRegistry(); // 获取 RMI 注册表
registry.bind("Hello", stub); // 将远程对象注册到 RMI 注册表中
System.out.println("Server ready!");
} catch (Exception e) {
System.err.println("Server exception: " + e.toString());
e.printStackTrace();
}
}
}

class RMIHello implements IHello{
protected RMIHello() throws RemoteException{
// 在没有继承UnicastRemoteObject的时候构造函数也可以写成如下形式
UnicastRemoteObject.exportObject(this,0);
}

@Override
public String sayHello(String name) throws RemoteException {
System.out.println("Hello World!");
return "hey";
}
}

IHello.java

1
2
3
4
5
6
7
8
9
10
package learn.rmi;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface IHello extends Remote {
//定义了一个接口 IHello,它扩展了 Remote 接口。通过扩展 Remote 接口,表明该接口是一个 RMI 远程接口。
public String sayHello(String name) throws RemoteException;
//在接口中定义了一个方法 sayHello,它接受一个字符串参数 name,并返回一个字符串。方法声明中使用了 throws RemoteException,表示该方法可能抛出 RemoteException 异常,这是 RMI 中常见的远程调用异常。
}

tips

这段代码定义了一个RMI的远程接口IHello,其中包含了一个方法sayHello,客户端可以通过该方法向远程对象发送消息并获取返回接口。

IHello是客户端和服务端共用的接口(客户端本地必须有远程对象的接口,不然无法指定要调用的方法,而且其全限定名必须与服务器上的对象完全相同),RMIHello是一个服务端远程对象,提供了一个sayHello方法供远程调用。

以上就构成了RMI Server

  • 一个继承了java.rmi.Remote的接口IHello,内部定义了我们将要远程调用的对象方法sayHello()
  • 一个实现了此接口的类RMIHello
  • 一个主类RMIServer,用来创建Registry,并将类RMIHello实例化后绑定到一个地址

对象调用过程

本地对象的调用

1
2
ObjectClass objectA = new ObjectClass();
String retn = objectA.Method();

远程对象的调用

但是如果对象在JVM A上,而客户端在JVM B上;那么此时B如何访问A的对象呢?此时就需要利用到RMI机制了

在JVM之间通信时,RMI对远程对象和非远程对象的处理方式是不一样的,它并没有直接把远程对象复制一份传递给客户端,而是传递了一个远程对象的Stub(存根),Stub相当于远程对象的引用或者代理。Stub对开发者是透明的,客户端可以像调用本地方法一样直接通过它来调用远程方法。Stub中包含了远程对象的定位信息,如Socket端口、服务端主机地址等等,并实现了远程调用过程中具体的底层网络通信细节。而位于服务器端的Skeleton(骨架),能够读取客户端传递的方法参数,调用服务器方的实际对象方法, 并接收方法执行后的返回值。所以RMI远程调用逻辑大致是这样的

从逻辑上来看,数据是在Client和Server之间横向流动的,但是实际上是从Client到Stub,然后从Skeleton到Server这样纵向流动的

具体的通信流程如下

  • Server监听一个端口,这个端口是JVM随机选择的
  • Client并不知道Server远程对象的通信地址和端口,但是位于Client的Stub中包含了这些信息,并封装了底层网络操作。Client可以调用Stub上的方法,并且也可以向Stub发送方法参数。
  • Stub连接到Server监听的通信端口并提交参数
  • Server执行具体的方法,并将结果返回给Stub
  • Stub返回执行结果给Client。因此在Clinet看来,就好像是Stub在本地执行了这个方法。

那么问题来了,位于Client上的Stub是怎么获取到远程Server的通信信息的呢?这就需要使用RMI Registry了。

RMI Registry

RMI Registry的注册

JDK提供了一个RMI注册表(RMI Registry)来解决这个问题。RMI Registry也是一个远程对象,默认监听在1099端口上,可以使用代码启动RMI Registry,也可以使用rmiregistry命令。

要注册远程对象,需要RMI URL和一个远程对象的引用

1
2
3
4
5
6
7
8
9
private void register() throws Exception{
//这是一个私有方法 register(),声明了一个可能抛出异常的方法,需要在调用处进行异常处理或继续抛出。
RMIHello rmiHello=new RMIHello();
//创建一个 RMIHello 的实例对象 rmiHello。RMIHello 是一个远程对象,通过此实例可以在远程上提供服务。
LocateRegistry.createRegistry(1099);
//在本地创建一个 RMI 注册表,使用默认端口 1099。RMI 注册表用于存储远程对象的引用,客户端可以通过指定名称来查找和访问这些远程对象。
Naming.bind("rmi://127.0.0.1:1099/hello",rmiHello);
//通过 Naming.bind() 方法将远程对象 rmiHello 绑定到指定的 RMI URL 上。这里的 URL 是 "rmi://127.0.0.1:1099/hello",表示绑定到本地 IP 地址为 127.0.0.1,端口为 1099 的 RMI 注册表上,并使用名称 "hello" 标识该远程对象。绑定后,客户端可以使用该 URL 和名称来查找并访问远程对象。
}

在主类的register()方法中,我们首先实例化了一个将被远程调用的类RMIHello,然后使用 LocateRegistry.createRegistry(port)在本地的某个端口上创建了一个Registry。最后使用Naming.bind()将实例化对象和地址上的hello绑定在一起,作为远程对象的名字。注意这里使用的是rmi://协议。这样,我们就完成了对RMI Registry的注册。

RMI Registry的使用

注册完RMI Registry以后,我们将要调用的远程对象已经和服务器端的某个地址绑定在了一起。那么Clinet又是怎么从Registry获取服务器远程对象信息的呢?我们创建一个简单的RMI Client,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package learn.rmi;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIClient {
public static void main(String[] args) throws Exception{
Registry registry= LocateRegistry.getRegistry("127.0.0.1",1099);
IHello iHello=(IHello) registry.lookup("hello");
//通过 registry.lookup() 方法查找指定名称为 "hello" 的远程对象,并将其转换为 IHello 接口类型的引用。这里假设已经在远程注册表上绑定了名称为 "hello" 的远程对象。
System.out.println(iHello.sayHello("hey"));
//调用 iHello 引用的远程对象的 sayHello() 方法,传入字符串参数 "hey",并将返回结果打印输出。
}
}

LocateRegistry.getRegistry()会使用给定的主机和端口等信息在本地创建一个Stub对象作为Registry远程对象的代理,从而启动整个远程调用逻辑。服务端应用程序可以向RMI注册表中注册远程对象,然后客户端向RMI注册表查询某个远程对象名称,来获取该远程对象的Stub。这里我们使用了registry.lookup()来查询获取注册表中的远程对象。还有另一种写法:

1
2
3
4
5
public static void main(String[] args) throws Exception{

System.out.println(iHello.sayHello("hhheey"));
}
}

使用了RMI Registry后,RMI的调用关系如下:

RMI的简单实现

Server

一个RMIServer分为三部分

  • 一个继承了java.rmi.Remote接口,其中定义了我们要远程调用的函数,比如在这个例子中的hello()
  • 一个实现接口的类
  • 一个主类,用来创建Registry,并将上面的类实例化后绑定到一个地址,这也就是我们所谓的Server

接口实现

首先是接口实现并继承Remote;并在里面创建一个hello()方法

1
2
3
public interface IRemoteHelloWorld extends Remote{
public String hello() throws RemoteException;
}

创建类用于远程调用

然后创建一个类去实现这个接口用于远程调用;继承UnicastRemoteObject类后会使用默认socket进行通讯,并且该类会一直运行在服务器上。如果不继承UnicastRemoteObject类,则需要手工初始化对象,在远程对象的构造方法的调用UnicasrReomteObject.exportObject()静态方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.example.RMI;  //包声明

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject; //导入必需的类;UnicastRemoteObject:用于导出远程对象并使它们能够接收远程方法调用的类

public class RMIServer extends UnicastRemoteObject implements IRemoteHelloWorld{

protected RMIServer() throws RemoteException{
super();
}
@Override
public String hello() throws RemoteException{
return "hello world!";
}
}

此时我们将重点放在类的继承和接口的实现

此时RMIServer类继承了UnicastRemoteObject使得该类称为远程对象,能够接受远程调用;并且此时实现了IRemoteHelloWorld接口;这个接口继承java.rmi.remote,定义了可以远程调用的方法。这意味着RMIServer必需提供IRemoteHelloWorld接口的中所声明的方法的实现

接下来把目光放到实现远程方法

在这里RMIServer实现了IRemoteHelloWorld接口的hello()方法;当客户端调用这个方法时会返回hello world。方法使用@Override注解标记,这表明该方法覆盖或实现了父类或接口中的方法。

实现调用

现在可以被远程调用的对象创建好了,接下来就是考虑如何调用了。

Java RMI中设计了一个Registry的思想,很好理解我们可以使用注册表来查找一个远程对象的使用。通俗来说这就是一个RMI电话本,当我们想在某个人那里获取信息时(Remote Method Invocation)就在电话本Registry中通过这个人的名称Name中来找到这个人的电话Reference,并通过这个电话找到这个人Remote Object

这种电话本的思想,由 java.rmi.registry.Registryjava.rmi.Naming 来实现。这里分别来说说这两个东西。

java.rmi.Naming

java.rmi.Naming提供了在远程注册表(Registry)中存储和获取远程对象的方法。这个类提供的每个方法都有一个 URL 格式的参数,格式如下: //host:port/name

  • host 表示注册表所在的主机
  • port 表示注册表接受调用的端口号,默认为 1099
  • name 表示一个注册 Remote Object 的引用的名称,不能是注册表中的一些关键字

那么这样就好理解了,我们现在实现了服务端待调用的对象,现在我们要把他装载进“电话本”,也就是注册(registry)

注册

  1. 利用LocateRegistry.createRegistry(1099);创建注册中心
  2. 实例化远程对象
  3. 把这个实例化对象绑定name存入“电话本”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.example.RMI;

import java.net.MalformedURLException;
import java.rmi.AlreadyBoundException;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;

public class RemoteServer {
public static void main(String[] args) throws RemoteException, MalformedURLException, AlreadyBoundException {
//创建注册中心
LocateRegistry.createRegistry(1099);
//创建远程对象
RMIObject rmiObject = new RMIObject();
// 绑定name
Naming.bind("rmi://localhost:1099/Hello", rmiObject);

}
}

服务器端完整代码

IHello.java

1
2
3
4
5
6
7
8
package learn.rmi;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface IHello extends Remote {
public String sayHello(String name) throws RemoteException;
}

RMIServer.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package learn.rmi;

import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;

public class RMIServer {

public class RMIHello extends UnicastRemoteObject implements IHello {
protected RMIHello() throws RemoteException{
super();
}

@Override
public String sayHello(String name) throws RemoteException {
System.out.println("Hello World!");
return name;
}
}

private void register() throws Exception{
RMIHello rmiHello=new RMIHello();
LocateRegistry.createRegistry(1099);
Naming.bind("rmi://0.0.0.0:1099/hello",rmiHello);
System.out.println("Registry运行中......");
}

public static void main(String[] args) throws Exception {
new RMIServer().register();
}
}

Client

  1. 使用NamingRegistry中寻找到名字是Hello的对象
  2. 调用远程对象的方法属性

Naming有很多种方法

这里用lookup来测试,他返回:a reference for a remote object,远程对象的引用

  • Naming.lookup()调用检查在 localhost 中运行的 RMI 注册表中是否存在名为“Hello”的绑定
  • 它返回一个必须转换为我期望的任何远程接口的对象
  • 然后就可以使用该对象调用接口中定义的远程方法

客户端完整代码

IHello.java

1
2
3
4
5
6
7
8
package learn.rmi;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface IHello extends Remote {
public String sayHello(String name) throws RemoteException;
}

RMIClient.java

1
2
3
4
5
6
7
8
9
10
11
12
package learn.rmi;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIClient {
public static void main(String[] args) throws Exception{
Registry registry= LocateRegistry.getRegistry("127.0.0.1",1099);
IHello iHello=(IHello) registry.lookup("hello");
System.out.println(iHello.sayHello("hhheey"));
}
}

JRMP协议分析

Java远程方法协议(Java Remote Method Protocol,JRMP)特定于Java技术的、用于查找和引用远程对象的协议。这是运行在Java远程方法调用(RMI)之下、TCP/IP之上的线路层协议。

第一条TCP链接

首先是TCP三次握手来建立第一条TCP链接,客户端连接服务器的1099端口,这里真正连接到的其实是RMI Registry,然后二者建立JRMP链接

随后ClinetRegistry发送”Call”信息,Registry回复”ReturnData”。我们看一下Registry的回复内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
0000   00 0c 29 b3 84 37 14 18 c3 e1 a9 29 08 00 45 00  ..)..7.....)..E.
0010 01 62 3d 67 40 00 80 06 00 00 c0 a8 2b a2 c0 a8 .b=g@.......+...
0020 2b 0a 04 4b b5 d0 a1 2c d2 91 8b 75 e2 86 50 18 +..K...,...u..P.
0030 08 04 d9 51 00 00 51 ac ed 00 05 77 0f 01 82 3c ...Q..Q....w...<
0040 5d f8 00 00 01 7e 4c 4d 6c e9 80 05 73 7d 00 00 ]....~LMl...s}..
0050 00 02 00 0f 6a 61 76 61 2e 72 6d 69 2e 52 65 6d ....java.rmi.Rem
0060 6f 74 65 00 10 6c 65 61 72 6e 2e 72 6d 69 2e 49 ote..learn.rmi.I
0070 48 65 6c 6c 6f 70 78 72 00 17 6a 61 76 61 2e 6c Hellopxr..java.l
0080 61 6e 67 2e 72 65 66 6c 65 63 74 2e 50 72 6f 78 ang.reflect.Prox
0090 79 e1 27 da 20 cc 10 43 cb 02 00 01 4c 00 01 68 y.'. ..C....L..h
00a0 74 00 25 4c 6a 61 76 61 2f 6c 61 6e 67 2f 72 65 t.%Ljava/lang/re
00b0 66 6c 65 63 74 2f 49 6e 76 6f 63 61 74 69 6f 6e flect/Invocation
00c0 48 61 6e 64 6c 65 72 3b 70 78 70 73 72 00 2d 6a Handler;pxpsr.-j
00d0 61 76 61 2e 72 6d 69 2e 73 65 72 76 65 72 2e 52 ava.rmi.server.R
00e0 65 6d 6f 74 65 4f 62 6a 65 63 74 49 6e 76 6f 63 emoteObjectInvoc
00f0 61 74 69 6f 6e 48 61 6e 64 6c 65 72 00 00 00 00 ationHandler....
0100 00 00 00 02 02 00 00 70 78 72 00 1c 6a 61 76 61 .......pxr..java
0110 2e 72 6d 69 2e 73 65 72 76 65 72 2e 52 65 6d 6f .rmi.server.Remo
0120 74 65 4f 62 6a 65 63 74 d3 61 b4 91 0c 61 33 1e teObject.a...a3.
0130 03 00 00 70 78 70 77 37 00 0a 55 6e 69 63 61 73 ...pxpw7..Unicas
0140 74 52 65 66 00 0e 31 39 32 2e 31 36 38 2e 34 33 tRef..192.168.43
0150 2e 31 36 32 00 00 ec 3c ba 3f a1 47 ea 85 db bb .162...<.?.G....
0160 82 3c 5d f8 00 00 01 7e 4c 4d 6c e9 80 01 01 78 .<]....~LMl....x

这里传输的是服务器的序列化数据。注意以上加粗倾斜的部分。\xAC\xEDJava序列化的魔术头,该数据流往后的部分就是序列化的内容了。\xEC\x3C转换成十进制为60476,这便是Server在本地开放的随机端口。

我们分析一下第一条TCP链接干了什么。首先Client根据传入的rmi地址链接远端服务器1099端口上的RMI Registry,然后RegistryClient发送Server上的序列化数据,包括IP和开放的随机端口等。

第二条TCP链接

再往下是第二个TCP链接Client连接ReturnData中返回的端口,这条TCP链接用于Client与Server之间的传输数据。实际上是Client的Stub和Server上的Skeleton之间进行数据传输的。

两条TCP链接分别断开

再往后就是四次挥手,两条TCP链接分别断开

RMI Registry就像一个网关,Server在Registry中注册绑定在name上的远程对象,Client在Registry中根据name查询远程对象绑定信息。然后Client的Stub连接位于Server上的Skeleton,最终远程方法还是在服务器上执行。