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

news2025/1/18 6:51:07

简介:

首先我们需要知道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,由于作者能力有限可能有一些错误还烦请大家指出来,我会第一时间进行反思和修改,感谢。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

QT作业1

自由发挥登录窗口的应用场景&#xff0c;实现一个登录窗口界面 头文件代码&#xff1a; #ifndef MYWIDGET_H #define MYWIDGET_H#include <QWidget> #include <QIcon> #include <QLabel> //标签类 #include <QMovie> //动图类 #include <…

Linux操作系统一

一、Linux操作系统通俗认知 假设&#xff0c;我们现在正在做一家外包公司&#xff0c;我们的目标是把这家公司做上市。其中&#xff0c;操作系统就是这家外包公司的老板。我们可以把这家公司的发展阶段分为以下几个阶段&#xff1a; &#xff08;1&#xff09;初创阶段&#x…

查看Linux的Ubuntu的版本

我的Ubuntu版本是 Jammy x86_64&#xff0c;即 Ubuntu 22.04.3 LTS&#xff0c;代号为"Jammy Jellyfish"&#xff0c;架构是 x86_64&#xff08;64位&#xff09;。

微信小程序访问不了阿里云oss图片链接解决办法

以下都有可能导致访问不了oss图片 1.小程序没有加访问白名单 这个需要前端搞,加上白名单,如果是域名加域名白名单,ip的话加ip白名单 2.阿里云设置域名白名单 打开bucket列表,选择对应的bucket 配置这个白名单,配置好以后,开发者工具可以预览了,手机端预览不了,查看自己的路…

【遥感方向EI会议征稿中】第三届遥感与测绘国际学术会议(RSSM 2024)

第三届遥感与测绘国际学术会议&#xff08;RSSM 2024&#xff09; 2024 3rd International Conference on Remote Sensing, Surveying and Mapping 遥感与测绘技术&#xff0c;在全球变化、生态、环境、农、林、气象、人类活动等众多领域发挥了重要作用&#xff0c;受到世界各…

共创共赢|美创科技获江苏移动2023DICT生态合作“产品共创奖”

12月6日&#xff0c;以“5G江山蓝 算网融百业 数智创未来”为主题的中国移动江苏公司2023DICT合作伙伴大会在南京成功举办。来自行业领军企业、科研院所等DICT产业核心力量的百余家单位代表参加本次大会&#xff0c;共话数实融合新趋势&#xff0c;共拓合作发展新空间。 作为生…

1-2算法基础-常用库函数

1.排序 sort(first,last,cmp) first指向要排序范围的第一个元素&#xff0c;从0起 last指向要排序范围的最后一个元素的下一个位置 cmp&#xff08;可选&#xff09;&#xff0c;自定义函数&#xff0c;默认从小到大 评测系统 #include <iostream> #include<algorith…

KUKA机器人坐标点如何赋值?

KUKA机器人坐标点如何赋值? KUKA机器人系统中如何实现将某个点位整体赋值给另一个点位呢? 具体的方法可参考以下内容: 如下图所示,选中某个程序,然后点击下方的打开, 如下图所示,进入程序后,这里有P1和P2两个点位,如果要实现让P2的点位和P1的点位完全相同,除了通过示…

一文详解Java单元测试Junit

文章目录 概述、Junit框架快速入门单元测试概述main方法测试的问题junit单元测试框架优点&#xff1a;使用步骤&#xff1a; 使用案例包结构 Junit框架的常见注解测试 概述、Junit框架快速入门 单元测试概述 就是针对最小的功能单元&#xff08;方法&#xff09;&#xff0c;…

1146-table performance-schema.session_variables don‘t exits打卡navicat连接MySQL报错

navicat连接MySQL时报错&#xff1a; 管理员权限打开cmd 输入下面代码&#xff1a; mysql_upgrade -u root -p --force输入密码 然后就可以正常连接了。 mysql_upgrade检查所有数据库中与mysql服务器当前版本不兼容的所有表。 mysql_upgrade也会升级系统表&#xff0c;以便你…

用23种设计模式打造一个cocos creator的游戏框架----(一)生成器模式

1、模式标准 模式名称&#xff1a;生成器模式 模式分类&#xff1a;创建型 模式意图&#xff1a;将一个复杂对象的构建与它的表示分离&#xff0c;使得同样的构建过程可以创建不同的表示。 结构图&#xff1a; 适用于&#xff1a; 当创建复杂对象的算法应该独立于该对象的…

原生cesium、mars3d、supermap-cesium在vue3+vite中引入

1. 原生cesium 需要下载 yarn add cesiumyarn add vite-plugin-cesium2. mars3d 需要下载 yarn add mars3d mars3d-cesiumyarn add vite-plugin-mars3d3. supermap-cesium 只需要引入官网下载的包&#xff0c;build文件夹下的cesium&#xff0c;以及项目中引入的其他cesiu…

Densely Connected Convolutional Networks(2018.1)

文章目录 Abstract1. Introduction提出问题以前的解决方法我们的方法效果 2. Related Work3. DenseNetsResNets.Dense connectivity.Composite function.Pooling layers.Growth rate.Bottleneck layers.Compression.Implementation Details. 4. Experiments5. DiscussionModel …

C语言之动态内存管理(malloc calloc realloc)

C语言之动态内存管理 文章目录 C语言之动态内存管理1. 为什么要有动态内存管理2. malloc 和 free2.1 malloc2.2 free2.3 例子 3. calloc 和 realloc3.1 calloc3.2 realloc 4. 常见的动态内存错误4.1 对NULL指针的解引⽤操作4.2 对动态开辟空间的越界访问4.3 对⾮动态开辟内存使…

【ARM Trace32(劳特巴赫) 使用介绍 13 -- Trace32 变量篇】

文章目录 Trace32 查看变量值Var.view 查看变量值Var.view 查看数据类型的大小Var.view 根据变量地址查看变量值 Trace32 查看变量值 步骤1 步骤2 步骤3&#xff1a; 步骤4&#xff1a; 查看结构体变量 str_t32 的值 struct t32_str {uint32_t t32_val;uint32_t …

苹果手机ios系统安装了一个免签应用书签webclip描述文件该如何卸载?

随着移动应用的普及&#xff0c;越来越多的用户开始关注到苹果免签的应用。相比于需要通过 App Store 审核和签名的应用&#xff0c;免签应用无需经过苹果的审核过程&#xff0c;可以直接安装和使用。那么&#xff0c;苹果免签应用是如何制作的呢&#xff1f;本文将介绍制作苹果…

震坤行2023安全月活动顺利收官

震坤行2023安全月活动顺利收官 2023年6月&#xff0c;是第22个全国“安全生产月”&#xff0c;主题为 “人人讲安全、个个会应急”。震坤行工业超市(上海)有限公司基于国家 “安全生产月”的主题要求&#xff0c;以及公司具体的安全形势&#xff0c;于6月1日在全公司范围内正式…

计算机毕业设计 基于大数据的智能家居销量数据分析系统的设计与实现 Java实战项目 附源码+文档+视频讲解

博主介绍&#xff1a;✌从事软件开发10年之余&#xff0c;专注于Java技术领域、Python人工智能及数据挖掘、小程序项目开发和Android项目开发等。CSDN、掘金、华为云、InfoQ、阿里云等平台优质作者✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精…

keil添加了头文件仍然报找不到头文件的原因

如图&#xff0c;我在user分组新建Item&#xff0c;可是keil提示头文件不存在&#xff0c;所有的一切设置都是对的&#xff0c;但就是找不到头文件&#xff0c;找了很久&#xff0c;最后才发现是user分组和文件系统中的文件夹不一致的原因。 如图&#xff0c;在分组的文件系统的…