利用RPC技术实现一个学生信息管理系统
目录一、具体要求
1.客户端实现用户交互,服务器端实现学生信息存储和管理。客户端与服务器端利用RPC机制进行协作。中间件任选。
2.服务器端至少暴漏如下RPC接口:
- bool add(Student stu) 添加一条学生信息
- Student queryByID(int stuID) 查询指定ID号的学生信息
- Student queryByName(String name) 按姓名查询符合条件的学生信息
- bool delete (int stuID) 删除指定ID号的学生信息
二、相关理论
理论迭代
1.单机结构
一个系统业务量很小的时候,所有的代码都放在一个项目中,然后这个项目部署在一台服务器上。整个项目所有的服务都由这台服务器提供。这就是单机结构。 它的缺点是显而易见的,因为单机的处理能力毕竟是有限的,当你的业务增长到一定程度的时候,单机的硬件资源将无法满足你的业务需求。此时便出现了集群模式。
2.集群结构
单机处理到达瓶颈的时候,把单机复制几份,这样就构成了一个“集群”。集群中每台服务器就叫做这个集群的一个“节点”,所有节点构成了一个集群。每个节点都提供相同的服务,那么这样系统的处理能力就相当于提升了好几倍(有几个节点就相当于提升了这么多倍)。
但问题是用户的请求究竟由哪个节点来处理呢?最好能够让此时此刻负载较小的节点来处理,这样使得每个节点的压力都比较平均。要实现这个功能,就需要在所有节点之前增加一个“调度者”的角色,用户的所有请求都先交给它,然后它根据当前所有节点的负载情况,决定将这个请求交给哪个节点处理。这个“调度者”有个名字——负载均衡服务器。
集群结构的好处就是系统扩展非常容易。如果随着系统业务的发展,当前的系统又支撑不住了,那么给这个集群再增加节点就行了。但是,当你的业务发展到一定程度的时候,你会发现一个问题——无论怎么增加节点,整个集群性能的提升效果并不明显了。这时候,就需要使用微服务结构了。
3.微服务结构
从单机结构到集群结构,代码基本无需要作任何修改,要做的仅仅是多部署几台服务器,每台服务器上运行相同的代码就行了。但是,当要从集群结构演进到微服务结构的时候,之前的那套代码就需要发生较大的改动了。所以对于新系统建议系统设计之初就采用微服务架构,这样后期运维的成本更低。因为如果一套老系统需要升级成微服务结构的话,那就得对代码大动干戈了。
微服务介绍
微服务就是将一个完整的系统,按照业务功能,拆分成一个个独立的子系统,在微服务结构中,每个子系统就被称为“服务”。这些子系统能够独立运行在web容器中,它们之间通过RPC方式通信。
eg.假设需要开发一个在线商城。按照微服务的思想,我们需要按照功能模块拆分成多个独立的服务,如:用户服务、产品服务、订单服务、后台管理服务、数据分析服务等等。这一个个服务都是一个个独立的项目,可以独立运行。如果服务之间有依赖关系,那么通过RPC方式调用。
相关优点:
- 系统之间的耦合度大大降低,可以独立开发、独立部署、独立测试,系统与系统之间的边界非常明确,排错也变得相当容易,开发效率大大提升。
- 系统之间的耦合度降低,从而系统更易于扩展。我们可以针对性地扩展某些服务。假设这个商城要搞一次大促,下单量可能会大大提升,因此我们可以针对性地提升订单系统、产品系统的节点数量,而对于后台管理系统、数据分析系统而言,节点数量维持原有水平即可。
- 服务的复用性更高。比如,当我们将用户系统作为单独的服务后,该公司所有的产品都可以使用该系统作为用户系统,无需重复开发。
RPC介绍
-
注册中心 :注册中心负责服务地址的注册与查找,相当于目录服务。服务端启动的时候将服务名称及其对应的地址(ip+port)注册到注册中心,服务消费端根据服务名称找到对应的服务地址。有了服务地址之后,服务消费端就可以通过网络请求服务端了。
例如:
- RMI客户端在调用远程方法时先创建Stub。
- Skeleton处理客户端请求:lookup、rebind、unbind,如果是lookup则查找RMI服务名绑定的接口对象。
-
网络传输 :既然要调用远程的方法就要发请求,请求中至少要包含你调用的类名、方法名以及相关参数。
例如:
- RMI客户端的远程引用层传输RemoteCall序列化后的请求信息通过Socket连接的方式(传输层)传输到RMI服务端的远程引用层。
-
序列化 :既然涉及到网络传输就一定涉及到序列化,这里直接使用 JDK 自带的序列化。这种内置的序列化机制允许开发者将实现了
Serializable
接口的Java对象转换为一个字节流。例如:
- RemoteCall序列化RMI服务名称、Remote对象。
- Skeleton调用RemoteCall反序列化RMI客户端传过来的请求信息。
- Skeleton处理客户端请求,序列化该对象并通过RemoteCall传输到客户端。
- RMI客户端反序列化服务端结果,获取远程对象的引用。
- RMI服务端反射调用RMI服务实现类的对应方法并序列化执行结果返回给客户端。
- RMI客户端反序列化RMI远程方法调用结果,即为最终结果。
-
动态代理 : 另外,动态代理也是需要的。因为 RPC 的主要目的就是让我们调用远程方法像调用本地方法一样简单,使用动态代理可以屏蔽远程方法调用的细节比如网络传输。也就是说当你调用远程方法的时候,实际会通过代理对象来传输网络请求(不然的话,怎么可能直接就调用到远程方法呢?)
例如:- RMI客户端在调用远程方法时先创建Stub。这个步骤通常涉及动态代理,因为Stub作为客户端和服务端之间的代理,隐藏了远程方法调用的复杂性。
-
负载均衡 :如果我们的系统中的某个服务的访问量特别大,我们将这个服务部署在了多台服务器上,当客户端发起请求的时候,多台服务器都可以处理这个请求。那么,如何正确选择处理该请求的服务器就很关键。负载均衡就是为了避免单个服务器响应同一请求,容易造成服务器宕机、崩溃等问题。
例如:
- 尽管负载均衡在这个基础的RMI调用流程中没有直接提及,但如果RMI服务部署在多个服务器上,负载均衡可以在客户端调用远程方法时选择合适的服务器实例。
使用到的相关包
RMI相关包
import java.rmi.Naming;//这个类提供了在RMI注册表中查找和绑定远程对象的方法。
import java.rmi.registry.LocateRegistry;// 这个类用于获取和创建RMI注册表的实例,注册表是用于注册和查找远程对象的。
import java.rmi.Remote;// 这是一个标记接口,用于标识可以从非本地虚拟机上调用的接口。
import java.rmi.RemoteException;// 这个类用于声明远程方法调用过程中可能出现的异常。
import java.rmi.server.UnicastRemoteObject;// 这个类用于导出一个对象,使其成为远程对象,并使其能够接收远程方法调用。
其他包
import InterFace.*;// 这是一个自定义的包,包含了项目中定义的所有接口。
import java.util.List;// 这个接口代表一个元素的有序集合。
import java.util.ArrayList;// 这个类提供了List接口的可调整大小的数组实现。
import java.util.HashMap;// 这个类提供了基于哈希表的Map接口的实现。
import java.util.stream.Collectors;// 这个工具类提供了基于流操作的归约和汇总操作的实现。
import java.util.Scanner;// 这个类用于解析基本类型和字符串的简单文本扫描器。
import org.slf4j.Logger;// SLF4J的Logger类,用于记录日志。
import org.slf4j.LoggerFactory;// LoggerFactory类用于创建Logger对象。
import java.sql.Connection;// 这个接口定义了与特定数据库的连接,通过它可以发送命令并接收结果。
import java.sql.PreparedStatement;// 这个接口表示预编译的SQL语句,可以用于执行带或不带输入参数的SQL语句。
import java.sql.ResultSet;// 这个接口表示数据库结果集,它是通过执行查询数据库的语句生成的。
import java.sql.SQLException;// 提供关于数据库访问错误或其他错误信息的异常。
import java.sql.DriverManager;// 这个类管理一组JDBC驱动程序的基本服务。
import java.io.Serializable;// 这是一个标记接口,用于启用其序列化功能的类必须实现此接口。
MySQL启动方法
mysql的启动和环境配置 https://blog.csdn.net/qq_47900752/article/details/130324285
三、代码架构
- 客户端 (
RMIClient
):- 提供用户界面,通过控制台与用户交互,执行增加、查询、修改、删除和打印学生信息的操作。
- 使用Java RMI的
Naming.lookup
方法从RMI注册表中获取服务端的远程对象引用,以调用远程方法。
- 服务端 (
RMIServer
):- 注册远程对象
StudentSystemImpl
,使其可通过RMI客户端访问。 - 创建和管理RMI服务注册表,允许在指定的端口(如9527)上接收请求,最后将RMI服务名绑定至远程方法实现类上。
- 先定义RMI服务器IP地址、监听端口号、RMI服务名称,再创建注册中心。
- 注册远程对象
- 远程接口 (
StudentSystemInt
):- 定义了客户端和服务端共用的远程方法接口,如添加、查询、删除、修改学生信息和打印学生列表。
- 所有方法都声明了抛出
RemoteException
,以处理远程调用中可能发生的异常。
- 远程对象实现 (
StudentSystemImpl
):- 其中实现学生信息管理系统的数据结构定义以及数据操纵函数。
- 实现了
StudentSystemInt
接口,处理实际的业务逻辑。 - 使用
HashMap
来存储学生数据,同时通过StudentDaoImpl
与数据库交互,实现数据的持久化。
- 数据访问对象 (
StudentDao
,StudentDaoImpl
):- 提供对数据库的访问方法,如保存、查询、删除和更新学生信息。
- 使用JDBC(是一种Java API,用于在Java应用程序中连接和执行查询与数据库。这个API,由一组用Java语言编写的类和接口组成,提供了一种标准方法,让Java程序能够独立于底层数据库访问与操作数据)进行数据库操作,保证数据的持久存储。
四、功能实现+界面展示
基本功能:
- 增加学生:客户端收集学生ID和姓名,调用服务端的添加方法。服务端检查缓存(
HashMap
)中是否已存在该学生,若不存在则添加到缓存和数据库。 - 查询学生:支持通过ID或姓名查询。通过ID查询会先查缓存,未命中再查询数据库。通过姓名查询使用并行流从缓存中筛选,未命中则查询数据库。
- 删除学生:从缓存和数据库中删除指定ID的学生。
- 修改学生信息:更新指定学生的姓名信息,在缓存和数据库中都进行更新。
- 打印学生列表:从数据库中检索所有学生信息,并在客户端打印。
技术点:
除了Java RMI技术实现网络间的远程方法调用外,还具备以下优势:
1. 查询速度提高
- 索引:在操作储存在缓存中的数据部分时,考虑使用更高效的数据结构,如
HashMap
,以学生ID作为键,这样查询的时间复杂度可以从O(n)降低到接近O(1)。
2. 数据模型和封装
- 封装
Student
类的属性:最初Student
类的studentID
和studentName
属性是公开的(public
)。后面改进为使用私有属性和公开的getter
/setter
方法来增强封装性。
3. 异常处理
- 细化异常处理:最初的实现中,异常被捕获后仅仅打印堆栈跟踪。所以我根据不同的异常类型做更细化的处理,例如,对用户输入错误或网络问题给出更友好的提示信息。
4. queryByName
方法的性能
- 使用并行流:将数据结构已经从
ArrayList
切换到了HashMap
后,考虑到对于queryByName
这样的操作,仍然需要遍历整个集合。为了提高处理大量数据的效率,我使用了Java 8的parallelStream()
来并行处理这些数据,将数据分片后,同时查询多个片段,从而减少总体查询时间。
5. 增加日志记录
- 使用日志框架:在关键操作处(如添加、删除学生信息)增加日志记录。这不仅有助于开发和调试,而且对于生产环境中的问题排查也非常有用。我使用了SLF4J的日志框架。
6. 安全性考虑
- 参数校验:在处理客户端请求时,对输入参数进行校验,比如检查
studentID
或studentName
是否为null
或空字符串,以避免潜在的错误或安全问题。
7. 数据持久化
- 持久化学生数据:数据一般保存在缓存中,我将学生信息持久化到MySQL数据库中,这样即使程序重启,学生信息也不会丢失。
界面展示:
1.启动成功的服务端:
2.启动成功的客户端:
3.为了演示操作,我先把数据库里的东西都删掉:
比如说我的数据库原来里面是:
我在客户端进行如下操作:
然后我的服务端可见我的删除日志:
然后我刷新数据库:
可见数据已经被全部删除;
4.接下来,我们从第一条开始尝试功能:
5.查看我们的日志:
6.我们可以先看缓存里面添加成功没有,输入6,打印学生列表:
7.接下来,我们刷新数据库:
8.然后,我们试试我们的查询功能:
(1)通过ID查询:
我们查看日志:
显示的是通过缓存查到了对应学生,这是因为我设置了先从缓存查,再从数据库查,从缓存查到后就不再查询数据库。
我们再试个查不到的:
下面是我们的日志:
(2)通过姓名查询:
下面是我们的日志:
我们再试个查不到的:
下面是我们的日志:
9.我们试试删除功能:
下面是我们的日志:
我们来展示一下我们的学生列表:
可以看到ID==3的红太狼被删了,接下来,我们可以看看数据库:
10.最后,我们试一下改名功能:
我们日志也有正确呈现:
我们看一下数据库:
改名成功!!!
11.我们试试加入ID相同的学生:
我们的日志也有回应:
数据库也没有添加成功:
另外,补充一下我们的数据库设置:
12.我们试试把学生都删了再继续删:
删后的数据库:
可见程序可以准确识别错误:
13.我们尝试退出程序:
14.我们试试在数据库直接加一个学生:
可以发现查询ID==1可以查到他!
我们查看日志:
演示完毕!成功!!!
五、系统的展望
1.其他序列化协议:因为JDK 自带的序列化效率低并且有安全漏洞。 所以,还要考虑使用哪种序列化协议,比较常用的有 hession2、kyro、protostuff;
2.多个服务端:现在只有一个服务端,后面拓展到多个后调用服务时,从很多服务地址中根据相应的负载均衡算法选取服务地址;
3.逐步引入服务专用数据库:当条件允许时,为每个服务配置独立的数据库;
4.容器化部署:当使用了微服务架构后,我们将一个原本完整的系统,按照业务逻辑拆分成一个个可独立运行的子系统。为了降低系统间的耦合度,我们希望这些子系统能够运行在独立的环境中,这些环境之间能够相互隔离。若使用虚拟机来实现运行环境的相互隔离的话成本较高,虚拟机会消耗较多的计算机硬件/软件资源。所以这里建议使用Docker,Docker不仅能够实现运行环境的隔离,而且能极大程度的节约计算机资源。
5.自动化构建:当我们使用微服务架构后,随着业务的逐渐发展,系统之间的依赖关系会日益复杂,而且各个模块的构建顺序都有所讲究。这里可以使用Jenkins,我们只需在Jenkins中配置好代码仓库、各个模块的构建顺序和构建命令,在以后的构建中,只需要点击“立即构建”按钮,Jenkins就会自动到你的代码仓库中拉取最新的代码,然后根据你事先配置的构建命令进行构建,最后发布到指定的容器中运行。
6.安全性与数据管理:考虑引入更复杂的错误处理机制、增强的安全性措施(如使用SSL/TLS加密RMI通信),以及更高级的数据管理策略(如引入ORM框架如Hibernate)。
六、运行老师的文件的收获
首先,我的环境是JDK8
我对代码及其结构均进行了调整:
刚好可以在下面这张图里基本都展示出来:
然后,有用的步骤是:
1.分别到server/client所在目录下cmd完成.java文件的编译: javac -encoding utf-8 *.java,然后在targe目录下就会出现一些.class文件(必须的,不然编译时找不到文件;注意修改.java内容后要重新编译);
2.在客户端与服务器端.class端的大目录里面建立一个jar包:
jar cvf shared.jar rmiserver\MyCalc.class
3.先清理占用1099端口的进程:
(1)也是到客户端与服务器端.class端的大目录,cmd一下;
(2)清理进程
netstat -ano|findstr 1099
tasklist|findstr 99232
(这部可有可无)
taskkill /pid 99232 -t -f
(3)引入注册中心
start /b java -classpath "D:\Grade 3\分布式计算\_0406tri\rmi_teacher\target\classes;D:\Grade 3\分布式计算\_0406tri\rmi_teacher\target\classes\shared.jar" sun.rmi.registry.RegistryImpl 1099
4.在IDEA上先运行服务端,再运行客户端~
大功告成!!!
正确结果:
服务端先:
客户端后:
然后返回服务端:
整体界面展示:
错误回忆
1.这个的造成一方面是因为我现在的JDK版本会限制接口MyCalc必须是唯一的,另一方面是因为我的代码当时没有在客户端把这个接口删掉+申明用服务端编译好的MyCalc,还有一方面是运行目录也不对;
2.修改代码后,原来对应的.class文件最好删掉,重新编译!
另外补充
命令行运行方式(前提:编译好class文件+进入正确目录):
服务器端:
java -classpath "target\classes;target\classes\shared.jar" rmiserver.MyRMIServer
客户端:
java -classpath "target\classes;target\classes\shared.jar" rmiclient.MyRMIClient
(那时代码还没改hhh,所以有一丢丢问题,代码改好就正确啦!)
PS:错误的根本原因是因为客户端尝试将远程对象转换为 rmiclient.MyCalc
,而实际上它应该转换为远程接口 rmiserver.MyCalc
。即使两个接口的源代码完全相同,如果它们的包路径不同,Java 也会将它们视为完全不同的类型。
七、参考链接
我手写了一个RPC框架。成功帮助读者斩获字节、阿里等大厂offer。 - 知乎 (zhihu.com) 基于 Netty+Kyro+Zookeeper 实现的 RPC 框架(非常优秀!)
分布式计算_grpc_书籍信息管理系统(不配叫系统)_分布式计算实现书籍管理-CSDN博客
【JAVA】基于RMI的书籍信息管理系统_bool add(book b) 添加一个书籍对象。 book querybyid(int book-CSDN博客
CourseDesigned: 课程设计 - Gitee.com
Java的RMI远程方法调用实现和应用-阿里云开发者社区 (aliyun.com)
Distributed-Computing/exp3-rpcsystem at main · silence-tang/Distributed-Computing (github.com)
八、未来的学习方向
1.(微服务讲得特别浅显易懂,我有时间一定实现一下)
如何零基础搭建一套微服务框架(Spring Boot + Dubbo + Docker + Jenkins) - 知乎 (zhihu.com)
2.注册中心:Nacos 网关:Gateway 后端基础框架:ssm 前端:Vue + SPA Axios(request.js)
SpringCloud微服务之学生管理_基于微服务的学生管理系统-CSDN博客
3.基于 MySQL + springcloud 微服务框架的作业管理系统_基于微服务的作业管理系统-CSDN博客
4.文章详细,可视化良好
在idea中搭建微服务项目(22版),详细教程_idea创建微服务项目-CSDN博客
5.作为补充吧
JDK 1.8 IEDA 2019(推荐)Maven 3.5.x(推荐) nacos 1.1.3(推荐)
手把手教你搭建第一个微服务(框架) - 知乎 (zhihu.com)
标签:服务,项目,数据库,编程,分布式计算,java,RMI,远程,客户端 From: https://www.cnblogs.com/HYLOVEYOURSELF/p/18211787