java springBoot 一个demo搞定大文件上传 分片上传 断点续传 秒传

news2024/11/27 16:49:12

java springBoot js 大文件上传分片上传 断点续传 秒传

文件上传在项目开发中再常见不过了,大多项目都会涉及到图片、音频、视频、文件的上传,通常简单的一个Form表单就可以上传小文件了,但是遇到大文件时比如1GB以上,或者用户网络比较慢时,简单的文件上传就不能适用了,会出现以下隐患或问题
1、网络传输速度慢
上传时间长,大文件完整上传需要占用持续稳定的上行带宽,如果网络条件不好,上传会非常慢,损耗用户体验。
2、中间失败需重新上传
上传过程中如果由于网络等原因发生中断,整个传输会失败。这就需要用户重新再上传一遍完整文件,重复劳动。
3、服务器压力大
服务端需要占用较多资源持续处理一个大文件,对服务器性能压力较大,可能影响到其他服务。
4、流量资源浪费
一次完整上传大文件,如果遇到已经存在相同文件,会重复消耗大量网络流量,是数据浪费。
5、难以实现上传进度提示
用户无法感知上传进度,如果上传失败也不知道已经上传了多少数据。

所以为了解决以上这些问题,使用分片、断点续传等技术就非常重要。它可以分批次上传数据块,避免一次性全量上传的弊端。同时结合校验、记录已上传分片等手段,可以使整个上传过程可控、可恢复、节省流量,大幅提升传输效率。

大文件上传主要是借助于分治思想(任务分解法)实现,将大文件分解为多个易处理的小文件,并循环递归的上传小文件,最后把子问题的结果合并为原始问题的结果。

在这里插入图片描述

小文件上传

文件小传非常的简单,本项目后端我们使用SrpingBoot 3.1.2 + JDK17,前端我们使用原生的JavaScript+spark-md5.min.js实现。

项目引入

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.3</version>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

    </dependencies>

后端代码

    public static final String UPLOAD_PATH = "D:\\upload\\";

    /**
     * 小文件上传
     * @param file
     * @return
     * @throws IOException
     */
    @PostMapping(value = "/upload", consumes = "multipart/form-data")
    public ResponseEntity<Map<String, String>> upload(@RequestParam MultipartFile file) throws IOException {
        File dstFile = new File(UPLOAD_PATH, String.format("%s.%s", UUID.randomUUID(), StringUtils.getFilename(file.getOriginalFilename())));
        file.transferTo(dstFile);
        Map result = new HashMap();
        result.put("path", dstFile.getAbsolutePath());
        return ResponseEntity.ok(result);
    }

前端代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>upload</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/spark-md5/3.0.2/spark-md5.min.js"></script>
</head>
<body>
upload
<form enctype="multipart/form-data">
    <input type="file" name="fileInput" id="fileInput">
    <input type="button" value="上传" onclick="uploadFile()">
</form>
上传结果
<span id="uploadResult"></span>
<script>
    var  uploadResult=document.getElementById("uploadResult")
    function uploadFile() {
        var fileInput = document.getElementById('fileInput');
        var file = fileInput.files[0];
        if (!file) return; // 没有选择文件

        var xhr = new XMLHttpRequest();
        // 处理上传进度
        xhr.upload.onprogress = function(event) {
            var percent = 100 * event.loaded / event.total;
            uploadResult.innerHTML='上传进度:' + percent + '%';
        };
        // 当上传完成时调用
        xhr.onload = function() {
            if (xhr.status === 200) {
                uploadResult.innerHTML='上传成功'+ xhr.responseText;
            }
        }
        xhr.onerror = function() {
            uploadResult.innerHTML='上传失败';
        }
        // 发送请求
        xhr.open('POST', 'http://localhost:8080/demo/api/upload', true);
        var formData = new FormData();
        formData.append('file', file);
        xhr.send(formData);
    }
</script>
</body>
</html>

在这里插入图片描述

注意事项

在上传过程会报文件大小限制错误,主要有三个参数需要设置:

server:
  port: 8080
  servlet:
    context-path: /demo
spring:
  servlet:
    multipart:
      max-file-size: 1024MB
      max-request-size: 1024MB

大文件分片上传

如何实现分片上传?

1、分片上传的目的是将大文件切割为多个小块,实现并发上传以提高传输速度。
2、分片上传可以按配置的分片大小(例如1M一个分片)将大文件分割。
3、前端项目将切割的每个分片按顺序上传至后端服务器。
4、后端收到分片后可以暂存于本地,并记录这个分片的特征信息,如分片序号、文件MD5等,写入到数据库。
5、全部分片上传完成后,后端按序号顺序重新组装成原完整文件。
在这里插入图片描述
在这里插入图片描述

前端

大文件分片上传前端主要有三步:
在这里插入图片描述
前端上传代码计算文件MD5值用了spark-md5这个库,使用也是比较简单的。这里为什么要计算MD5简单说一下,因为文件在传输写入过程中可能会出现错误,导致最终合成的文件可能和原文件不一样,所以要对比一下前端计算的MD5和后端计算的MD5是不是一样,保证上传数据的一致性。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>upload</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/spark-md5/3.0.2/spark-md5.min.js"></script>
</head>
<body>
upload
上传结果
<span id="uploadResult"></span>
分片上传
<form enctype="multipart/form-data">
    <input type="file" name="fileInput" id="bigFileInput">
    <input type="button" value="计算文件MD5" onclick="calculateFileMD5()">
    <input type="button" value="上传" onclick="uploadBigFile()">
    <input type="button" value="检测文件完整性" onclick="checkFile()">
</form>

<p>
    文件MD5:
    <span id="fileMd5"></span>
</p>
<p>
    上传结果:
    <span id="bigUploadResult"></span>
</p>
<p>
    检测文件完整性:
    <span id="checkFileRes"></span>
</p>
<script>
   //每片的大小
    var chunkSize = 1 * 1024 * 1024;
    var bigUploadResult = document.getElementById("bigUploadResult")
    var fileMd5Span = document.getElementById("fileMd5")
    var checkFileRes = document.getElementById("checkFileRes")
    var  fileMd5;

    function  calculateFileMD5(){
        var fileInput = document.getElementById('bigFileInput');
        var file = fileInput.files[0];
        getFileMd5(file).then((md5) => {
            console.info(md5)
        fileMd5=md5;
        fileMd5Span.innerHTML=md5;
    })
    }
    function uploadBigFile() {
        var fileInput = document.getElementById('bigFileInput');
        var file = fileInput.files[0];
        if (!file) return;
        if (!fileMd5) return;
        //获取到文件
        let fileArr = this.sliceFile(file);
        //保存文件名称
        let fileName = file.name;

        let start = new Date();
        fileArr.forEach((e, i) => {
            //创建formdata对象
            let data = new FormData();
        data.append("totalNumber", fileArr.length)
        data.append("chunkSize", chunkSize)
        data.append("chunkNumber", i)
        data.append("md5", fileMd5)
        data.append("file", new File([e],fileName));
        upload(data);
        })

        let end = new Date();
        console.log(end - start);
    }

    /**
     * 计算文件md5值
     */
    function getFileMd5(file) {
        return new Promise((resolve, reject) => {
            let fileReader = new FileReader()
            fileReader.onload = function (event) {
            let fileMd5 = SparkMD5.ArrayBuffer.hash(event.target.result)
            resolve(fileMd5)
        }
        fileReader.readAsArrayBuffer(file)
    })
    }
    function upload(data) {
        var xhr = new XMLHttpRequest();
        // 当上传完成时调用
        xhr.onload = function () {
            if (xhr.status === 200) {
                bigUploadResult.append( '上传成功分片:' +data.get("chunkNumber")+'\t' ) ;
            }
            if (xhr.status === 1000) {
                bigUploadResult.append( '分片已存在:秒传成功!' +data.get("chunkNumber")+'\t' ) ;
            }
        }
        xhr.onerror = function () {
            bigUploadResult.innerHTML = '上传失败';
        }
        // 发送请求
        xhr.open('POST', 'http://localhost:8080/demo/api/uploadBig', true);
        xhr.send(data);
    }

    function checkFile() {
        var xhr = new XMLHttpRequest();
        // 当上传完成时调用
        xhr.onload = function () {
            if (xhr.status === 200) {
                checkFileRes.innerHTML = '检测文件完整性成功:' + xhr.responseText;
            }
        }
        xhr.onerror = function () {
            checkFileRes.innerHTML = '检测文件完整性失败';
        }
        // 发送请求
        xhr.open('POST', 'http://localhost:8080/demo/api/checkFile', true);
        let data = new FormData();
        data.append("md5", fileMd5)
        xhr.send(data);
    }
    function sliceFile(file) {
        const chunks = [];
        let start = 0;
        let end;
        while (start < file.size) {
            end = Math.min(start + chunkSize, file.size);
            chunks.push(file.slice(start, end));
            start = end;
        }
        return chunks;
    }
</script>
</body>
</html>

前端注意事项

  1. 前端调用uploadBig接口有四个参数:
    在这里插入图片描述
  2. 计算大文件的MD5可能会比较慢,这个可以从流程上进行优化,比如上传使用异步去计算文件MD5、不计算整个文件MD5而是计算每一片的MD5保证每一片数据的一致性。

后端

后端就两个接口/uploadBig用于每一片文件的上传和/checkFile检测文件的MD5。

/uploadBig接口总体流程:

在这里插入图片描述
这里需要注意的:

  1. MD5.conf每一次检测文件不存在里创建个空文件,使用byte[] bytes = new byte[totalNumber];将每一位状态设置为0,从0位天始,第N位表示第N个分片的上传状态,0-未上传 1-已上传,当每将上传成功后使用randomAccessConfFile.seek(chunkNumber)将对就设置为1。
  2. randomAccessFile.seek(chunkNumber * chunkSize);可以将光标移到文件指定位置开始写数据,每一个文件每将上传分片编号chunkNumber都是不一样的,所以各自写自己文件块,多线程写同一个文件不会出现线程安全问题。
  3. 大文件写入时用RandomAccessFile可能比较慢,可以使用MappedByteBuffer内存映射来加速大文件写入,不过使用MappedByteBuffer如果要删除文件可能会存在删除不掉,因为删除了磁盘上的文件,内存的文件还是存在的。
/checkFile接口设计思路

在这里插入图片描述

@RestController
public class UploadController {

    public static final String UPLOAD_PATH = "D:\\upload\\";

    /**
     * @param chunkSize   每个分片大小
     * @param chunkNumber 当前分片
     * @param md5         文件总MD5
     * @param file        当前分片文件数据
     * @return
     * @throws IOException
     */
    @RequestMapping("/uploadBig")
    public ResponseEntity<Map<String, String>> uploadBig(@RequestParam Long chunkSize, @RequestParam Integer totalNumber, @RequestParam Long chunkNumber, @RequestParam String md5, @RequestParam MultipartFile file) throws IOException {
        //文件存放位置
        String dstFile = String.format("%s\\%s\\%s.%s", UPLOAD_PATH, md5, md5, StringUtils.getFilenameExtension(file.getOriginalFilename()));
        //上传分片信息存放位置
        String confFile = String.format("%s\\%s\\%s.conf", UPLOAD_PATH, md5, md5);
        //第一次创建分片记录文件
        //创建目录
        File dir = new File(dstFile).getParentFile();
        if (!dir.exists()) {
            dir.mkdir();
            //所有分片状态设置为0
            byte[] bytes = new byte[totalNumber];
            Files.write(Path.of(confFile), bytes);
        }
        //随机分片写入文件
        try (RandomAccessFile randomAccessFile = new RandomAccessFile(dstFile, "rw");
             RandomAccessFile randomAccessConfFile = new RandomAccessFile(confFile, "rw");
             InputStream inputStream = file.getInputStream()) {
            //定位到该分片的偏移量
            randomAccessFile.seek(chunkNumber * chunkSize);
            //写入该分片数据
            randomAccessFile.write(inputStream.readAllBytes());
            //定位到当前分片状态位置
            randomAccessConfFile.seek(chunkNumber);
            //设置当前分片上传状态为1
            randomAccessConfFile.write(1);
        }
        return ResponseEntity.ok(Map.of("path", dstFile));
    }


    /**
     * 获取文件分片状态,检测文件MD5合法性
     *
     * @param md5
     * @return
     * @throws Exception
     */
    @RequestMapping("/checkFile")
    public ResponseEntity<Map<String, String>> uploadBig(@RequestParam String md5) throws Exception {
        String uploadPath = String.format("%s\\%s\\%s.conf", UPLOAD_PATH, md5, md5);
        Path path = Path.of(uploadPath);
        //MD5目录不存在文件从未上传过
        if (!Files.exists(path.getParent())) {
            return ResponseEntity.ok(Map.of("msg", "文件未上传"));
        }
        //判断文件是否上传成功
        StringBuilder stringBuilder = new StringBuilder();
        byte[] bytes = Files.readAllBytes(path);
        for (byte b : bytes) {
            stringBuilder.append(String.valueOf(b));
        }
        //所有分片上传完成计算文件MD5
        if (!stringBuilder.toString().contains("0")) {
            File file = new File(String.format("%s\\%s\\", UPLOAD_PATH, md5));
            File[] files = file.listFiles();
            String filePath = "";
            for (File f : files) {
                //计算文件MD5是否相等
                if (!f.getName().contains("conf")) {
                    filePath = f.getAbsolutePath();
                    try (InputStream inputStream = new FileInputStream(f)) {
                        String md5pwd = DigestUtils.md5DigestAsHex(inputStream);
                        if (!md5pwd.equalsIgnoreCase(md5)) {
                            return ResponseEntity.ok(Map.of("msg", "文件上传失败"));
                        }
                    }
                }
            }
            return ResponseEntity.ok(Map.of("path", filePath));
        } else {
            //文件未上传完成,反回每个分片状态,前端将未上传的分片继续上传
            return ResponseEntity.ok(Map.of("chucks", stringBuilder.toString()));
        }

    }
}

配合前端上传演示分片上传,依次按如下流程点击按钮:
在这里插入图片描述
效果
在这里插入图片描述

断点续传

如何实现断点续传?
1、前端在上传文件时,将文件切成多个小块,每次上传一个小块。
2、每上传一个小块,后端会记录这个小块的信息,比如该小块的序号、文件MD5、内容Hash等。可以保存在MySQL数据库中。
3、如果上传中断了,前端可以向后端询问已经上传了哪些小块。
4、后端从数据库中查询,返回已上传小块的信息给前端。
5、前端就可以接着只上传中间中断的那一部分小块。
6、后端会根据小块的序号、文件MD5来把这些小块重新拼接成完整的文件。
7、以后如果这个文件再次上传,通过MD5值就可以知道该文件已经存在了,则直接返回上传成功,无需上传实际内容。这样通过切片上传和持久化记录已上传切片的信息,就可以实现断点续传。
在这里插入图片描述
有了上面的设计做断点续传就比较简单的,后端代码不需要改变,只要修改前端上传流程就好了:
在这里插入图片描述
在这里插入图片描述
用/checkFile接口,文件里如果有未完成上传的分片,接口返回chunks字段对就的位置值为0,前端将未上传的分片继续上传,完成后再调用/checkFile就完成了断点续传

秒传

如何实现秒传?
1、前端在上传文件时,先计算该文件的MD5值,然后将MD5值发送到后端服务器。
2、后端服务器收到MD5值后,查询MySQL数据库,检查是否已存在相同MD5值的文件。
3、如果存在,表示该文件已上传过,服务器直接从数据库查询到该文件存在哪些分片,并返回给客户端。
4、客户端拿到文件分片信息后,会直接组装完整的文件,而不再上传实际文件内容。
5、如果数据库不存在该MD5值,表示文件未上传过,服务器会返回需要客户端上传整个文件。
6、客户端上传完文件后,服务器才会在MySQL数据库中新增该文件与MD5的对应关系,以及存储文件分片信息。
7、下次再上传同样文件时,通过MD5值就可以实现秒传了

秒传也是比较简单的,只要修改前端代码流程就好了,比如张三上传了一个文件,然后李四又上传了同样内容的文件,同一文件的MD5值可以认为是一样的(虽然会存在不同文件的MD5一样,不过概率很小,可以认为MD5一样文件就是一样),10万不同文件MD5相同概率为110000000000000000000000000000\frac{1}{10000000000000000000000000000}100000000000000000000000000001​,福利彩票的中头奖的概率一般为11000000\frac{1}{1000000}10000001​ ,具体计算方法可以参考走近消息摘要–Md5产生重复的概率,,所以MD5冲突的概率可以忽略不计。当李四调用/checkFile接口后,后端直接返回了李四上传的文件路径,李四就完成了秒传。大部分云盘秒传的思路应该也是这样,只不过计算文件HASH算法更为复杂,返回给用户文件路径也更为安全,要防止被别人算出文件路径了。

秒传前端代码流程:
在这里插入图片描述

总体设计思路

在这里插入图片描述
页面访问 http://localhost:8080/demo/index.html 即可

项目链接

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

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

相关文章

WebSocket魔法师:打造实时应用的无限可能

1、背景 在开发一些前端页面的时候&#xff0c;总是能接收到这样的需求&#xff1a;如何保持页面并实现自动更新数据呢&#xff1f;以往的常规做法&#xff0c;是前端使用定时轮询后端接口&#xff0c;获取响应后重新渲染前端页面&#xff0c;这种做法虽然能达到类似的效果&…

Mac电脑Visio文件编辑查看软件推荐Visio Viewer for Mac

mac版Visio Viewer功能特色 在Mac OS X上查看Visio绘图和图表 在Mac OS X上轻松查看MS Visio文件 在Mac上快速方便地打开并阅读Visio文件&#xff08;.vsd&#xff0c;.vsdx&#xff09;。 支持通过放大&#xff0c;缩小&#xff0c;旋转&#xff0c;文本选择和复制&#xff0…

Langchain知识点(上)

输出格式 Pydantic (JSON) 解析器 # 创建模型实例 from langchain import OpenAI model OpenAI(model_nametext-davinci-003)# ------Part 2 # 创建一个空的DataFrame用于存储结果 import pandas as pd df pd.DataFrame(columns["flower_type", "price"…

Python万圣节礼物

文章目录 系列文章前言小海龟快速入门万圣节蝙蝠万圣节南瓜头万圣节礼物尾声 系列文章 序号文章目录直达链接1浪漫520表白代码https://want595.blog.csdn.net/article/details/1306668812满屏表白代码https://want595.blog.csdn.net/article/details/1297945183跳动的爱心http…

MySQL窗口函数——让查询变得更简单

文章目录 一、窗口函数概述1、什么是窗口函数2、窗口函数有哪些&#xff08;1&#xff09;聚合函数&#xff08;聚合函数不是本文讨论的重点&#xff09;&#xff08;2&#xff09;专用窗口函数 3、基本语法4、测试数据准备 二、窗口函数使用1、初识窗口函数&#xff1a;使用聚…

微头条项目实战:新增RequestHeader注解

1、RequestHeader package com.csdn.mymvc.annotation; import java.lang.annotation.*; Target(ElementType.PARAMETER) Retention(RetentionPolicy.RUNTIME) Inherited public interface RequestHeader { }2、DispatcherServlet package com.csdn.mymvc.core; import com.csd…

ssm+vue的项目管理平台(有报告)。Javaee项目,ssm vue前后端分离项目。

演示视频&#xff1a; ssmvue的项目管理平台&#xff08;有报告&#xff09;。Javaee项目&#xff0c;ssm vue前后端分离项目。 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站。 项目介绍…

list部分接口模拟实现(c++)

List list简介list基本框架list构造函数list_node结构体的默认构造list类的默认构造 push_back()iteartor迭代器迭代器里面的其他接口const迭代器通过模板参数实现复用operator->() insert()erase()clear()析构函数迭代器区间构造拷贝构造operator() list简介 - list可以在…

用友NC Cloud accept.jsp接口任意文件上传漏洞复现 [附POC]

文章目录 用友NC Cloud accept.jsp接口任意文件上传漏洞复现 [附POC]0x01 前言0x02 漏洞描述0x03 影响版本0x04 漏洞环境0x05 漏洞复现1.访问漏洞环境2.构造POC3.复现 0x06 修复建议 用友NC Cloud accept.jsp接口任意文件上传漏洞复现 [附POC] 0x01 前言 免责声明&#xff1a…

RVS—面向目标硬件的软件性能测试工具

产品概述 Rapita Verification Suite&#xff08;简称&#xff1a;RVS&#xff09;&#xff0c;为美国Danlaw公司提供的一款嵌入式系统在板测试套件&#xff0c;主要应用于汽车领域。其产品符合ISO-26262、DO178B/C、IEC-61508等行业标准&#xff0c;兼容Vxworks、Linux、SYSBI…

FTP链接如何直接打开不输密码

发布版本时&#xff0c;通过分享链接给负责生产的同事遇到如下较为麻烦的小问题 打开公司FTP链接&#xff1a; ftp://192.168.70.46/Rtos_5/FF615/3602/product/ 会跳出如下弹窗提示输入登录密码&#xff0c;这样对方还需要手动输入密码才能把包下下来&#xff1a; 通过直接给…

【ATTCK】MITRE ATTCK 设计与哲学

MITRE ATT&CK™:设计与哲学 来源&#xff1a;MITRE ATT&CK™: Design and Philosophy 摘要 MITRE ATT&CK知识库描述了网络对手的行为&#xff0c;并为攻击和防御提供了一个通用的分类。它已成为跨许多网络安全领域的一个有用工具&#xff0c;用于传递威胁情报&…

【干货】132道最新K8S面试题汇总~

k8s全称kubernetes&#xff0c;这个名字大家应该都不陌生&#xff0c;k8s是为容器服务而生的一个可移植容器的编排管理工具&#xff0c;越来越多的公司正在拥抱k8s&#xff0c;并且当前k8s已经主导了云业务流程&#xff0c;推动了微服务架构等热门技术的普及和落地&#xff0c;…

日常交流没有障碍,听力就一定正常吗?

你卖灯笼啊&#xff1f; 对啊对啊&#xff0c;我耳朵聋&#xff01; 你这灯笼多少钱呀&#xff1f; 我耳朵聋了好几年啦&#xff01; 这是个笑话 当然也可以从中看出 听力障碍对一个人日常生活的影响 日常交流没障碍 就是听力正常了吗&#xff1f; 首先我们要了解&#xf…

6大顶级团队计划目标管理软件盘点,全行业适用!

在快节奏的现代工作环境中&#xff0c;高效的团队计划和执行是团队取得成功的关键。然而&#xff0c;随着团队规模不断增大、工作任务不断增加&#xff0c;如何提高团队计划与效率成为了一个挑战。幸运的是&#xff0c;有许多先进的软件工具可以帮助团队更好地组织、协调和追踪…

关于c++中数据sqrt() 精度问题

情景介绍 今天在做一个算法题目的时候&#xff0c;发现&#xff0c;当使用sqrt()方法进行开方的时候&#xff0c;一直存在提交不通过的情况。 问题分析 对数据不断分析后&#xff0c;发现对35进行开方后&#xff0c;仍然满足条件&#xff0c;这就存在问题。 sqrt(35) 5.9160…

SOLIDWORKS软件提供了哪些特征造型方法?硕迪科技

SOLIDWORKS作为一款三维设计软件&#xff0c;为用户提供了多种特征造型方法&#xff0c;以下是其中几种常用的&#xff1a; 实体建模特征&#xff1a;SOLIDWORKS使用实体建模技术来创建和编辑三维几何体。通过使用基本几何体&#xff08;如立方体、圆柱体、圆锥体等&#xff09…

【广州华锐互动】楼宇智能化VR虚拟教学系统

在如今的技术时代&#xff0c;教育行业正在逐步引入各种创新方法以提升教学质量。VR公司广州华锐互动开发的楼宇智能化VR虚拟教学系统就是其中的一种&#xff0c;它利用虚拟现实(VR)技术&#xff0c;为学生提供一种全新的、沉浸式的学习体验。 楼宇智能化VR虚拟教学系统涵盖综合…

“我为家乡代言”第三届iEnglish英语风采秀即将启动

近期,第二届iEnglish英语风采秀活动圆满落幕,超2万名来自全国各地的青少年儿童通过此平台展示了他们的英语才华和自信。如今,第三届iEnglish英语风采秀即将于12月底正式启动。 据悉,iEnglish英语风采秀旨在为全国青少年儿童提供一个专属于自己的英语展示舞台,通过英文演讲的模…

19.5 Boost Asio 传输结构体

同步模式下的结构体传输与原生套接字实现方式完全一致&#xff0c;读者需要注意的是在接收参数是应该使用socket.read_some函数读取&#xff0c;发送参数则使用socket.write_some函数实现&#xff0c;对于套接字的解析同样使用强制指针转换的方法。 服务端代码如下所示 #incl…