尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Java RMI分布式实验:从零跑通远程调用最小闭环

Java RMI分布式实验:从零跑通远程调用最小闭环 简介华南理工大学分布式实验2是一份面向Java学习者的RMI远程调用实验完整方案适用于高校“分布式系统”或“Java网络编程”课程实践。实验围绕学生成绩或教师信息查询场景完整实现了远程接口定义、服务器端注册、客户端访问等核心步骤帮助读者掌握RMI的基本原理与编码流程。资源严格按照实验要求编写包含服务器端与客户端完整工程可直接用Eclipse或IDEA导入运行方便快速上手。压缩包共16个文件大小仅2.56MB包含6个Java源文件、5个class文件、2个jar依赖包以及doc说明文档、sql数据库脚本和txt文本说明结构紧凑、类型齐全便于对照学习与直接部署。资源还提供了MySQL连接驱动jar包省去额外配置依赖的麻烦。已有365人下载学习适合需要快速完成实验或深入理解RMI调用机制的读者。通过阅读源码、运行示例和查看文档可以直观体会远程对象绑定、注册中心交互以及客户端查找调用的完整链路。1. RMI 分布式实验一个能跑通的远程调用最小闭环如果你刚开始接触分布式开发第一个真正需要弄懂的概念往往不是分布式锁也不是分布式事务而是远程调用到底是怎么发生的。华南理工大学分布式实验2 给的正是这个切入点基于 Java 原生的 RMIRemote Method Invocation用最少的代码把 Client 和 Server 拆到两个进程里让客户端像调用本地方法一样调用服务端对象。这套实验资源在网络上流传的版本很多我拆的这份包含完整的实验步骤说明和可复现的代码骨架覆盖从javac编译到注册中心、服务端、客户端三者协同运行的完整链路。适合正在做课程实验、需要快速理解 RMI 机制、或者想补 Java 分布式基础的人。2. 实验原理与工程骨架先理解 RMI 在分布式实验里的位置2.1 为什么分布式实验2 选 RMI 而不是 WebService 或 Spring Cloud现在的教程生态里一提分布式就是 Spring Cloud、ZooKeeper、Redis 分布式锁很多同学甚至没写过一次原生的远程调用就上了微服务框架。RMI 在 Java 里是 1998 年就有的老技术但它把远程调用这件事的核心步骤暴露得很干净客户端持有 stub存根stub 通过网络把方法名和参数序列化后发给服务端服务端反序列化后调用真实对象的方法再把返回值序列化传回客户端。这整个流程是理解后续一切 RPC 框架的地基。RMI 相比 WebService 和 Spring Cloud 最大的优势是零依赖。JDK 自带的java.rmi.*包就把远程调用做完了不需要额外装 Tomcat、不需要写 WSDL、不需要引入 Spring 容器。对于分布式实验2 这种课时有限、重点在于理解远程调用机制的作业来说RMI 是最合适的最小闭环一个接口、一个实现类、一个客户端外加一个注册表四个角色就能完整演示分布式调用。这个实验和 Hadoop 伪分布式搭建也有一个共通点它们都依赖 Java 的序列化机制进行跨进程数据传输。Hadoop 的 DataNode 和 NameNode 之间的心跳、任务调度本质上也遵循着某个 JVM 里的对象被序列化后通过网络传输到另一个 JVM 里被还原这个逻辑。所以这份实验资源帮你建立的不是某个框架的具体用法而是一套在分布式环境里通用的传输思维。2.2 实验环境准备JDK、源码包和目录规划先说环境。这份实验用的是 JDK 自带的 RMI理论上 JDK 1.4 到 JDK 17 都能跑但我建议用 JDK 8 或 JDK 11 搭配对应版本的 javac。Java 11 之后rmic被移除但对这个实验没有影响因为 JDK 5 以后 RMI 支持动态 stub不再需要手动用rmic生成 stub 类。拿到资源包后的第一步是理解它的目录结构。我拆的这份是这样组织的路径角色关键内容src/server/服务端源码远程接口定义与实现类src/client/客户端源码远程调用入口README.md实验说明步骤讲解与截图指引docs/实验报告模板可直接改写的报告框架创建工程目录时我一般会这么做直接在终端里手动建不用 IDEmkdir -p rmi-lab/src/server mkdir -p rmi-lab/src/client mkdir -p rmi-lab/out/production cd rmi-lab javac -version这里-p参数的作用是递归创建多级目录避免一层层 mkdir。javac -version是为了先确认你的 JDK 版本符合预期避免后面编译时报出奇怪的 invalid flag 错误。如果你用的 IDE 是 IntelliJ IDEA也可以直接在工程里建两个 Module但实验报告要求的代码文件结构通常还是以这种扁平目录为准。启动顺序是这个实验里最容易踩坑的地方后文会专门展开。这里先把三个角色的关系记住注册中心Registry是整个调用的通讯录服务端把自己的远程对象注册到通讯录里客户端通过通讯录查到对象地址然后直接建立连接调用方法。三者缺一个都会让实验在启动阶段就失败。3. Server 端实现注册中心、远程接口与绑定细节3.1 远程接口的设计Remote 与 RemoteException 的规则RMI 远程接口的第一个规则是必须继承java.rmi.Remote。这个接口本身没有任何方法它起到的作用是给 JVM 一个标记告诉 RMI 运行时这个接口里声明的方法需要走网络调用流程。第二个规则是接口里每个方法都必须声明抛出java.rmi.RemoteException。这不是可有可无的装饰而是因为远程调用涉及网络连接、序列化、服务端异常包装这些异常无法像本地调用那样直接抛给调用方必须统一通过RemoteException传递。来看这份实验资源里服务端接口的标准写法// src/server/HelloService.java import java.rmi.Remote; import java.rmi.RemoteException; public interface HelloService extends Remote { // 远程方法必须声明 throws RemoteException String sayHello(String name) throws RemoteException; }我在实验里见过不少同学把RemoteException去掉只保留一个普通接口。这样编译时不会报错但Naming.rebind绑定的时候会抛出java.rmi.RemoteException: not a remote interface运行阶段才炸出来排查起来比编译期报错痛苦得多。所以记住这条硬规则接口继承Remote方法声明throws RemoteException。参数类型也有讲究。远程方法的参数和返回值必须能被序列化也就是实现java.io.Serializable否则跨 JVM 传输时序列化器会直接抛NotSerializableException。像String、Integer、ArrayList这些 JDK 自带类型天然满足条件但如果你在接口里定义了一个自定义的User对象作为参数就必须让User实现Serializable。这个坑在实验扩展场景里很常见后面避坑章节会再提。3.2 服务实现类UnicastRemoteObject 和 Registry 的交互接口定义完之后是服务实现类。RMI 的常用做法是让实现类继承UnicastRemoteObject这样在构造时 JVM 会自动导出远程对象并监听一个 TCP 端口。如果不继承这个类就需要手动调用UnicastRemoteObject.exportObject()来完成导出代码会多一步且容易遗漏。实现类需要同时被客户端引用所以它放哪个目录要看实验要求。我拆的这份资源里建议的做法是接口单独放在server目录客户端代码直接引用接口类型运行时通过 classpath 同时指向 server 和 client 的编译输出目录。// src/server/HelloServiceImpl.java import java.rmi.RemoteException; import java.rmi.server.UnicastRemoteObject; public class HelloServiceImpl extends UnicastRemoteObject implements HelloService { // 父类构造器会抛出 RemoteException子类必须显式声明 public HelloServiceImpl() throws RemoteException { super(); } Override public String sayHello(String name) throws RemoteException { System.out.println([Server] 收到来自客户端的调用: name); // 模拟一点耗时操作后面验证网络调用是否异步时有用 return Hello, name ! 来自远程服务器的响应; } }代码里有两个值得注意的细节。第一构造器必须声明throws RemoteException因为UnicastRemoteObject的构造器在导出对象时会创建 TCP socket 并绑定端口这个动作可能失败所以需要向上层抛出异常。第二sayHello方法里打印的这行日志非常重要它是在服务端 JVM 的终端里输出的不是客户端终端。如果你在客户端终端看到[Server]开头的内容说明你根本没有跑远程调用而是把实现类当成普通类在本地调用了。3.3 编译与启动服务端从 javac 到注册表绑定服务端代码写完后的编译和启动是整个实验里最容易卡住人的环节。我先给出完整的命令序列再逐条解释# 在工程根目录下执行 javac -d out/production src/server/*.java # 启动注册中心另开一个终端 rmiregistry 1099 # 再开一个终端启动服务端注意 classpath 要指向编译输出目录 java -cp out/production \ -Djava.rmi.server.codebasefile:./out/production \ -Djava.security.policysrc/server/policy.java \ server.Server第一条命令里的-d out/production指定编译输出目录这样HelloServiceImpl.class会被写到out/production/server/下。第二条命令启动rmiregistry默认端口是 1099这个程序是 JDK 自带的不需要写任何代码它只是维护一张服务名到远程对象引用的映射表。第三条命令启动服务端其中-Djava.rmi.server.codebase配置的是类文件所在的 URL 路径-Djava.security.policy指向安全策略文件。如果不配置java.security.policyJDK 8 及之前的版本会默认安装安全管理器并禁止远程加载类客户端会报AccessControlException: access denied。我拆的这份资源里自带了一个放开全部权限的策略文件内容一般是这样的:grant { permission java.security.AllPermission; };服务端主类的核心逻辑是创建注册表、实例化实现类、绑定服务名// src/server/Server.java import java.rmi.Naming; import java.rmi.registry.LocateRegistry; public class Server { public static void main(String[] args) { try { // 显式创建注册中心端口 1099 LocateRegistry.createRegistry(1099); HelloService service new HelloServiceImpl(); // 将服务绑定到注册中心 Naming.rebind(rmi://localhost:1099/HelloService, service); System.out.println([Server] 服务已注册到 rmiregistry); } catch (Exception e) { e.printStackTrace(); } } }这里rebind和bind的区别值得提一句。rebind会覆盖同名绑定重复执行不会报错bind遇到同名绑定会抛AlreadyBoundException。实验过程中服务端要反复重启用rebind可以省去清理注册表的麻烦。代码里指定了localhost作为注册中心地址如果服务端和客户端在两台机器上跑这里要改成服务端的 IP 地址。4. Client 端调用stub 获取、参数传递与一次完整运行4.1 客户端代码的骨架lookup 与类型转换客户端的核心动作是通过注册中心获取远程对象的引用然后调用方法。注意一点客户端拿到的并不是远程对象的真实字节码而是它的 stub 代理类。这个 stub 由 RMI 运行时动态生成内部维护着与远端 JVM 的 socket 连接对上层调用方是透明的。// src/client/Client.java import java.rmi.Naming; public class Client { public static void main(String[] args) { try { // 从注册中心获取远程对象引用stub HelloService service (HelloService) Naming.lookup( rmi://localhost:1099/HelloService ); // 调用远程方法和调用本地对象的方法没有区别 String response service.sayHello(张三); System.out.println([Client] 收到响应: response); } catch (Exception e) { System.err.println([Client] 远程调用失败: e.getMessage()); e.printStackTrace(); } } }这里的lookup方法返回的类型是Remote所以必须强转成HelloService。如果接口的包路径在客户端和服务端不一致这里会抛出ClassCastException。这是 RMI 实验中最隐蔽的错误之一因为编译期完全检查不出来。我拆的这份资源里把接口放在了server包下客户端导入的时候要确保import server.HelloService;和编译时 classpath 一致。调用远程方法和本地方法的体验差别几乎为零但这恰恰是 RMI 容易让人误解的地方你写的service.sayHello(张三)这行代码实际上经历了方法签名序列化 → TCP 传输 → 服务端反序列化 → 方法执行 → 返回值序列化 → 客户端反序列化一整条链路。如果在服务端方法里打印了日志你会在服务端终端看到调用记录这是判断远程调用是否真的发生的最直观证据。4.2 完整运行流程先注册表再 Server最后 Client实验报告里最容易失分的点不是代码写不出来而是启动顺序和配置方式没写对。RMI 的运行顺序有严格要求注册中心必须最先启动其次是服务端完成绑定最后才轮到客户端发起 lookup。如果在rmiregistry没启动时先跑 Server服务端会抛ConnectException: Connection refused to host: localhost。完整流程我按顺序列一遍# 1. 编译全部源码 javac -d out/production src/server/*.java src/client/*.java # 2. 终端 A启动注册中心 rmiregistry 1099 # 3. 终端 B启动服务端 java -cp out/production \ -Djava.rmi.server.codebasefile:./out/production \ -Djava.security.policysrc/server/policy.java \ server.Server # 4. 终端 C启动客户端 java -cp out/production client.Client第三步执行完后服务端终端会打印[Server] 服务已注册到 rmiregistry。这时注册中心已经记录了HelloService这个名字对应的远程对象地址。第四步执行客户端后客户端终端会打印[Client] 收到响应: Hello, 张三! 来自远程服务器的响应同时服务端终端会打印[Server] 收到来自客户端的调用: 张三。两个终端的日志要对照着看。只看到客户端打印响应还不能证明是远程调用因为客户端完全有可能只是调用了本地 classpath 里的某个实现类。只有当服务端同时打印出原始调用日志才能确认跨 JVM 通信真实发生。这是实验报告里实验结果与分析部分最有力的截图素材。4.3 验证一次远程调用是否真的走了网络每次实验我都会让学员做一个附加验证用lsof或netstat观察端口连接。在客户端运行期间检查客户端 JVM 是否与服务端 JVM 建立了 TCP 连接# 查看 1099 端口的连接状态 netstat -an | grep 1099 # 找到 java 进程的网络连接Linux / macOS lsof -iTCP -sTCP:ESTABLISHED | grep java正常情况下netstat的结果里会出现两条记录一条是客户端到服务端 1099 端口的连接用于注册中心查找另一条是客户端到服务端随机分配端口的连接用于实际的远程方法调用。第二条连接是 RMI 在 lookup 完成后自动建立的端口号是服务端导出的UnicastRemoteObject对象的监听端口不是 1099。这个做法的意义在于它把远程调用从一个抽象概念变成了可视化证据。你在实验报告里贴出netstat截图再配上服务端和客户端的日志截图整条通信链路的正确性就非常直观了。尤其当客户端出现调用成功但收不到响应的情况时通过netstat观察连接状态能快速定位到是网络半开还是注册中心查询失败。从那次以后我每次跑 RMI 实验都会强制开一个终端专门执行netstat监控这个习惯一直保留到了后面的 Hadoop 伪分布式实验。5. 避坑记录RMI 实验里最常见的五个翻车现场5.1 客户端报 AccessControlException: access denied现象客户端或服务端启动时报java.security.AccessControlException: access denied (java.net.SocketPermission localhost:1099 connect,resolve)程序直接退出。原因JDK 默认在存在安全管理器时执行严格的安全策略而 RMI 的远程调用需要建立 socket 连接和监听端口这些操作被默认策略禁止。解决在服务端启动命令中显式指定宽松的安全策略文件内容为grant { permission java.security.AllPermission; };。注意是服务端需要这个配置客户端通常不需要。我拆的这份实验资源里src/server/policy.java就是干这个用的如果没带这个文件用上面这条命令手动创建即可。5.2 启动 Server 时抛 ConnectException: Connection refused to host现象服务端启动时报ConnectException: Connection refused to host: localhost看起来像是注册中心连不上。原因绝大多数情况下是rmiregistry没有启动或者启动的端口不是 1099。服务端的LocateRegistry.createRegistry(1099)如果先于rmiregistry执行理论上它能自己创建注册表但如果两个终端里的注册中心服务存在端口冲突就会出现这个错误。还有一种情况是系统里之前残留了一个僵死的rmiregistry进程占用了端口新启动的服务端连不上它。解决先确认端口占用状态lsof -i:1099。如果端口被占用kill掉旧进程后重新启动rmiregistry。如果是没启动注册中心按先 registry、再 Server、最后 Client的顺序重跑一遍。另外注意 registry 程序必须留在前台运行不要用放后台然后关掉终端它会在 session 结束时被杀掉。5.3 客户端 ClassCastException 或 ClassNotFoundException现象客户端调用Naming.lookup(...)后强转HelloService时报ClassCastException或者运行时报ClassNotFoundException: server.HelloServiceImpl。原因接口或实现类的包路径在客户端和服务端不一致或者客户端的 classpath 没有包含编译输出目录。RMI 在返回 stub 时会把服务端的类信息序列化给客户端如果客户端 classpath 里存在同名但不同包路径的类JVM 就可能加载错误版本。解决把远程接口的.java文件同时交给客户端和服务端引用编译时统一执行javac -d out/production src/server/*.java src/client/*.java保证接口类的包路径完全一致。实验代码直接用server.HelloService这个包名是最稳妥的做法。5.4 序列化失败NotSerializableException现象当实验要求自定义参数或返回类型时运行时报java.io.NotSerializableException: com.example.User远程调用中断。原因远程方法的参数和返回值在跨 JVM 传输时必须实现java.io.Serializable接口。基础类型和String天然可序列化自定义类必须手动实现。解决给所有在远程接口中出现的自定义类加上implements Serializable并定义private static final long serialVersionUID 1L;。这个serialVersionUID的作用是防止序列化和反序列化时因为类结构变化导致版本不匹配。如果客户端和服务端用的同一个 jar 包写死成1L不会有问题。5.5 两台机器联调时一直在等待或超时现象服务端和客户端不在同一台机器时客户端lookup正常但调用方法时长时间无响应最后抛RemoteException: connection closed或 socket 超时。原因RMI 的注册中心查询走 1099 端口但远程对象建立实际通信时使用的是随机端口。当服务端在防火墙后面或 NAT 网络中客户端无法连接到这个随机端口。解决在服务端启动时用-Djava.rmi.server.hostname服务端IP指定对外通告的主机名同时把随机端口固定下来。实验里最简单的方式是给UnicastRemoteObject的导出过程指定固定端口// src/server/Server.java 中使用固定端口导出 UnicastRemoteObject.exportObject(service, 8888);上面这行代码需要额外 importjava.rmi.server.UnicastRemoteObject。固定端口后在防火墙上放行 1099 和 8888 两个 TCP 端口问题基本就解决了。这五个坑是我拆这份实验资源时最常遇到的情况前三个是典型的入门问题后两个是在扩展场景中才会触发。如果你在做实验过程中遇到的是其他报错一个通用排查思路是看异常堆栈里的第一行RemoteException后面的信息要么是网络连接要么是序列化要么是安全权限顺着这个分类去找对应配置就对了。6. 进阶验证用 RMI 日志开关把黑匣子看穿实验做完只是第一步我建议你在提交之前再花十分钟做一次可见化验证。RMI 背后有一整套日志系统只是默认是关闭状态。通过两个系统属性就能打开它把远程调用的每一步细节输出到终端# 在客户端启动命令中加入 RMI 日志开关 java -cp out/production \ -Djava.rmi.server.logCallstrue \ -Djava.rmi.server.logImpltrue \ client.ClientlogCalls会输出每次远程调用的方法名、参数和返回值包括调用了哪个远程对象、从哪个 IP 发起的请求这些信息能直接证明你的调用链路是通的。logImpl则显示服务端对象导出的完整日志包括动态 stub 的生成过程。跑一遍下来你会在终端里看到类似Call: HelloServiceImpl.sayHello(String)的输出比netstat更直观。另外一个实用技巧是动态 stub 的版本差异。旧版 RMI 需要手动用rmic编译生成 stub 类JDK 5 以后改成了运行时动态生成这省去了很多麻烦。但如果你在实验报告里提到编译过程中有rmi生成的临时类要注意表述准确因为新版 JDK 已经不需要这一步了。我从那次 RMI 实验之后养成一个习惯凡是涉及跨 JVM 通信的实验第一件事就是开日志开关第二件事才是看代码。因为日志能把代码看起来对和运行起来对之间的差距直接摊开在你面前尤其是分布式场景下黑匣子的状态比代码本身更难预测。这份实验资源的核心价值也在于此——它逼着你用最小成本走一遍完整远程调用链路而不是像在 Spring Cloud 里那样被框架包装得什么都看不见。希望这篇笔记能帮你把实验跑通拿到那个 Hello 响应之后你再看分布式锁、分布式事务这些话题底层的传输逻辑就再也不陌生了。本文还有配套的精品资源点击获取
返回列表