Java网络编程,使用UDP实现TCP(一), 基本实现三次握手

简介:

首先我们需要知道TCP传输和UDP传输的区别,UDP相当于只管发送不管对方是否接收到了,而TCP相当于打电话,需要进行3次握手,4次挥手,所以我们就需要在应用层上做一些功能添加,如:

  • 增加ack机制

  • 增加seq机制

  • 增加超时重传机制

  • 增加MTU机制

  • 增加数据校验机制

即可实现简单的用UDP实现TCP功能。

part1:了解Java网络编程如何实现UDP和TCP

UDP:

UDP客户端发送数据:

  • 创建UDP套接字:使用DatagramSocket类创建一个UDP套接字,用于发送和接收UDP数据报。可以指定端口号或让系统自动分配一个可用端口。

  • 准备发送的数据,转成字节数组。

  • 构造UDP数据报:创建一个DatagramPacket对象,用于封装要发送的数据和目标主机的信息。需要提供要发送的数据的字节数组、数据的长度、目标主机的IP地址和端口号。

  • 发送数据报:使用UDP套接字的send()方法将封装好的数据报发送给目标主机。该方法接受一个DatagramPacket对象作为参数。

  • 关闭套接字:使用UDP套接字的close()方法关闭套接字,释放相关的资源。

import java.io.IOException;
import java.net.*;

public class UDPClient {
    public static void main(String[] args) throws IOException {
        System.out.println("发送启动中。。。");
        
        //1. 使用 DatagramSocket(8888)
        DatagramSocket datagramSocket = new DatagramSocket(8888);

        //2. 准备数据,一定要转成字节数组
        String data = "hello java";
        //创建数据,并把数据打包
        byte[] datas = "hello java".getBytes();
        DatagramPacket datagramPacket = new DatagramPacket(datas, 0,datas.length, new InetSocketAddress("localhost",9999));

        //调用对象发送数据
        datagramSocket.send(datagramPacket);

        //关闭流
        datagramSocket.close();
    }
}

 UDP服务端接收数据:

  • 创建UDP套接字:使用DatagramSocket类创建一个UDP套接字,用于发送和接收UDP数据报。可以指定端口号或让系统自动分配一个可用端口。

  • 创建一个字节数组用于接收发送的数据。

  • 构造UDP数据报:创建一个DatagramPacket对象,用于封装要发送的数据和目标主机的信息。需要提供要发送的数据的字节数组、数据的长度、目标主机的IP地址和端口号。

  • 发送数据报:使用UDP套接字的receive()方法将封装好的数据报发送给目标主机。该方法接受一个DatagramPacket对象作为参数。

  • 关闭套接字:使用UDP套接字的close()方法关闭套接字,释放相关的资源。

package TCP_UDP_Practice.UDPrecieve;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;

public class UDPClient {
    public static void main(String[] args) throws IOException {
        System.out.println("接收方接收中。。。");
        DatagramSocket datagramSocket = new DatagramSocket(9999);
        byte[] container = new byte[1024 * 60];
        DatagramPacket packet = new DatagramPacket(container, 0, container.length);
        datagramSocket.receive(packet);
        System.out.println(new String(packet.getData(), 0, packet.getLength()));
        datagramSocket.close();
    }
}

TCP:

TCP客户端发送数据:

  • 创建TCP客户端套接字:在服务器接受到客户端的连接请求后,将创建一个新的TCP套接字,用于和客户端进行通信。服务器套接字和客户端套接字之间建立了一条连接。

  • 数据传输:使用客户端套接字和服务器套接字进行数据传输。可以使用输出流来写入数据。服务器套接字和客户端套接字之间可以进行双向的数据传输。

  • 关闭连接:当数据传输完成或需要关闭连接时,可以调用套接字的close()方法关闭连接。关闭连接后,服务器套接字将继续监听新的连接请求。

package TCP_UDP_Practice.TCPsendMsg;

import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;

public class ClientDemo {
    public static void main(String[] args) throws IOException {
        Socket socket = new Socket("127.0.0.1", 10005);
        //创建输入流对象,写入数据
        OutputStream outputStream = socket.getOutputStream();
        outputStream.write("hello tcp".getBytes());
        //关闭流
        socket.close();
    }
}

TCP服务端接收数据:

  • 创建TCP服务器套接字:使用ServerSocket类创建一个TCP服务器套接字,用于监听客户端的连接请求。需要指定服务器的端口号。
  • 数据传输:使用客户端套接字和服务器套接字进行数据传输。可以使用输入流来读取数据。服务器套接字和客户端套接字之间可以进行双向的数据传输。

  • 关闭连接:当数据传输完成或需要关闭连接时,可以调用套接字的close()方法关闭连接。关闭连接后,服务器套接字将继续监听新的连接请求。

package TCP_UDP_Practice.TCPrecieve;

import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class ServerDemo {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(10005);

        Socket accept = serverSocket.accept();

        //获取输入流
        InputStream inputStream = accept.getInputStream();
        byte[] bytes = new byte[1024];
        int read = inputStream.read(bytes);
        String s = new String(bytes, 0, read);
        System.out.println("数据是:" + s);

        //关闭流
        serverSocket.close();
    }
}

Part2:用UDP如何实现TCP的三次握手?

参考《TCP/IP详解》卷一的424页,我们可以得知三次握手须传输的主要数据有SYN, Seq和ACK,接下来我将详细说说三次握手这些数据有何变化,如何获取。

第一次握手:

  • 客户端会发送一个SYN 报文段(即一个在TCP头部位置SYN位置的TCP/IP数据包),并指明自己想要连接到的端口号和它的客户端初始序列号ISN。客户端发送的这个SYN报文段称为段1。
  •  那么问题来了:SYN,ISN到底如何获取,如何用Java程序写出来呢?
    • SYN:(Synchronize)是TCP(传输控制协议)中的一个标志位,用于建立连接的过程中进行同步。在TCP三次握手的过程中,SYN用于表示发起连接请求的一方(通常是客户端)希望建立连接。SYN标志位的值为1,表示发起连接请求或确认连接请求。
    • Seq:(Sequence Number)是用于标识数据字节顺序的字段。每个TCP报文段都包含一个Seq字段,用于指示报文段中的数据字节在整个数据流中的位置。

      • Seq字段的值表示报文段中的第一个数据字节的序列号。每个字节都有一个唯一的序列号,序列号从一个初始值开始,并随着每个传输的字节递增。

      • 在TCP连接建立后,双方会通过ISN(Initial Sequence Number)来初始化序列号。ISN是一个随机选择的32位无符号整数,用作初始的序列号。之后,发送方在发送数据时,会为每个报文段分配一个递增的序列号。

      • 接收方在接收到报文段时,根据Seq字段的值来确定数据字节的顺序。如果接收方发现某个报文段的Seq值不连续或重复,它会通知发送方进行相应的处理,以确保数据的正确传输和重组。

      • Seq字段的作用是保证TCP数据的有序性和可靠性。通过正确的序列号,接收方可以按正确的顺序重组数据,并检测丢失或重复的数据。

      • 需要注意的是,Seq字段的范围是32位无符号整数,因此序列号会在达到最大值后重新从0开始循环。

    • ISN:(Initial Sequence Number)是TCP(传输控制协议)中用于初始化序列号的值。序列号用于标识TCP报文段中的数据字节顺序,以便接收方可以按正确的顺序重组数据。

      在TCP连接建立时,双方需要协商一个初始的序列号。

      • ISN是一个随机选择的32位无符号整数,通常由操作系统生成。ISN的选择是为了增加连接的安全性,防止恶意攻击者猜测序列号并插入伪造的数据。

      • ISN的选择是根据一些算法和系统状态进行的,具体的实现可能因操作系统而异。通常,ISN的选择会考虑到时间、IP地址、端口号等因素,以确保序列号的唯一性和随机性。在[RFC1948]中提出了一个较好的初始化序列号ISN随机生成算法。ISN = M + F(localhost, localport, remotehost, remoteport). 

        注意:M是一个计时器,这个计时器每隔4毫秒加1。F是一个Hash算法,根据源IP、目的IP、源端口、目的端口生成一个随机数值。要保证hash算法不能被外部轻易推算得出,用MD5算法是一个比较好的选择。
      • 一旦双方在三次握手过程中成功建立连接,ISN就会被用作初始的序列号,并在后续的数据传输中递增。序列号的递增是为了确保数据的有序传输和重组。

      • 需要注意的是,ISN是每个TCP连接独立选择的,不同的连接会有不同的ISN。这样可以避免一个连接中的序列号被用于另一个连接,从而增加连接的安全性。

ISN初始化代码如下:

package TCP_handShake;

import java.time.LocalDateTime;
import java.util.UUID;

/**
 * 初始化Seq的值ISN
 * RFC1948中提出了一个较好的初始化序列号ISN随机生成算法:
 * ISN = M + F(localhost, localport, remotehost, remoteport).
 *
 */
public class initializeISN {
    private int ISN = generateISN() ;

    public int getISN() {
        return ISN;
    }

    private int generateISN(){
        // 获取当前时间
        String currentTime = String.valueOf(LocalDateTime.now().getSecond());

        // 生成UUID
        UUID uuid = UUID.randomUUID();

        // 将时间和UUID结合生成ISN
        String isnString = currentTime + uuid.toString();
        int isn = isnString.hashCode();

        return isn;
    }
}


在我的代码中,由于我的目的是简单的实现,所以并未采用 [RFC1948]提到的算法,而是使用当前时间的秒数(通过LocalDateTime类得到)和UUID进行字符串拼接,实现了唯一性。(由于没有做到后面的内容,如后续如发现有问题,会进行更改

SYN和Seq初始化代码如下

package TCP_handShake;

/**
 * 标志位 connectionMarks
 */
public class ConnectionMarks extends initializeISN{
    //每次建立新连接,将SYN初始化为1
    private int SYN;
    //获取ISN
    private int Seq;

    public ConnectionMarks() {
        this.SYN = 1;
        this.Seq = getISN();
    }


    public int getSeq() {
        return Seq;
    }


    //setter of SYN
    public Integer getSYN() {
        return SYN;
    }
}

第一次握手客户端发送数据:

 System.out.println("第一次握手:");
        System.out.println("正在发送SYN和Seq......");

        //1. 使用 DatagramSocket(8888)
        DatagramSocket datagramSocket = new DatagramSocket(8888);
        ConnectionMarks connectionMarks = new ConnectionMarks();
        String SYN = String.valueOf(connectionMarks.getSYN());
        //getSeq() 方法值等同于 getISN(),获取ISN(c)
        int ISN1 = connectionMarks.getSeq();
        String Seq = String.valueOf(ISN1);

        //2. 准备数据,一定要转成字节数组
        String data = SYN + " " + Seq;
        //创建数据,并把数据打包
        byte[] datas = data.getBytes();
        DatagramPacket datagramPacket = new DatagramPacket(datas, 0,datas.length, new InetSocketAddress("localhost",9999));

        //调用对象发送数据
        datagramSocket.send(datagramPacket);
        //关闭流
        datagramSocket.close();

第一次握手服务端接收数据:

  System.out.println("接收数据:...");
        //创建接收端对象
        DatagramSocket datagramSocket = new DatagramSocket(9999);

        //创建数据包,用于接收数据
        byte[] bytes = new byte[1024];
        DatagramPacket datagramPacket = new DatagramPacket(bytes, bytes.length);

        datagramSocket.receive(datagramPacket);
        String s = new String(datagramPacket.getData(), 0, datagramPacket.getLength());

        //解析数据包并且输出显示
        System.out.println("数据为: " + s);
        //关闭流
        datagramSocket.close();

第二次握手:

  1. 服务端收到客户端的SYN包(SYN=j)后,需要回复一个SYN+ACK的包给客户端。
  2. 这个SYN+ACK的包里,ACK的值为j+1,表示"我已经收到你的SYN了"。
  3. 同时,服务端也会发送自己的SYN包,序列号为ISN(s),这个序列号是服务端自己生成的。

服务端在第二次握手中发送的包,其SYN和ACK标志位都被设置为1(SYN+ACK),序列号(Seq)为服务端自己生成的初始序列号(ISN(s)),确认号(ACK)为客户端的初始序列号加1(ISN(c)+1)。

注意:此处ACK为一个flag标志位,只是说明得到了ACK

在connectionMark类补充ACKMark的初始化

package TCP_handShake;

/**
 * 标志位 connectionMarks
 */
public class ConnectionMarks extends initializeISN{
    //每次建立新连接,将SYN初始化为1
    private int SYN;
    //随机
    private int Seq;

    private  int ACKMark;

    public int getACKMark() {
        return ACKMark;
    }

    public void setACKMark(int ACKMark) {
        this.ACKMark = ACKMark;
    }

    public ConnectionMarks() {
        this.SYN = 1;
        this.Seq = getISN();
        this.ACKMark = 0;
    }


    public int getSeq() {
        return Seq;
    }


    //setter of SYN
    public Integer getSYN() {
        return SYN;
    }
}

第二次握手服务端发送数据:

System.out.println("====================");
        System.out.println("第二次握手:");
        System.out.println("正在发送SYN, Seq 和 ACK......");

        ConnectionMarks connectionMarks = new ConnectionMarks();
        //第二次握手,返回ACK = ISN + 1;
        //生成自己的ISN(s)
        String Seq2 = String.valueOf(connectionMarks.getSeq());
        //ACK2中的ISN为第一次传过来的ISN(c)+1
        String ACK2 = String.valueOf(ISN1+ 1);
        //将ack标志位设为1
        connectionMarks.setACKMark(1);
        String SYN2 = connectionMarks.getSYN() + "/" + connectionMarks.getACKMark();

        //2. 准备数据,一定要转成字节数组
        String data2 = SYN2 + " " + Seq2 + " " + ACK2;

        //创建数据,并把数据打包
        byte[] datas2 = data2.getBytes();
        DatagramPacket datagramPacket2 = new DatagramPacket(datas2, 0,datas2.length, new InetSocketAddress("localhost",8888));

        //调用对象发送数据
        datagramSocket.send(datagramPacket2);

第二次握手客户端接收数据:

System.out.println("====================");
        System.out.println("接收数据:...");
        //创建数据包,用于接收数据
        /**
         * 在第二次握手中,客户端主要会检查两个方面的内容:
         * 检查ACK标志位:客户端需要确认服务端发送的确认信息(SYN-ACK)中的ACK标志位是否已设置。ACK标志位表示服务端确认收到了客户端的握手请求。
         * 检查确认号(ACK):客户端需要检查服务端发送的确认信息中的确认号(ACK)是否正确。确认号应该是服务端发送的初始序列号加1,用于告知服务端它已经正确接收到服务端的数据。
         */
        byte[] bytes = new byte[1024];
        DatagramPacket datagramPacket2 = new DatagramPacket(bytes, bytes.length);
        datagramSocket.receive(datagramPacket2);
        String s = new String(datagramPacket2.getData(), 0, datagramPacket2.getLength());


        //拆分字符串获取其中的SYN,Seq和ACK
        String[] strArr = s.split(" ");
        String[] flag = strArr[0].split("/");
        //System.out.println(strArr[0]);
        //检验接收信息是否是满足需求的
        if (!(Integer.parseInt(flag[1]) != 0
                        && Integer.parseInt(flag[0]) == 1
                        && Integer.parseInt(strArr[2]) == ISN1 + 1)
        ){
            //TODO 异常提醒,非本次连接,如何处理
            throw new RuntimeException("wrong connection");
        }
        System.out.println("通过校验");
        //解析数据包并且输出显示
        System.out.println("数据为: " + s);

注意:第一次握手服务端不需要进行校验,但是第二次握手用户端就需要进行校验,ACK标志位是否为1,ACK值是否为ISN(c)+1,SYN值是否为1。

第三次握手

第三次握手,客户端会发送以下三个数据:

  1. ACK标志位应该为1,表示确认收到第二次握手客户端发来的消息。
  2. Seq,值和第二次握手服务端传来的ACK相同
  3. ACK值,为第二次握手服务端传来的ISN(s)+1

第三次握手客户端发送数据:

System.out.println("====================");
        //第三次握手
        System.out.println("第三次握手:");
        System.out.println("正在发送SYN, Seq 和 ACK......");
        connectionMarks.setACKMark(1);
        String ackMark = String.valueOf(connectionMarks.getACKMark());
        String Seq3 = strArr[2];
        String ACK3 = String.valueOf(Integer.parseInt(strArr[1]) + 1);
        //2. 准备数据,一定要转成字节数组
        String data3 = ackMark + " " + Seq3 + " " + ACK3;
//        System.out.println("+++++++++++++++++");
//        System.out.println(ACK3);
        //创建数据,并把数据打包
        byte[] datas3 = data3.getBytes();
        DatagramPacket datagramPacket3 = new DatagramPacket(datas3, 0,datas3.length, new InetSocketAddress("localhost",9999));

        //调用对象发送数据
        datagramSocket.send(datagramPacket3);

第三次握手服务端接收数据

 System.out.println("====================");
        System.out.println("接收数据:...");

        //创建数据包,用于接收数据
        byte[] bytes3 = new byte[1024];
        DatagramPacket datagramPacket3 = new DatagramPacket(bytes3, bytes3.length);

        datagramSocket.receive(datagramPacket3);
        String s3 = new String(datagramPacket3.getData(), 0, datagramPacket3.getLength());

        //解析数据包并且输出显示
        System.out.println("数据为: " + s3);

        //拆分字符串获取其中的SYN,Seq和ACK
        String[] strArr3 = s.split(" ");

        //System.out.println(strArr[0]);
        //检验接收信息是否是满足需求的
        if (Integer.parseInt(strArr3[0]) != 1){
            //TODO 异常提醒,非本次连接,如何处理
            throw new RuntimeException("wrong connection");
        }
        System.out.println("通过校验,完成三次握手");

初步总结:

至此完成了简单的三次握手,但是并没有实现超时重传机制,MTU输入缓冲。后续会进行完善和修改,全部代码会在我完成整个TCP通信流程后,开源到GitHub,由于作者能力有限可能有一些错误还烦请大家指出来,我会第一时间进行反思和修改,感谢。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/226573.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Spring基于注解存储对象

小王学习录 前言基于注解存储对象Controller (控制器存储)Service (服务存储)Repository (仓库存储)Component (组件存储)Configuration (配置存储)Bean(方法注解) 前言 上一篇文章中已经介绍了在Spring中存储Bean和取Bean的方法. 而在 Spring 中想要更简单的存储和读取对象的…

让工作更高效,那些不能错过的8款泳道图绘制工具

在现代企业的运营管理中,泳道图扮演了至关重要的角色。这种独特的图表工具以其直观、清晰的特点,帮助我们理解和改进复杂的工作流程,从而提升效率。本文将为你分享8款实用且高效的泳道图绘制工具,它们能够帮助你轻松创建出专业级别…

Emscripten运行时

本章将简要介绍Emscripten环境下与运行时相关的部分知识,包括消息循环、文件系统、内存管理等内容。 main函数与生命周期 生成本地代码时,作为C/C程序的入口函数,通常main()函数意味着程序的整个生命周期,程序随main()函数返回的…

第二十一章网络通信总结博客

局域网与互联网 为了实现两台计算机的通信,必须用一个网络线路连接两台计算机。如下图所示 网络协议 1.IP协议 IP是Internet Protocol的简称,是一种网络协议。Internet 网络采用的协议是TCP/IP协议,其全称是Transmission Control Protocol/I…

对Spring源码的学习:一

目录 BeanFactory开发流程 ApplicationContext BeanFactory与ApplicationContext对比 基于XML方式的Bean的配置 自动装配 BeanFactory开发流程 这里的第三方指的是Spring提供的BeanFactory,Spring启动时会初始化BeanFactory,然后读取配置清单&#…

力扣每日一题:1466. 重新规划路线(2023-12-07)

力扣每日一题 题目:1466. 重新规划路线 日期:2023-12-07 用时:45 m 36 s 时间:37ms 内存:69.64MB 代码: class Solution {public int minReorder(int n, int[][] connections) {list new List[n];Arrays…

IPTABLES(一)

文章目录 1. iptables基本介绍1.1 什么是防火墙1.2 防火墙种类1.3 iptables介绍1.4 包过滤防火墙1.5 包过滤防火墙如何实现 2. iptables链的概念2.1 什么是链2.2 iptables有哪些链 3. iptables表的概念3.1 什么是表3.2 表的功能3.3 表与链的关系 4. iptables规则管理4.1 什么是…

Shell数组函数:数组——数组和循环(二)

for脚本快速定义数组 [rootlocalhost ~]# vim for12.sh #脚本编辑 #!/bin/bash for a in cat /etc/hosts do hosts[o]$a donefor i in ${!hosts[]} do echo "$i : ${hosts[$a]}" done[rootlocalhost ~]# vim for12.sh #执行脚本区别 :for的空格分割…

coredump

linux原生 一、设置 $ cat /proc/sys/kernel/core_pattern 通过查看core_pattern文件,发现其确实指定了一个路径,于是我前往那个路径,发现竟然是脚本程序,后来查看说明文件,才知道core_pattern中如果首先指定了一个 …

docker 可视化工具操作说明 portainer

官网地址 https://docs.portainer.io/start/install-ce/server/docker/linux 1.First, create the volume that Port docker volume create portainer_data2.下载并安装容器 docker run -d -p 8000:8000 -p 9443:9443 --name portainer --restartalways -v /var/run/docker…

前端:让一个div悬浮在另一个div之上

使用 CSS 的 position 属性和 z-index 属性 首先,将第二个 div 元素的 position 属性设为 relative 或 absolute。这样可以让该元素成为一个定位元素,使得后代元素可以相对于它进行定位。 然后,将要悬浮的 div 元素的 position 属性设为 ab…

DOS 批处理 (二)

DOS 批处理 1. 基础 DOS 命令1.1 基础命令1.2 文件系统操作1.3 文件夹管理1.4 文件管理1.5 网络相关1.6 系统管理1.7 IF、FOR和NETIFFORNET 1. 基础 DOS 命令 command /? 查找帮助DOS命令不区分命令字母的大小写 C:\Users\Administrator>echo 1 1 C:\Users\Administrator…

SQL面试题,判断if的实战应用

有如下表,请对这张表显示那些学生的成绩为及格,那些为不及格 1、创建表,插入数据 CREATE TABLE chapter8 (id VARCHAR(255) NULL,name VARCHAR(255) NULL,class VARCHAR(255) NULL,score VARCHAR(255) NULL );INSERT INTO chapter8 (id, n…

嵌入式系统

嵌入式系统 目前国内一个普遍认同的嵌入式系统定义是:以应用为中心、以计算机技术为基础,软件硬件可裁剪,适应应用系统对功能、可靠性、成本、体积、功耗严格要求的专用计算机系统。(引用自《嵌入式系统设计师教程》) …

点评项目——商户查询缓存

2023.12.7 redis实现商户查询缓存 在企业开发中,用户的访问量动辄成百上千万,如果没有缓存机制,数据库将承受很大的压力。本章我们使用redis来实现商户查询缓存。 原来的操作是根据商铺id直接从数据库查询商铺信息,为了防止频繁地…

一维相位解包裹

一维相位解包裹 本文首先介绍最简单的一维的位相解包裹算法。设W是包裹运算符,中是解包裹位相,是包裹的位相。则一维位相解包裹可表示为: 解包裹就是要选取正确的k,满足: 两个相邻像素位相的差值如下: 由式(2-1)和式(2…

JOSEF快速中间继电器DZK-916 4A AC220V板后嵌入式安装

系列型号 DZK-911快速中间继电器;DZK-912快速中间继电器; DZK-914快速中间继电器;DZK-916快速中间继电器; DZK-917快速中间继电器;DZK-918快速中间继电器; DZK-924快速中间继电器;DZK-934快速中…

德国进口高速主轴电机在机器人上的应用及选型方案

随着机器人技术的日新月异,高速主轴电机在机器人领域的应用也日趋广泛。德国进口的SycoTec高速主轴电机,以其高转速、高精度、高刚度的特点,在机器人的切割、铣削、钻孔、去毛刺等加工应用中发挥着关键作用。 一、高速主轴电机的特点 SycoT…

【项目问题解决】IDEA2020.3 使用 lombok 插件 java: 找不到符号 符号: 方法 builder()

目录 lombok找不到符号问题修改 1.问题描述2.问题原因3.解决思路4.解决方案5.总结6.参考 文章所属专区 项目问题解决 1.问题描述 IDEA2020.3 使用 lombok 插件 java: 找不到符号 符号: 方法 builder(),无法使用lombok下应有的注解,一度怀疑是版本问题 …

什么是SPA(Single Page Application)?它的优点和缺点是什么?

聚沙成塔每天进步一点点 ⭐ 专栏简介 前端入门之旅:探索Web开发的奇妙世界 欢迎来到前端入门之旅!感兴趣的可以订阅本专栏哦!这个专栏是为那些对Web开发感兴趣、刚刚踏入前端领域的朋友们量身打造的。无论你是完全的新手还是有一些基础的开发…