跳转至

Java网络编程、正则表达式、单例设计模式与Lombok

约 6815 个字 466 行代码 4 张图片 预计阅读时间 29 分钟

Java网络编程

软件结构

  • C/S结构 :全称为Client/Server结构,是指客户端和服务器结构
    • 优点:
      • 高性能:由于计算任务可以分配给最适合处理该任务的设备,因此可以在性能上得到优化例如,图形密集型应用可以由具有强大图形处理能力的客户端来处理,而数据存储和检索则由服务器完成
      • 安全性较高:数据集中存储在服务器端,可以更容易地实施安全措施,如访问控制、加密等,从而提高系统的整体安全性
      • 资源有效利用:服务器可以根据需求进行扩展,以支持更多的客户端连接,而不需要每个用户都有强大的硬件配置
      • 灵活性:C/S架构允许对网络中的各个组件单独进行升级和维护,而不影响其他部分的运行
    • 缺点:
      • 安装和维护成本高:与浏览器/服务器(B/S)架构相比,C/S架构需要在每个客户端安装特定的应用程序,并且每次更新都需要在所有客户端上重复进行
      • 依赖性较强:如果服务器出现故障或者需要停机维护,那么所有的客户端都将无法使用相关服务
      • 可扩展性有限:虽然服务器可以根据需要增加硬件资源,但是增加新的客户端可能需要额外的软件授权和配置工作,这可能会限制系统的可扩展性
      • 网络带宽要求:对于某些应用程序来说,客户端与服务器之间的通信可能会消耗大量的网络带宽,尤其是在大量数据交换的情况下
  • B/S结构 :全称为Browser/Server结构,是指浏览器和服务器结构
    • 优点:
      • 易于部署和维护:用户只需要一个支持相应Web标准的浏览器即可访问应用程序,无需在每台客户端机器上安装特殊软件软件更新也只需在服务器端进行,减少了客户端维护的工作量
      • 平台无关性:基于Web的应用程序通常可以跨多个操作系统和设备运行,只需要一个现代的Web浏览器即可,这使得用户可以从任何地方访问应用
      • 较低的客户端需求:与C/S架构相比,B/S架构对客户端硬件的要求较低,因为大部分处理都在服务器端完成
      • 易于共享和协作:Web应用天生适合信息共享和团队协作,特别是在云计算和SaaS(Software as a Service)模式下
    • 缺点:
      • 性能问题:由于所有的处理都发生在服务器端,当用户数量增加时,可能会导致服务器负载过大,从而影响应用程序的响应速度
      • 安全性挑战:虽然可以通过多种方式增强Web应用的安全性,如SSL/TLS加密、身份验证机制等,但Web应用仍然面临各种安全威胁,如SQL注入、XSS攻击等
      • 用户体验受限:与原生应用程序相比,Web应用在某些方面(如图形界面、输入设备兼容性等)可能提供较差的用户体验不过,随着HTML5、CSS3和JavaScript的发展,这种差距正在逐渐缩小
      • 依赖于网络连接:B/S架构的应用必须始终保持网络连接才能正常工作,这对于那些网络不稳定或没有互联网接入的地区来说是个问题

网络基础知识

相关概念

服务器:简单理解为安装了服务器软件的计算机,后面将会学习到的服务器软件:Tomcat

网络通信协议:网络通信协议是指一组规则和标准,用于规定网络中不同设备之间如何进行通信。这些协议定义了数据传输的格式、顺序、错误检测方法以及其他控制机制,以确保数据能够在网络中可靠地传输

IP地址

IP地址(Internet Protocol Address)是指在使用Internet协议(IP)的网络中,为每个网络接口分配的一个数字标识符。这个标识符用于在网络中唯一标识一个设备以及在不同的计算机之间连接

常见的IP地址有两种:

  1. IPv4:IPv4使用32位地址空间,表示为四个十进制数(范围从0到255),中间用点号.分隔,最多可以表示42亿个,例如:192.168.1.1。IPv4地址空间有限,随着互联网用户的增长,IPv4地址变得越来越稀缺
  2. IPv6:为了解决IPv4地址耗尽的问题,IPv6采用128位地址空间,表示为八组十六进制数,每组四个十六进制数,中间用冒号:分隔,例如:2001:0db8:85a3:0000:0000:8a2e:0370:7334。IPv6提供了巨大的地址空间,理论上可以支持更多的设备连接到互联网

Note

在Windows下可以在命令行中输入ipconfig查看当前计算机的IP地址

如果需要判断自己的计算机是否可以连接到其他计算机,可以使用ping命令:ping IP地址/网址

特殊的IP地址:127.0.0.1,一般情况下固定不变,并且大多数情况下可以使用localhost代替127.0.0.1,其后一般会跟端口号,例如Tomcat的端口号为8080

Note

在JavaWeb部分将更多使用localhost来访问服务器上的资源,例如:

localhost:8080/应用名称/index.html

TCP协议和UDP协议介绍

UDP协议:UDP(User Datagram Protocol,用户数据报协议)是一种无连接的传输层协议,主要用于提供不可靠但高效的网络数据传输服务

特点:

  1. 无连接
    • UDP在发送数据之前不需要先建立连接,也不需要在数据传输完成后关闭连接。这意味着UDP可以立即开始发送数据,适用于实时应用,如在线语音聊天、视频流媒体等
  2. 不可靠
    • UDP不保证数据报文的可靠传输。数据报可能丢失、重复或乱序到达,也不会有重传机制。对于不需要可靠传输的应用场景,UDP提供了更快的速度
  3. 无序传输
    • UDP不保证数据报文按照发送顺序到达接收方。数据报可能按任意顺序到达。
  4. 轻量级
    • UDP的头部开销较小,只有8字节,包括源端口、目标端口、长度和校验和字段。这使得UDP非常适合对延迟敏感的应用
  5. 广播和多播支持
    • UDP支持广播(向网络中的所有设备发送数据)和多播(向特定组内的设备发送数据),这是TCP所不具备的功能

TCP协议:TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议

特点:

  1. 面向连接
    • 在数据传输前,TCP需要先建立连接,这通常通过三次握手完成。连接建立后,双方可以开始数据传输。传输结束后,还需要通过四次挥手来断开连接。
  2. 可靠传输
    • TCP提供了可靠的数据传输机制,确保数据无差错、按序到达。如果数据在传输过程中丢失或损坏,TCP会自动重传这些数据。
    • 通过序列号和确认应答(ACK)机制,TCP确保了数据的顺序性和完整性。
  3. 流量控制
    • TCP使用滑动窗口机制来进行流量控制,确保发送方的发送速率不超过接收方的处理能力,避免拥塞。
  4. 拥塞控制
    • TCP具有拥塞控制功能,可以动态调整发送速率,减少网络拥塞的发生,提高网络的整体性能。
  5. 全双工通信
    • TCP支持全双工通信,即数据可以同时在两个方向上传输,每个方向上的数据传输都是独立的。

TCP协议的三次握手和四次挥手

三次握手:

  1. 第一次握手,客户端向服务器端发出连接请求,等待服务器确认
  2. 第二次握手,服务器端向客户端回送一个响应,通知客户端收到了连接请求
  3. 第三次握手,客户端再次向服务器端发送确认信息,确认连接

四次挥手:

  1. 第一次挥手:客户端向服务器端提出结束连接,让服务器做最后的准备工作。此时,客户端处于半关闭状态,即表示不再向服务器发送数据了,但是还可以接受数据。
  2. 第二次挥手:服务器接收到客户端释放连接的请求后,会将最后的数据发给客户端。并告知上层的应用进程不再接收数据。
  3. 第三次挥手:服务器发送完数据后,会给客户端发送一个释放连接的报文。那么客户端接收后就知道可以正式释放连接了。
  4. 第四次挥手:客户端接收到服务器最后的释放连接报文后,要回复一个彻底断开的报文。这样服务器收到后才会彻底释放连接。这里客户端,发送完最后的报文后,会等待2MSL,因为有可能服务器没有收到最后的报文,那么服务器迟迟没收到,就会再次给客户端发送释放连接的报文,此时客户端在等待时间范围内接收到,会重新发送最后的报文,并重新计时。如果等待2MSL后,没有收到,那么彻底断开。

例子理解:

三次握手:

  1. 第一次握手(SYN)
    • 假设小明(客户端)想要和小红(服务器)通话。小明先给小红发了一条短信:“嘿,小红,你想不想跟我聊聊天?”这条短信相当于发送了一个SYN(同步)信号,告诉小红小明想开始聊天。
    • 小明的状态变成了“等待小红的回应”。
  2. 第二次握手(SYN+ACK)
    • 小红收到了小明的短信后,很高兴地回复说:“好的,我也愿意跟你聊天。”这条回复包含了SYN(确认小明想聊天)和ACK(确认收到小明的消息)。
    • 这时,小红的状态变成了“等待小明的确认”。
  3. 第三次握手(ACK)
    • 小明收到小红的回复后,回复说:“太好了,那我们就开始吧!”这条回复只是一个ACK(确认收到小红的消息),表示小明确认了小红的意愿。
    • 此时,小明和小红都进入了“准备聊天”的状态,双方都可以开始发送数据了。

四次挥手:

  1. 第一次挥手(FIN)
    • 假设小明(客户端)觉得聊得差不多了,想结束这次对话。于是他给小红发了一条短信:“小红,今天聊得很开心,但我得去吃饭了,我们下次再聊吧。”这条短信相当于发送了一个FIN(结束)信号,表示小明想要结束这次对话。
    • 小明的状态变成了“等待小红的确认”。
  2. 第二次挥手(ACK)
    • 小红收到小明的短信后,回复说:“好的,我也觉得聊得差不多了,你去吃饭吧。”这条回复只是一个ACK(确认收到小明的消息),表示小红同意结束对话。
    • 小红的状态变成了“等待小明的确认”。
  3. 第三次挥手(FIN)
    • 小红接着给小明发了一条短信:“那我也要去做我的事情了,再见。”这条短信也是一个FIN信号,表示小红也准备结束这次对话。
    • 小红的状态变成了“等待小明的确认”。
  4. 第四次挥手(ACK)
    • 小明收到小红的短信后,回复说:“好的,再见。”这条回复只是一个ACK(确认收到小红的消息),表示小明确认了小红结束对话的意愿。
    • 此时,双方都确认了对话的结束,连接正式关闭。

UDP协议编程

UDP协议最大的特点就是不需要在双方都连接的情况下发送数据

在Java中,实现UDP协议编程需要使用到两个类:

  1. DatagramSocket:创建 UDP 客户端和服务器,实现数据的非连接、不可靠传输
  2. DatagramPacket:构造一个包含数据和目标地址及端口的数据包

创建客户端

客户端创建步骤如下:

  1. 创建DatagramSocket对象:创建一个UDP客户端
    1. 对应的有参构造:DatagramSocket(int port),参数传递本机端口号
    2. 无参构造:DatagramSocket(int port),随机端口号
  2. 确定发送内容
  3. 创建DatagramPacket对象:(常用构造方法)DatagramPacket(byte[] buf, int length, InetAddress address, int port)
    1. 第一个参数:存储传递数据的字节数组
    2. 第二个参数:传递数据的元素个数
    3. 第三个参数:服务端的IP地址,通过InetAddress中的静态方法:static InetAddress getByName(String host)设置服务端的IP地址,参数即为服务端的IP地址
    4. 第四个参数:服务端的端口号
  4. 发送数据:使用DatagramSocket中的方法:void send(DatagramPacket p)将打包好的DatagramPacket对象发送给服务器
  5. 关闭UDP客户端

例如下面的代码:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class Client {
    public static void main(String[] args) throws Exception {
        // 创建DatagramSocket对象
        DatagramSocket datagramSocket = new DatagramSocket(8081);
        // 创建数据
        byte[] bytes = "这是客户端发送的数据".getBytes();
        // 创建IP地址
        InetAddress byName = InetAddress.getByName("127.0.0.1");

        // 创建DatagramPacket对象
        DatagramPacket datagramPacket = new DatagramPacket(bytes, bytes.length, byName, 2222);
        // 发送数据
        datagramSocket.send(datagramPacket);
        // 关闭资源
        datagramSocket.close();
    }
}

直接点击运行可以看到没有任何报错,因为不论是否存在服务端,UDP都可以发送

创建服务端

创建服务端依旧使用前面的两个类DatagramSocket以及DatagramPacket

创建步骤如下:

  1. 创建DatagramSocket对象:使用有参构造指定端口号
  2. 创建接受数据的byte数据
  3. 创建DatagramPacket对象:使用DatagramPacket(byte[] buf, int length)构造用于接收数据的对象
  4. 调用DatagramSocket中的方法:void receive(DatagramPacket p),传递DatagramPacket对象接受数据
  5. 解析DatagramPacket对象:
    1. 调用DatagramPacket中的方法:byte[] getData(),解析数据包收到的数据
    2. 调用DatagramPacket中的方法:int getLength(),获取数据包中的数据数组长度
    3. 调用DatagramPacket中的方法:int getPort(),获取到发送方的端口
    4. 调用DatagramPacket中的方法:InetAddress getAddress(),获取到发送方的IP地址
  6. 关闭UDP服务端
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
public class Server {
    public static void main(String[] args) throws Exception{
        // 1. 创建DatagramSocket对象:使用有参构造指定端口号
        DatagramSocket datagramSocket = new DatagramSocket(2222);
        // 2. 创建接受数据的byte数据
        byte[] bytes = new byte[1024];
        // 3. 创建DatagramPacket对象:使用DatagramPacket(byte[] buf, int length)构造用于接收数据的对象
        DatagramPacket datagramPacket = new DatagramPacket(bytes, bytes.length);
        // 4. 调用DatagramSocket中的方法:void receive(DatagramPacket p),传递DatagramPacket对象接受数据
        datagramSocket.receive(datagramPacket);
        // 5. 解析DatagramPacket对象:
        // 接收到的数据
        byte[] data = datagramPacket.getData();
        // 数据大小
        int length = datagramPacket.getLength();
        // 发送端的IP地址
        InetAddress address = datagramPacket.getAddress();
        // 发送端的端口号
        int port = datagramPacket.getPort();
        System.out.println(new String(data,0, length));
        // 释放资源
        datagramSocket.close();
    }
}

编写服务端时,需要注意,服务端在创建DatagramSocket对象时需要指定端口号,否则发送端无法确定需要接收的服务端

运行

因为是发送端发送数据,服务器端接收数据,所以运行时需要保证服务器端先运行,再运行发送端,再前面服务端的代码中因为存在一个打印语句,所以当服务端接收到数据时,就会将数据打印到服务器端的控制台

TCP协议编程

TCP协议的特点是必须双方相互连接才能发送数据

在Java中,实现TCP协议编程需要使用到Socket类:用于实现TCP网络通信

TCP协议下发送数据的过程:

创建客户端

创建客户端的步骤如下:

  1. 创建Socket对象,使用构造方法:Socket(InetAddress address, int port) 指定服务器端的IP地址和服务器端的端口号
  2. 调用Socket类中的方法:OutputStream getOutputStream()向服务器端发送请求
  3. 向服务器写数据:调用OutputStream中的write方法即可
  4. 调用Socket类中的方法:InputStream getInputStream()接受服务器发送的请求
  5. 读取接收到的数据:调用InputStream中的read方法即可
  6. 关闭上面开启动Socket流、输入流和输出流

例如:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class Client {
    public static void main(String[] args) throws Exception{
        Socket socket = new Socket("127.0.0.1", 2222);
        // 向服务器发送请求
        OutputStream outputStream = socket.getOutputStream();
        // 向服务器写数据
        outputStream.write("这是客户端发送的请求".getBytes());

        // 接受服务器端的数据
        byte[] bytes = new byte[1024];
        InputStream inputStream = socket.getInputStream();
        // 读取服务器端的数据
        int len = inputStream.read(bytes);
        // 输出接受到的结果
        System.out.println(new String(bytes, 0, len));
        // 关流
        inputStream.close();
        outputStream.close();
        socket.close();
    }
}

创建服务端

TCP的服务端使用到ServerSocket

创建服务端的步骤如下:

  1. 创建ServerSocket对象,调用构造方法:ServerSocket(int port),参数为服务器端的端口号
  2. 调用ServerSocket中的Socket accept()方法,返回一个Socket类对象
  3. 使用Socket类中的方法:InputStream getInputStream(),接受来自客户端发送的数据。可以读取客户端发送的数据并打印
  4. 使用Socket类中的方法:OutputStream getOutputStream(),向客户端发送数据
  5. 关闭ServerSocket流、Socket流、输入流和输出流

例如:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class Server {
    public static void main(String[] args) throws Exception{
        ServerSocket serverSocket = new ServerSocket(2222);
        // 等待接受客户端的请求
        Socket accept = serverSocket.accept();
        // 接受客户端的数据
        InputStream inputStream = accept.getInputStream();
        byte[] bytes = new byte[1024];
        int len = inputStream.read(bytes);
        System.out.println(new String(bytes, 0, len));
        // 向客户端发送数据
        OutputStream outputStream = accept.getOutputStream();
        outputStream.write("我发给你了数据".getBytes());
        // 关流
        outputStream.close();
        inputStream.close();
        accept.close();
        serverSocket.close();
    }
}

运行

因为是发送端发送数据,服务器端接收数据,所以运行时需要保证服务器端先运行,再运行发送端。在上面的代码中:

对于客户端来说,因为需要向服务器发送请求,所以需要先向服务器写数据,接下来就是等待接收服务器发送数据

对于服务器端来说,因为需要接收客户端的请求,所以需要等待服务器接收,接收到请求后,服务器可以打印接收到的数据,再向客户端写数据

最后,客户端接收到服务器端发送的数据

文件上传案例

案例介绍:

使用IP地址127.0.0.1以及端口8230进行客户端上传文件给服务器端

案例分析:

对于客户端来说:首先需要有文件上传,这个文件一开始在硬盘中,所以需要用到基本流或者缓冲流读取文件到内存中,再通过网络编程将读取到的文件通过写的方式输出到服务器端

对于服务器端来说:接收到客户端的文件后,需要将接受到的文件保存到本地硬盘,所以此处也需要使用到基本流或者缓冲流写文件到硬盘中

整体过程如下图所示:

创建客户端

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
public class Client {
    public static void main(String[] args) throws Exception{
        // 指定服务器端口和IP地址
        Socket socket = new Socket("127.0.0.1", 8230);
        // 读取磁盘文件
        BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream("C:\\Users\\18483\\Music\\conan.mp3"));
        int len = 0;
        byte[] bytes = new byte[1024];
        // 向服务器写数据
        OutputStream outputStream = socket.getOutputStream();
        while((len = bufferedInputStream.read(bytes)) != -1) {
            outputStream.write(bytes, 0, len);
        }
        // 向服务器写入结束标记
        socket.shutdownOutput();

        // 接收服务器的结果
        InputStream inputStream = socket.getInputStream();
        byte[] bytes1 = new byte[1024];
        int len1 = inputStream.read(bytes1);
        System.out.println(new String(bytes1, 0, len1));
        // 关流
        inputStream.close();
        outputStream.close();
        bufferedInputStream.close();
        socket.close();
    }
}

需要注意,在上面的代码中,一定要向服务器写入结束标记,因为客户端读取文件遇到结束标记时就不会再继续进入循环,此时如果没有向服务器写入结束标记,则服务器会一直处于接收状态,导致死循环

写入结束标记可以使用Socket类中的方法:void shutdownOutput()

创建服务器端

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
public class Server {
    public static void main(String[] args) throws Exception{
        ServerSocket serverSocket = new ServerSocket(8230);
        // 接收客户端请求
        Socket accept = serverSocket.accept();
        // 读取客户端的数据
        InputStream inputStream = accept.getInputStream();
        // 向硬盘中写入接收到的数据
        byte[] bytes = new byte[1024];
        BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream("./conan.mp3"));
        int len = 0;
        while ((len = inputStream.read(bytes)) != -1) {
            bufferedOutputStream.write(bytes, 0, len);
        }

        // 响应客户端
        OutputStream outputStream = accept.getOutputStream();
        outputStream.write("服务器接收文件完成".getBytes());
        // 关流
        outputStream.close();
        bufferedOutputStream.close();
        inputStream.close();
        accept.close();
        serverSocket.close();
    }
}

拓展

在上面的基础上,可以实现不同的设备之间在局域网下文件的传输,只需要改变客户端IP地址为局域网内另一台设备的IP即可

结合多线程

结合多线程的本质就是服务端每一次收到一个客户端传输数据就创建一个线程,但是使用多线程编写服务器端就会遇到异常无法使用throws的问题,此时就必须使用try...catch,为了保证关流的部分一定执行,关流部分就需要放到finally中,此时就会涉及到作用域的问题,为了方便控制,可以将对应的关流方案抽取放到一个工具类的静态方法中

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
33
34
35
public class Server {
    public static void main(String[] args) throws Exception{
        ServerSocket serverSocket = new ServerSocket(8230);
        // 接收客户端请求
        Socket accept = serverSocket.accept();

        new Thread(new Runnable() {
            InputStream inputStream = null;
            BufferedOutputStream bufferedOutputStream = null;
            OutputStream outputStream = null;
            @Override
            public void run() {
                try {
                    // 读取客户端的数据
                    inputStream = accept.getInputStream();
                    // 向硬盘中写入接收到的数据
                    byte[] bytes = new byte[1024];
                    bufferedOutputStream = new BufferedOutputStream(new FileOutputStream("./conan.mp3"));
                    int len = 0;
                    while ((len = inputStream.read(bytes)) != -1) {
                        bufferedOutputStream.write(bytes, 0, len);
                    }

                    // 响应客户端
                    outputStream = accept.getOutputStream();
                    outputStream.write("服务器接收文件完成".getBytes());
                } catch (Exception e) {
                    e.printStackTrace();
                }
                // 关流
                CloseUtils.close(serverSocket, inputStream, bufferedOutputStream, outputStream, accept);
            }
        }).start();
    }
}

工具类:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class CloseUtils {
    public static void close(ServerSocket serverSocket, InputStream inputStream, BufferedOutputStream bufferedOutputStream, OutputStream outputStream, Socket accept) {
        try {
            if (outputStream != null) {
                outputStream.close();
            } else if (bufferedOutputStream != null) {
                bufferedOutputStream.close();
            } else if (inputStream != null) {
                inputStream.close();
            } else if (accept != null) {
                accept.close();
            } else if (serverSocket != null) {
                serverSocket.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

结合线程池

基本思路与前面一致,只是通过线程池限制创建的线程个数,可以使用一个循环,持续接收请求,为了保证多次接收到文件保存到服务器不会覆盖前面的文件,可以随机文件名,这里可以使用随机的字符串形式的UUID作为文件名

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class Server {
    public static void main(String[] args) throws Exception{
        ServerSocket serverSocket = new ServerSocket(8230);
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        int cnt = 0;
        while (cnt++ < 3) {
            // 接收客户端请求
            Socket accept = serverSocket.accept();
            System.out.println(cnt);

            executorService.submit(new Thread(new Runnable() {
                InputStream inputStream = null;
                BufferedOutputStream bufferedOutputStream = null;
                OutputStream outputStream = null;
                @Override
                public void run() {
                    try {
                        // 读取客户端的数据
                        inputStream = accept.getInputStream();
                        // 向硬盘中写入接收到的数据
                        byte[] bytes = new byte[1024];
                        // 使用UUID随机文件名
                        String s = UUID.randomUUID().toString();
                        bufferedOutputStream = new BufferedOutputStream(new FileOutputStream("./"+s+".mp3"));
                        int len = 0;
                        while ((len = inputStream.read(bytes)) != -1) {
                            bufferedOutputStream.write(bytes, 0, len);
                        }

                        // 响应客户端
                        outputStream = accept.getOutputStream();
                        outputStream.write("服务器接收文件完成".getBytes());
                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        // 关流
                        CloseUtils.close(inputStream, bufferedOutputStream, outputStream, accept);
                    }
                }
            }));
        }
        // 接收3次请求后自动关闭
        serverSocket.close();
        executorService.shutdown();
    }
}

工具类:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class CloseUtils {
    public static void close(InputStream inputStream, BufferedOutputStream bufferedOutputStream, OutputStream outputStream, Socket accept) {
        try {
            if (outputStream != null) {
                outputStream.close();
            } else if (bufferedOutputStream != null) {
                bufferedOutputStream.close();
            } else if (inputStream != null) {
                inputStream.close();
            } else if (accept != null) {
                accept.close();
            } 
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

正则表达式

正则表达式是一个具有特殊规则的字符串,一般用于校验输入的数据,例如电话号码,QQ号等

String类中,有一个方法可以判断字符串是否匹配参数的正则表达式:boolean matches(String regex)

Note

一般可以使用网站生成对应的正则表达式,例如:正则表达式生成

字符类

java.util.regex.Pattern:正则表达式的编译表示形式。

正则表达式-字符类:[]表示一个区间,范围可以自己定义

语法示例:

  1. [abc]:代表a或者b或者c字符中的一个
  2. [^abc]:代表除a,b,c以外的任何字符
  3. [a-z]:代表a-z的所有小写字符中的一个
  4. [A-Z]:代表A-Z的所有大写字符中的一个
  5. [0-9]:代表0-9之间的某一个数字字符
  6. [a-zA-Z0-9]:代表a-z或者A-Z或者0-9之间的任意一个字符
  7. [a-dm-p]:a 到 d 或 m 到 p之间的任意一个字符

Note

字符与字符之间没有其他符号时,每一个字符之间为或的关系

每一个[]匹配一个字符,多个[]并列匹配多个字符

示例如下:

Java
1
2
3
4
5
6
public class Test {
    public static void main(String[] args) {
        boolean matches = "acf".matches("[abc][bcd][cdf]");
        System.out.println(matches);
    }
}

逻辑运算符

语法示例:

  1. &&:并且
  2. |:或者
Java
1
2
3
4
5
6
public class Test01 {
    public static void main(String[] args) {
        boolean matches = "a".matches("[[a]&&[^b]]");
        System.out.println(matches);
    }
}

Note

需要注意,使用&&连接两个区间需要在外侧使用一对[]包裹该部分,如果不写任何逻辑运算符

预定义字符

语法示例:

  1. .: 匹配任何字符(重点),注意:不能加[]
  2. \\d:任何数字[0-9]的简写(重点)
  3. \\D:任何非数字[^0-9]的简写
  4. \\s: 空白字符:[ \t\n\x0B\f\r]的简写
  5. \\S: 非空白字符:[^\s] 的简写
  6. \\w:单词字符:[a-zA-Z_0-9]的简写(重点)
  7. \\W:非单词字符:[^\w]
Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class Test02 {
    public static void main(String[] args) {
        //1.验证字符串是否是三位数字
        //boolean result01 = "111".matches("[0-9][0-9][0-9]");
        boolean result01 = "111".matches("\\d\\d\\d");
        System.out.println("result01 = " + result01);

        //2.验证手机号: 1开头 第二位3 5 8 剩下的都是0-9的数字
        boolean result02 = "13838381438".matches("[1][358]\\d\\d\\d\\d\\d\\d\\d\\d\\d");
        System.out.println("result02 = " + result02);

        //3.验证字符串是否以h开头,d结尾,中间是任意一个字符
        boolean result03 = "had".matches("[h].[d]");
        System.out.println("result03 = " + result03);
    }
}

数量词

  1. X?x出现的数量为 0次或1次
  2. X*x出现的数量为 0次到多次 任意次
  3. X+x出现的数量为 1次或多次 ,即X>=1
  4. X{n}:x出现的数量为 恰好n次 ,即X=n
  5. X{n,}:x出现的数量为 至少n次 ,即X>=n
  6. X{n,m}:x出现的数量为 nm次(nm都是包含的),即[n, m]
Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class Test03 {
    public static void main(String[] args) {
        //1.验证字符串是否是三位数字
        boolean result01 = "111".matches("\\d{3}");
        System.out.println("result01 = " + result01);
        //2.验证手机号: 1开头 第二位3 5 8 剩下的都是0-9的数字
        boolean result02 = "13838381438".matches("[1][358]\\d{9}");
        System.out.println("result02 = " + result02);

        //3.验证qq号:  不能是0开头,都是数字,长度为5-15
        boolean result03 = "111111".matches("[1-9][0-9]{4,14}");
        System.out.println("result03 = " + result03);
    }
}

分组括号

分组括号的作用:括号中的内容必须并列同时出现

例如:

Java
1
2
3
4
5
6
7
public class Test04 {
    public static void main(String[] args) {
        //校验abc可以出现任意次
        boolean result = "abcabc".matches("(abc)*");
        System.out.println("result = " + result);
    }
}

String类中其他关于正则表达式的方法

  1. boolean matches(String regex):判断字符串是否匹配给定的正则表达式。
  2. String[] split(String regex):根据给定正则表达式的匹配拆分此字符串。
  3. String replaceAll(String regex, String replacement):把满足正则表达式的字符串,替换为新的字符

例如:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class Test05 {
    public static void main(String[] args) {
        //String[] split(String regex) 根据给定正则表达式的匹配拆分此字符串。
        String s1 = "abc hahah  hehe   hdhshsh";
        String[] arr1 = s1.split(" +");
        System.out.println(Arrays.toString(arr1));
        //String replaceAll(String regex, String replacement)把满足正则表达式的字符串,替换为新的字符
        String s2 = s1.replaceAll(" +", "z");
        System.out.println("s2 = " + s2);
    }
}

设计模式

介绍

设计模式(Design pattern),是一套被反复使用、经过分类编目的、代码设计经验的总结,使用设计模式是为了可重用代码、保证代码可靠性、程序的重用性,稳定性。

1995 年,GoF(Gang of Four,四人组)合作出版了《设计模式:可复用面向对象软件的基础》一书,共收录了 23 种设计模式。

总体来说设计模式分为三大类:

  1. 创建型模式,用于创建对象,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式
  2. 结构型模式,用于对功能进行增强,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式
  3. 行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式

模版方法设计模式

模板方法(Template Method)模式:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。明确了一部分功能,而另一部分功能不明确需要延伸到子类中实现

例如:开车行为,所有人都需要先买车、上车和启动车子,但是目的地不同,所以前往不同的目的地就需要对应的子类单独实现

示例代码;

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
33
34
35
36
37
38
39
// 自定义抽象类
public abstract class Drive {
    public void drive() {
        System.out.println("买车");
        System.out.println("上车");
        System.out.println("启动汽车");
        // 需要子类实现
        dest();
    }

    public abstract void dest ();
}

// 子类实现
public class Peter extends Drive{
    @Override
    public void dest() {
        System.out.println("去武汉");
    }
}

public class David extends Drive{
    @Override
    public void dest() {
        System.out.println("去上海");
    }
}

// 测试
public class Test {
    public static void main(String[] args) {
        Peter peter = new Peter();
        David david = new David();

        peter.drive();
        System.out.println();
        david.drive();
    }
}

单例模式

目的:让一个类只产生一个对象,供外界使用

单例模式分为两类:

  1. 饿汉模式:在加载类时就创建对象
  2. 懒汉模式:需要时再创建对象

饿汉模式

  1. 因为必须保证类在创建后不可以被他人创建对象,所以需要将构造私有
  2. 因为需要满足加载类时就创建对象,所以需要使用static,并且保证只创建一次
  3. static成员不可以在类外直接类名调用
Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class Hungry {
    // 私有构造
    private Hungry() {

    }

    // 创建私有静态对象
    private static Hungry hungry = new Hungry();

    // 提供方法供外界获取
    public static Hungry getSingleton() {
        return hungry;
    }
}

懒汉模式

  1. 因为必须保证类在创建后不可以被他人创建对象,所以需要将构造私有
  2. 因为是需要时创建对象,所以需要在方法中创建对象,但是必须保证只创建一个对象,后面再调用时返回的是同一个对象,所以需要判断对象引用是否为空,如果为空,证明是第一个对象,否则不是
Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class Lazy {
    // 私有构造
    private Lazy() {
    }

    // 对象引用
    private static Lazy lazy;

    // 获取对象
    public static Lazy getSingleton() {
        if (lazy == null) {
            lazy = new Lazy();
        }

        return lazy;
    }
}

但是,上面的代码存在线程安全问题,如果有多个线程,当其中一个线程进入if语句还没执行完毕时,另一个线程也进入if语句,此时就会就不满足单例模式的要求,所以可以使用下面的方式进行修改,可能的结果效果如下:

Java
1
2
com.epsda.advanced.test_Singleton.Lazy@f1c89c
com.epsda.advanced.test_Singleton.Lazy@1fb6f50

修改方式如下:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class Lazy {
    // 私有构造
    private Lazy() {
    }

    // 对象引用
    private static Lazy lazy;

    // 获取对象
    public synchronized static Lazy getSingleton() {
        if (lazy == null) {
            lazy = new Lazy();
        }

        return lazy;
    }
}

也可以使用静态同步代码块:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Lazy {
    // 私有构造
    private Lazy() {
    }

    // 对象引用
    private static Lazy lazy;

    // 获取对象
    public static Lazy getSingleton() {
        // 对象引用为空再抢锁
        if (lazy == null) {
            synchronized (Lazy.class) {
                // 对象引用为空才创建对象
                if (lazy == null) {
                    lazy = new Lazy();
                }
            }
        }

        return lazy;
    }
}

Note

注意,使用静态同步代码块一定要判断两次对象引用是否为空,第一次用于判断是否需要抢锁,第二次是为了防止两个线程刚好都进入第一个if之后依次拿到锁后创建多个对象

测试代码如下:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class Test {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Lazy.getSingleton());
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Lazy.getSingleton());
            }
        }).start();
    }
}

使用Lombok简化Javabean

Lombok介绍

Lombok通过增加一些“处理程序”,可以让Javabean变得简洁、快速。

Lombok能以注解形式来简化Java代码,提高开发效率。开发中经常需要写的Javabean,都需要花时间去添加相应的getter/setter,也许还要去写构造器、equals等方法,而且需要维护。

Lombok能通过注解的方式,在编译时自动为属性生成构造器、getter/setterequalshashcodetoString方法。出现的神奇就是在源码中没有gettersetter方法,但是在编译生成的字节码文件中有gettersetter方法。这样就省去了手动重建这些代码的麻烦,使代码看起来更简洁些。

为了使注解可以生效,需要进行下面的设置:

Lombok安装

  1. 下载插件:需要注意,IDEA 2022及以上不用下载Lombok插件,IDEA 2022以下需要下载
  2. 导入Lombokjar
  3. 指定位置解压包即可

Lombok常用注解

@Getter@Setter

  • 作用:生成成员变量的getset方法
  • 写在成员变量上,指对当前成员变量有效
  • 写在类上,对所有成员变量有效
  • 注意:静态成员变量无效

@ToString

  • 作用:生成toString()方法
  • 注解只能写在类上

@NoArgsConstructor@AllArgsConstructor

  • @NoArgsConstructor:无参数构造方法
  • @AllArgsConstructor:全参数构造方法
  • 注解只能写在类上

@EqualsAndHashCode

  • 作用:生成hashCode()equals()方法
  • 注解只能写在类上

@Data

  • 作用:生成get/settoStringhashCodeequals,无参构造方法
  • 注解只能写在类上

Note

需要注意,如果使用了@Data想使用有参构造必须手动@AllArgsConstructor,此时想有无参构造需要@NoArgsConstructor

示例如下:

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
// 自定义类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Person {
    private String name;
    private Integer age;
}

// 测试
public class Test {
    public static void main(String[] args) {
        Person person = new Person();
        person.setName("张三");
        person.setAge(10);

        System.out.println(person.getName() + "..." + person.getAge());

        System.out.println("================");

        Person p1 = new Person("李四", 28);
        System.out.println(p1.getName() + "..." + p1.getAge());
    }
}