一.相关概念
1. RPC
RPC(Remote Procedure Call,远程过程调用)本质是一套 "简化分布式应用开发" 的程序设计模式。目标是让开发者能够像调用本地方法一样调用远程计算机上的函数或方法,而无需关心底层网络细节。它屏蔽了网络传输、序列化等复杂过程,使得分布式应用的开发更加简单和透明。
核心机制:
-
通信机制:基于 TCP 或 UDP 协议,在客户端与服务器之间建立连接,所有远程调用数据都通过该连接传输。
-
统一接口:客户端和服务器使用相同的接口定义,使得远程调用在形式上与本地调用一致。
-
序列化与反序列化:调用参数和返回值需要在网络中以二进制形式传输。客户端将参数序列化为二进制流发送给服务器,服务器接收后反序列化并执行方法,再将结果序列化返回给客户端。
常见的 RPC 实现包括 CORBA、Java RMI、Dubbo、Web Service 等。
2. RMI
RMI(Remote Method Invocation,远程方法调用)是 RPC 的 "Java 定制版"------ 它完全遵循 RPC 的思想,但专门针对 Java 的面向对象特性设计,底层默认用 TCP 协议传输
RMI 的目的在于对开发人员屏蔽横跨不同 JVM 和网络连接等细节,使得分布在不同 JVM 上的对象像是存在于一个统一的 JVM 中一样,客户端调用远程服务方法,使用起来就好像调用本地方法。有一些用 Java 实现的 RPC 框架底层也使用了 RMI 技术。
实现原理:
RMI 为客户端 B 的对象和远程服务端 A 的对象分别提供了客户端辅助对象 (也称为客户端 stub)和远程服务辅助对象(服务端 stub,也称为 skeleton),来帮助本地客户端的对象真正和远程服务对象进行沟通。
RMI 在客户端 B 为客户端 stub 创建和远程服务对象相同的方法。客户端调用客户端 stub 上的方法,仿佛客户端 stub 就是真正的服务,客户端 stub 再负责为我们转发这些请求。客户端 stub 会联系服务器 A,传送方法调用信息(例如方法名称、变量等),然后等待服务器 A 对应远程方法的返回。
在服务器端 A,服务端 stub 通过 Socket 连接从客户端 stub 中接收请求,将调用的信息解包,然后调用真正服务对象上的同名方法。所以对于提供远程服务的服务对象来说,调用是本地的,来自服务端 stub,而不是远程客户。服务端 stub 从服务中得到返回值,将它打包,然后传送回到客户端 stub(通过网络 Socket 的输出流),客户端 stub 对信息解包,最后将返回值交给客户对象。
RMI 的优点在于我们不必亲自写任何网络或 I/O 代码(Java 1.5 后,也不需要用户来手动生成客户端和服务端 stub,完全被隐藏了细节)。客户程序感觉不到调用远程方法,其调用过程就好像是在调用自己的本地对象的方法。整个过程如示意图 12.1 所示,图中 1~10 序号表示一个完整的远程方法调用的执行顺序。

注意:
上面的示意图是 A 为 B 提供远程服务。其实远程对象是个相对的概念,不仅可以位于 A 处,也可以位于 B 处,这时客户端 B 也可以为服务器端 A 提供远程方法调用,这种调用过程被称为回调。在这一刻,B 和 A 的角色就好像是临时互换了(B 充当了客户端角色,而 A 就充当了远程服务提供者),所以服务器端和客户端是相对的。只是我们一般约定一直监听的那端为服务器端。
二、RMI 程序的实现
我们按以下步骤来创建符合 RMI 规范的程序:
1. 创建远程接口
在远程接口中定义的方法就是可以被客户端远程调用的方法。任何远程接口都要满足以下四个要求:
(1) 直接或间接继承 `java.rmi.Remote` 接口;
(2) 接口中定义的方法都要声明抛出 `RemoteException` 异常;
(3)远程方法的参数和返回值必须属于基本类型,或实现了 `Serializable` 接口的引用类型,或者是远程接口类型;
(4) 服务器端和客户端都必须拥有完全相同的远程接口,包括包名、接口名和方法定义。
示例:创建远程接口 HelloService
java
package rmi;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.Date;
public interface HelloService extends Remote {
public String echo(String msg) throws RemoteException;
public Date getTime() throws RemoteException;
public ArrayList<Integer> sort(ArrayList<Integer> list) throws RemoteException;
}
2. 创建服务端的远程接口实现类
远程接口实现类必须实现远程接口,并通常扩展 `java.rmi.server.UnicastRemoteObject` 类,以自动获得远程对象功能。构造方法必须声明抛出 `RemoteException`。
示例:创建远程服务类 HelloServiceImpl
java
package chapter12.server;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
public class HelloServiceImpl extends UnicastRemoteObject
implements HelloService {
private String name;
public HelloServiceImpl() throws RemoteException {
}
public HelloServiceImpl(String name) throws RemoteException {
this.name = name;
}
@Override
public String echo(String msg) throws RemoteException {
System.out.println("服务端完成一些 echo 方法相关任务......");
return "echo: " + msg + " from " + name;
}
@Override
public Date getTime() throws RemoteException {
System.out.println("服务端完成一些 getTime 方法相关任务......");
return new Date();
}
@Override
public ArrayList<Integer> sort(ArrayList<Integer> list) throws RemoteException {
System.out.println("服务端完成排序相关任务......");
Collections.sort(list);
return list;
}
}
3. 创建远程服务发布程序
远程服务发布程序负责启动 RMI 注册器,并将远程对象注册到注册器中,以便客户端可以通过名称查找和调用。
示例:创建服务发布程序 HelloServer
java
package chapter12.server;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class HelloServer {
public static void main(String[] args) {
try {
// 绑定固定 IP,避免多网卡问题
System.setProperty("java.rmi.server.hostname", "本机器的 IP 地址");
// 启动 RMI 注册器,监听 1099 端口
Registry registry = LocateRegistry.createRegistry(1099);
// 实例化远程服务对象
HelloService helloService = new HelloServiceImpl("张三的远程服务");
// 注册远程服务对象,使用助记符 "HelloService"
registry.rebind("HelloService", helloService);
System.out.println("发布了一个 HelloService RMI 远程服务");
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
4. 创建客户端程序
客户端通过 RMI 注册器查找远程服务,获取客户端存根(Stub),然后像调用本地方法一样调用远程方法。
示例:创建客户端程序 HelloClientFX
java
package chapter12.client;
// 引入必要的 JavaFX 和 RMI 包
public class HelloClientFX extends Application {
private TextArea taDisplay = new TextArea();
private TextField tfMessage = new TextField();
Button btnEcho = new Button("调用 echo 方法");
Button btnGetTime = new Button("调用 getTime 方法");
Button btnListSort = new Button("调用 sort 方法");
// 客户端也有一份和服务端相同的远程接口
private HelloService helloService;
@Override
public void start(Stage primaryStage) {
// 界面初始化代码...
new Thread(() -> { rmiInit(); }).start();
// 按钮事件绑定
btnEcho.setOnAction(event -> {
try {
String msg = tfMessage.getText();
taDisplay.appendText(helloService.echo(msg) + "\n");
} catch (RemoteException e) {
e.printStackTrace();
}
});
// 其他按钮事件...
}
/**
* 初始化 RMI 相关操作
*/
public void rmiInit() {
try {
// 获取 RMI 注册器
Registry registry = LocateRegistry.getRegistry("服务端的 IP 地址", 1099);
// 查找远程服务,获取客户端 Stub
helloService = (HelloService) registry.lookup("HelloService");
} catch (Exception e) {
e.printStackTrace();
}
}
}
5. 程序运行
-
首先运行服务端的HelloServer ,启动 RMI 注册器并发布服务;
-
然后运行客户端 HelloClientFX,进行远程方法调用。
说明:在实际应用中,服务端和客户端通常部署在不同主机上,需确保两端的远程接口完全一致,包括包结构和接口定义。
三. 实验代码
包rmi
1.接口RmiKitService
java
package rmi;
import java.rmi.Remote;
import java.rmi.RemoteException;
/**
* 教师端和学生端各自一份的远程服务接口,定义实用的远程抽象方法
* 这些远程抽象方法全部由学生端实现,教师端进行调用
*/
public interface RmiKitService extends Remote {
/**
* 将"-"分隔的MAC地址逆序转换为":"分隔的MAC地址
* 示例:输入16-AC-60-6E-39-AF,输出AF:39:6E:60:AC:16
* @param mac 待转换的"-"分隔MAC地址
* @return 转换后":"分隔的MAC地址,若输入异常则返回错误提示
* @throws RemoteException 远程调用过程中发生网络或通信异常时抛出
*/
public String convertMacAddress(String mac) throws RemoteException;
}
2.接口RmiMsgService
java
package rmi;
import java.rmi.Remote;
import java.rmi.RemoteException;
/**
* 教师端和学生端各自一份的远程服务接口,定义学生端可调用的远程方法
* 所有远程方法由教师端实现,学生端通过RMI远程调用这些方法完成计分任务
*/
public interface RmiMsgService extends Remote {
/**
* 远程方法一:学生端向教师端发送通用信息
* 可用于发送测试信息或任务指令(如"学号&我的RMI服务器已经启动")
* @param msg 学生端发送的信息内容
* @return 教师端返回的响应信息(如测试结果、分数登记提示)
* @throws RemoteException 远程调用过程中发生网络故障、通信中断等异常时抛出
* (符合RMI规范:所有远程方法必须声明此异常)
*/
String send(String msg) throws RemoteException;
/**
* 远程方法二:学生端向教师端发送学号和姓名
* 用于完成课堂计分任务一,调用成功后教师端返回2分登记信息
* @param yourNo 学生学号(唯一标识)
* @param yourName 学生姓名
* @return 教师端返回的响应信息(如"学生已经完成第12讲1题任务,已经成功登记该题2分")
* @throws RemoteException 远程调用过程中发生异常时抛出
*/
String send(String yourNo, String yourName) throws RemoteException;
}
包chapter12
1.RmiStudentServer
java
package chapter12;
import rmi.RmiKitService;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
/**
* 学生端远程服务发布程序:注册RmiKitService服务,供教师端回调
*/
public class RmiStudentServer {
public static void main(String[] args) {
try {
// 1. 绑定固定IP(解决多网卡/虚拟网卡导致的IP访问错误,替换为自己机器的实际IP)
System.setProperty("java.rmi.server.hostname", "192.168.22.32");
// 2. 启动RMI注册器,监听默认端口1099
Registry registry = LocateRegistry.createRegistry(1099);
System.out.println("RMI注册器已启动,监听端口1099");
// 3. 实例化远程服务实现类(RmiKitService接口的实现)
RmiKitService rmiKitService = new RmiKitServiceImpl();
// 4. 注册远程服务,助记符必须为"RmiKitService"(教师端约定检索名称)
registry.rebind("RmiKitService", rmiKitService);
System.out.println("学生端RmiKitService远程服务发布成功,等待教师端回调...");
} catch (RemoteException e) {
System.err.println("远程服务发布失败:");
e.printStackTrace();
} catch (Exception e) {
System.err.println("程序异常:");
e.printStackTrace();
}
}
}
2.RmiKitServiceImpl
java
package chapter12;
import rmi.RmiKitService;
import java.rmi.RemoteException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* RmiKitService远程接口的实现类
* 继承UnicastRemoteObject以获得远程服务能力,实现接口中定义的MAC地址转换方法
*/
public class RmiKitServiceImpl extends java.rmi.server.UnicastRemoteObject implements RmiKitService {
/**
* 无参构造方法,必须声明抛出RemoteException
* 原因:父类UnicastRemoteObject的构造方法会抛出RemoteException,子类需显式声明
* @throws RemoteException 构造远程对象过程中发生异常时抛出
*/
protected RmiKitServiceImpl() throws RemoteException {
super();
}
/**
* 实现MAC地址转换逻辑:"-"分隔 → 片段逆序 → ":"分隔
* @param mac 待转换的MAC地址字符串(格式要求:6段,每段2个字符,用"-"分隔)
* @return 转换后的MAC地址(":"分隔),若输入格式错误则返回具体错误信息
* @throws RemoteException 远程调用过程中发生网络异常时抛出
*/
@Override
public String convertMacAddress(String mac) throws RemoteException {
// 1. 校验输入是否为空
if (mac == null || mac.trim().isEmpty()) {
return "错误:MAC地址不能为空!";
}
// 2. 按"-"分割MAC地址片段,校验片段数量是否为6(标准MAC地址格式)
String[] macSegments = mac.split("-");
if (macSegments.length != 6) {
return "错误:MAC地址格式无效!需为6段,示例:16-AC-60-6E-39-AF";
}
// 3. 校验每段MAC片段是否为2个字符(避免非法片段,如空字符串、1个字符等)
for (String segment : macSegments) {
if (segment.length() != 2) {
return "错误:MAC地址片段[" + segment + "]无效!每段需为2个字符";
}
}
// 4. 将片段数组转为List,执行逆序操作
List<String> segmentList = Arrays.asList(macSegments);
Collections.reverse(segmentList);
// 5. 将逆序后的片段用":"连接,返回最终结果
return String.join(":", segmentList);
}
}
3.RmiStudentClientFX
java
package chapter12;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import rmi.RmiMsgService;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
/**
* 学生端客户端程序:调用教师端RmiMsgService远程服务,完成计分任务
*/
public class RmiStudentClientFX extends Application {
// 界面控件
private TextArea taDisplay = new TextArea();
private TextField tfMessage = new TextField(); // 输入通用信息
private TextField tfNO = new TextField(); // 输入学号
private TextField tfName = new TextField(); // 输入姓名
private Button btnSendMsg = new Button("发送信息");
private Button btnSendNoAndName = new Button("发送学号和姓名");
// 远程服务接口引用(教师端提供的RmiMsgService服务)
private RmiMsgService rmiMsgService;
// 教师端服务配置(固定为202.116.195.71:1099)
private static final String TEACHER_SERVER_IP = "202.116.195.71";
private static final int TEACHER_SERVER_PORT = 1099;
private static final String SERVICE_ALIAS = "RmiMsgService"; // 教师端服务助记符
public static void main(String[] args) {
launch(args); // 启动JavaFX应用
}
@Override
public void start(Stage primaryStage) {
// 1. 初始化界面布局
initUI(primaryStage);
// 2. 启动线程初始化RMI连接(避免阻塞UI线程)
new Thread(this::rmiInit).start();
// 3. 绑定按钮事件
bindButtonEvents();
}
/**
* 初始化界面布局
*/
private void initUI(Stage primaryStage) {
// 主容器:垂直布局
VBox vBoxMain = new VBox();
vBoxMain.setSpacing(15);
vBoxMain.setPadding(new Insets(20));
vBoxMain.setAlignment(Pos.TOP_LEFT);
// 信息显示区配置
taDisplay.setPrefHeight(300);
taDisplay.setPrefWidth(600);
taDisplay.setEditable(false); // 禁止编辑,仅用于显示
// 输入区:水平布局(通用信息输入)
HBox hBoxMsg = new HBox();
hBoxMsg.setSpacing(10);
hBoxMsg.setAlignment(Pos.CENTER_LEFT);
hBoxMsg.getChildren().addAll(
new Label("输入信息:"),
tfMessage,
btnSendMsg
);
// 学号姓名输入区:水平布局
HBox hBoxNoName = new HBox();
hBoxNoName.setSpacing(10);
hBoxNoName.setAlignment(Pos.CENTER_LEFT);
hBoxNoName.getChildren().addAll(
new Label("学号:"),
tfNO,
new Label("姓名:"),
tfName,
btnSendNoAndName
);
// 组装主容器
vBoxMain.getChildren().addAll(
new Label("信息显示区:"),
taDisplay,
hBoxMsg,
hBoxNoName
);
// 设置场景和舞台
Scene scene = new Scene(vBoxMain, 700, 450);
primaryStage.setTitle("学生端RMI计分客户端");
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* 初始化RMI连接:获取教师端远程服务
*/
private void rmiInit() {
try {
// 1. 获取教师端RMI注册器
Registry registry = LocateRegistry.getRegistry(TEACHER_SERVER_IP, TEACHER_SERVER_PORT);
taDisplay.appendText("已连接教师端RMI注册器:" + TEACHER_SERVER_IP + ":" + TEACHER_SERVER_PORT + "\n");
// 2. 查找并获取远程服务(助记符必须与教师端一致)
rmiMsgService = (RmiMsgService) registry.lookup(SERVICE_ALIAS);
taDisplay.appendText("成功获取RmiMsgService远程服务,可开始发送请求...\n");
} catch (Exception e) {
taDisplay.appendText("RMI初始化失败:" + e.getMessage() + "\n");
e.printStackTrace();
}
}
/**
* 绑定按钮点击事件
*/
private void bindButtonEvents() {
// 1. "发送信息"按钮:调用教师端 send(String msg) 方法
btnSendMsg.setOnAction(event -> {
String msg = tfMessage.getText().trim();
if (msg.isEmpty()) {
taDisplay.appendText("提示:输入信息不能为空!\n");
return;
}
try {
// 调用远程服务,获取教师端返回结果
String result = rmiMsgService.send(msg);
taDisplay.appendText("From服务器:" + result + "\n");
tfMessage.clear(); // 清空输入框
} catch (Exception e) {
taDisplay.appendText("发送信息失败:" + e.getMessage() + "\n");
e.printStackTrace();
}
});
// 2. "发送学号和姓名"按钮:调用教师端 send(String yourNo, String yourName) 方法
btnSendNoAndName.setOnAction(event -> {
String no = tfNO.getText().trim();
String name = tfName.getText().trim();
if (no.isEmpty() || name.isEmpty()) {
taDisplay.appendText("提示:学号和姓名不能为空!\n");
return;
}
try {
// 调用远程服务,获取教师端返回结果
String result = rmiMsgService.send(no, name);
taDisplay.appendText("From服务器:" + result + "\n");
tfNO.clear(); // 清空学号输入框
tfName.clear(); // 清空姓名输入框
} catch (Exception e) {
taDisplay.appendText("发送学号姓名失败:" + e.getMessage() + "\n");
e.printStackTrace();
}
});
}
}
四. 实验步骤
1.运行RmiStudentServer
2.运行RmiStudentClientFX
3.在客户端程序中使用"发送信息"按钮发送一些测 试信息,看教师端是否返回测试通过的信息。
4.如果测试通过,则完成以下两个 课堂任务:
(1)文本框中分别输入学号和姓名,使用"**发送学号和姓名"**按钮进行发 送,如果调用成功,教师端会返回获得2 分的信息;
(2)输入信息的文本框中输入"学号&我的RMI服务器已经启动",例 如输入 20181111111&我的RMI 服务器已经启动,(上面的信息中没有空格, 注意不要直接复制上面的内容,可能会带入空格),然后再通过"发送信息" 按钮发送,教师端将调用你实现的远程方法,如果方法实现正确,教师端将返 回获得3 分的信息。
