本图书管理系统已经经历了三个阶段
第二代图书管理系统:
第一代,我们的三层架构实际上是MVC缩水版,service层和controller层其实是一个东西,连接前端的(控制台实现)是直接调用对象.属性的方式来实现的。这次我将跨度直接来一个大升级,直接将客户端和服务端拆分。
相信大家都写得出来吧!无非一些键盘录入,switch,操作集合容器,IO读写
虽然使用IO流可以轻松的将数据持久化到本地,但是我们的View和Service之间还是以传输对象为主,直接调用Service层的方法,耦合性很高,而且我们这个项目始终是个整体,想要跑一跑这个项目,得把数据文件整个一起打包传输给别人才行,不然你上述代码查看所有图书功能在不给别人数据文件的前提下怎么展示呢?
改造!
注意事项
在不大规模修改代码,已实现的功能应该保持原样
客户端服务端应该是两个不同的项目而不是一个包下面的两个子包!
客户端不应该有操作数据的方法,服务端不能有操作客户端的方法。
技术难点
信息通信,相信简单的网络编程TCP和UDP的小Demo大家都会写吧,我这里简单罗列一下代码
这里我引入其他博主优秀的帖子:网络通信协议,TCP和UDP协议。
写两个简单的案例:
UDP发送端
public class UdpSend{
public static void main(String[] args) throws SocketException {
DatagramSocket datagramSocket = new DatagramSocket();
while (true) {
//控制台发送信息
Scanner scanner = new Scanner(System.in);
String message = scanner.nextLine();
byte[] bytes = message.getBytes();
//发送数据包
try {
DatagramPacket datagramPacket = new DatagramPacket(bytes, bytes.length, InetAddress.getByName("127.0.0.1"), 10001);
datagramSocket.send(datagramPacket);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
UDP接收端
public static void main(String[] args) throws SocketException {
DatagramSocket datagramSocket=new DatagramSocket(10000);
while (true){
//新建一个数据包,用于存储数据
byte[] bytes = new byte[1024];
DatagramPacket datagramPacket = new DatagramPacket(bytes, bytes.length);
//通过服务的receive方法将受到的数据存入数据包中
try {
datagramSocket.receive(datagramPacket);
} catch (IOException e) {
e.printStackTrace();
}
//获取连接的属性(ip 端口 文件内容)
String ip=datagramPacket.getAddress().getHostAddress();
String data = new String(datagramPacket.getData(), 0, datagramPacket.getLength());
int port = datagramPacket.getPort();
System.out.println(" ip : "+ip+", data : "+data+" , port : "+port);
}
}
玩一玩
UDP大概就是数据加进数据包,然后发送一个数据包,至于是否发送到别人手上,我不管!
细节就不说了,TCP就是一种稳定的协议,知道要传输给谁,而且保证能传输。
TCP客户端
import java.io.*;
import java.net.Socket;
public class TcpClient {
public static void main(String[] args) throws IOException {
//创建一个客户端对象
Socket socket=new Socket("192.168.1.3", 10000);
//用于接收键盘录入到流中
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
//用于接收服务端返回结果
BufferedReader receive = new BufferedReader(new InputStreamReader(socket.getInputStream()));
//用于发送请求到服务端
BufferedWriter send = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
//读取键盘录入
String request = null;
while ((request=bufferedReader.readLine())!=null){
if ("exit".equals(request)){
break;
}
//发送信息
send.write(request);
send.newLine();
send.flush();
//等待服务器范返回
String response=receive.readLine();
System.out.println(response);
}
//关闭资源
bufferedReader.close();
receive.close();
send.close();
socket.close();
}
}
TCP服务端
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class TcpServer {
public static void main(String[] args) throws IOException {
//建立服务端对象,监听10000端口
ServerSocket server=new ServerSocket(10000);
//接收一个客户端对象
Socket socket=server.accept();
//用于接收客户端信息
BufferedReader receive = new BufferedReader(new InputStreamReader(socket.getInputStream()));
//用于信息发送客户端
BufferedWriter send = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
String line = null;
//接收客户端发出的信息
while ((line=receive.readLine())!=null){
//将请求字符串的大写格式返回
send.write(line.toUpperCase());
send.newLine();
send.flush();
}
//关闭资源
socket.close();
server.close();
receive.close();
send.close();
}
}
玩一玩
简单的说就是发送完一个字符串,服务器接收到然后把字符串大写的结果返回给客户端
那么要更新我们的图书管理系统,我们应该用哪个协议呢?
我们想象一下:
**UDP:**客户端把一个请求写为/login/root/123456打包成数据包,发送到服务端,服务端解析字符串,把/loginResut/true发送给客户端,客户端解析字符串,得到结果==》登录成功
**TCP:**服务端客户端建立连接,客户端将/login/root/123456打包成字符串写进socket流,服务端读取到流,解析字符串,将"true"字符串写进流,客户端读取到流,字符串转布尔值==》登录成功
看上去差别不大,UDP每次重新打包,而且如果有多个客户端,你是不是不知道应该是哪个客户端发来的数据包,而且服务端发送数据包也要注名是发给谁,这样的操作每次都要进行,既然如此那不如干脆我们两个先建立连接再通过流告诉的读写。
根据以上分析,我们决定用稳定的TCP协议来连接前后端。
首先,我们不能大规模改代码,我们应该在不怎么修改代码的前提下,完成业务拆分
我们看一下我们原来的项目架构。
我们发现:
我们提出以下架构
首先我们新增了一个管理中心,也就是Socket服务,也就是TCP,我们前后端交互都是通过对管理中心进行交互数据来实现通信的。
新增一个BookHandler类,用于解析socket流传过来的东西,然后再调用BookService方法,将返回值再通过TCP通讯返还给BookView前端界面。
为什么有两个BookService类?两个BookService冲突吗?**
前端的每一个方法都是依赖于调用BookService.方法名实现的,如果我们不希望页面飘红,我们就保留BookService这个类,但是我们的对方法体要进行修改,不能像以前一样。
不冲突!因为我们的架构设计,前后端已经分离了,为了体现分离,我们可以新建两个module,分别写前端和后端,这样即便方法重名也互不干涉!
真正意义的分离
Book_Client 客户端
其中BookUtils对象是抽取出前端才需要的方法,没有更改代码,BookView也没有更改任何代码
Book_Server 服务端
其中 Book,BookService,util对象没有进行任何的更改。
先看简单的前端的BookService
我们看到我们把参数都封装成一个类似于get请求的字符串形式,再通过BookClient的request方法发送出去,方法有个String类型的返回值,也就是response,我们发现我们都是把字符串转换成布尔值,我们看看其他方法。
这里有返回结果为void,返回结果为String的方法,可以看到,不同的方法,我们都进行不同的处理,返回的都是我们想要的字符串。原因是我们客户端服务器是固定的,每个方法发出的请求字符串的前缀就是方法名也是唯一的,参数也是给定的,所以我们前端发送这些请求就可以被后端正确的解析并且返回想要的结果。
由于BookService的方法体变了,但是方法返回值,参数等信息都是没有变化的,所以我们的前端不会飘红。
接下来上一点难的代码
这是BookClient的request方法,简单的说就是如果不终止,那么就输入流里写请求,然后输出流拿结果。当然结果是readline是字符串,比如返回结果是"true",我们用Boolean.parseBoolean()来转换
可以看到,客户端由于几乎没有处理数据的方法,只是简单的字符串类型转换,和发送request字符串,还是很好实现的,没有大规模改动BookView的代码。
接下来看服务端
先从简单的开始
服务端除了BookHandler和BookServer以外,其他代码都是原封不动的
我们先看一下建立通讯的BookServer
这里没什么好说的,我们看BookHandler的handler方法
实现细节我发红色字了,大概就是TCP简单Demo多一些条件判断而已
模拟请求解析过程
我们发现,实际上就是知道是哪个方法然后用类加载机制通过方法名得到方法Method对象,然后传入参数,将结果返回,由于response都是String类型的,而String.valueOf方法可以将其他类型转化成String类型。
流程图
流程叙述
BookView调用BookService的方法,BookService将方法名和参数信息拼接为一条为一条字符串,比如login/username/123456交给BookClient的request方法。
BookClient将请求字符串写入socket的输入流中,然后BookServer的readline方法读取到socket输出流中前端传递过来的字符串,将字符串交给BookHandler方法。
BookHandler的handler方法解析请求字符串为String类型的数组。比如login/username/123456,解析为String{login,username,password}。通过数组的第一个元素来选择要调用的方法的方法名,使用类加载机制获取到BookService对应的方法对象Method,然后用Method.invoke(对象,参数…)来调用方法。返回结果再转换为字符串,然后返回给BookHandler。
BookHandle将返回结果写进socket的输入流中,然后前端的BookClient接收到socket输出流中的response字符串,交给BookService。
BookService根据需求决定是将字符串转换为布尔值,还是直接返回,还是打印等各种操作。
结果演示
由于已经是分布式了,所以我们可以分别打包成jar包,通过jar包的方式来运行java程序,这样更加的直观。
由于篇幅问题,一些项目的细节以及采坑记录我会单独的整理,要源码的可以私信我。