当前位置:

首页 > 编程开发 > Java如何实现文件断点续传功能?

Java如何实现文件断点续传功能?

本文目录

    什么是断点续传用户上传大文件,网络差点的需要历时数小时,万一线路中断,不具备断点续传的服务器就只能从头重传,而断点续传就是,允许用户从上传断线的地方继续传送,这样大大减少了用户的烦恼。解决上传大文件服务器内存不够的问题解决如果因为其他因素导致上传终止的问题,并且刷新浏览器后仍然能够续传,重启浏览器(关闭浏览器后再打开)仍然能够继续上传,重启电脑后仍然能够上传检测上传过程中因网络波动导致文件出现了内容丢失那么需要自动检测并且从新上传解决方案前端需要进行分割上传的文件需要对上传的分片文件进行指定文件序号需要监

    什么是断点续传

    用户上传大文件,网络差点的需要历时数小时,万一线路中断,不具备断点续传的服务器就只能从头重传,而断点续传就是,允许用户从上传断线的地方继续传送,这样大大减少了用户的烦恼。

    • 解决上传大文件服务器内存不够的问题

    • 解决如果因为其他因素导致上传终止的问题,并且刷新浏览器后仍然能够续传,重启浏览器(关闭浏览器后再打开)仍然能够继续上传,重启电脑后仍然能够上传

    • 检测上传过程中因网络波动导致文件出现了内容丢失那么需要自动检测并且从新上传

    解决方案

    前端

    • 需要进行分割上传的文件

    • 需要对上传的分片文件进行指定文件序号

    • 需要监控上传进度,控制进度条

    • 上传完毕后需要发送合并请求

    Blob 对象,操作文件

    后端

    • 上传分片的接口

    • 合并分片的接口

    • 获取分片的接口

    • 其他工具方法,用于辅助

    前端端需要注意的就是: 文件的切割,和进度条

    后端需要注意的就是: 分片存储的地方和如何进行合并分片

    效果演示

    先找到需要上传的文件

    Java如何实现文件断点续传功能?

    当我们开始上传进度条就会发生变化,当我们点击停止上传那么进度条就会停止

    Java如何实现文件断点续传功能?

    我们后端会通过文件名+文件大小进行MD5生成对应的目录结果如下:

    Java如何实现文件断点续传功能?

    当前端上传文件达到100%时候就会发送文件合并请求,然后我们后端这些分片都将被合并成一个文件

    Java如何实现文件断点续传功能?

    通过下图可以看到所有分片都没有了,从而合并出来一个文件

    Java如何实现文件断点续传功能?

    • 文件上传过程中网络波动导致流丢失一部分(比对大小)

    • 文件上传过程中,服务器丢失分片 (比对分片的连续度)

    • 文件被篡改内容(比对大小)

    效验核心代

    Java如何实现文件断点续传功能?

    参考代码

    前端

    
    
    
    
        
        
        
        Document
    
    
    
    
        

    html5大文件断点切割上传

        
                                import FileSliceUpload  from '../jsutils/FileSliceUpload.js'         let testingUrl="http://localhost:7003/fileslice/testing"         let uploadUrl="http://localhost:7003/fileslice/uploads"         let margeUrl="http://localhost:7003/fileslice/merge-file-slice"         let progressUrl="http://localhost:7003/fileslice/progress"          let fileSliceUpload=  new FileSliceUpload(testingUrl,uploadUrl,margeUrl,progressUrl,"#file")          fileSliceUpload.addProgress("#progressBar")           let btn=  document.querySelector("#btn")           let btn1=  document.querySelector("#btn1")         btn.addEventListener("click",function () {             fileSliceUpload.startUploadFile()         })         btn1.addEventListener("click",function () {             fileSliceUpload.stopUploadFile()         })     
    //大文件分片上传,比如10G的压缩包,或者视频等,这些文件太大了  (需要后端配合进行)
    class FileSliceUpload{
         
        constructor(testingUrl, uploadUrl, margeUrl,progressUrl, fileSelect) {
                this.testingUrl = testingUrl; // 检测文件上传的url
                this.uploadUrl = uploadUrl;//文件上传接口
                this.margeUrl = margeUrl; // 合并文件接口
                this.progressUrl = progressUrl; //进度接口
                this.fileSelect = fileSelect;
                this.fileObj = null;
                this.totalize = null;
                this.blockSize = 1024 * 1024; //每次上传多少字节1mb(最佳)
                this.sta = 0; //起始位置
                this.end =  this.sta +  this.blockSize; //结束位置
                this.count = 0; //分片个数
                this.barId = "bar"; //进度条id
                this.progressId = "progress";//进度数值ID
                this.fileSliceName = ""; //分片文件名称
                this.fileName = "";
                this.uploadFileInterval = null;  //上传文件定时器
    
        }
    
        /**
         *  样式可以进行修改
         * @param {*} progressId   需要将进度条添加到那个元素下面
         */
        addProgress (progressSelect) {
            let bar = document.createElement("div")
            bar.setAttribute("id", this.barId);
            let num = document.createElement("div")
            num.setAttribute("id", this.progressId);
            num.innerText = "0%"
            bar.appendChild(num);
            document.querySelector(progressSelect).appendChild(bar)
         
        }
        //续传  在上传前先去服务器检测之前是否有上传过这个文件,如果还有返回上传的的分片,那么进行续传
        // 将当前服务器上传的最后一个分片会从新上传, 避免因为网络的原因导致分片损坏 
        sequelFile () {
            if (this.fileName) {
                var xhr = new XMLHttpRequest();
                //同步
                xhr.open('GET', this.testingUrl + "/" + this.fileName+ "/" + this.blockSize+ "/" + this.totalize, false);
                xhr.send();
                if (xhr.readyState === 4 && xhr.status === 200) {
                    let ret = JSON.parse(xhr.response)
                    if (ret.code == 20000) {
                       let data= ret.data
                        this.count = data.code;
                        this.fileSliceName = data.fileSliceName
                        //计算起始位置和结束位置
                        this.sta = this.blockSize * this.count
                        //计算结束位置
                        this.end = this.sta + this.blockSize
                    } else {
                        this.sta = 0; //从头开始
                        this.end = this.sta + this.blockSize;
                        this.count = 0; //分片个数
                    }
                }
            }
        }
    
        stopUploadFile () {
            clearInterval(this.uploadFileInterval)
        }
    
        // 文件上传(单文件)
        startUploadFile () { 
             // 进度条
             let bar = document.getElementById(this.barId)
             let progressEl = document.getElementById(this.progressId)
            this.fileObj = document.querySelector(this.fileSelect).files[0];
            this.totalize = this.fileObj.size;
            this.fileName = this.fileObj.name;
     
            //查询是否存在之前上传过此文件,然后继续
            this.sequelFile()
            let ref = this; //拿到当前对象的引用,因为是在异步中使用this就是他本身而不是class
            this.uploadFileInterval = setInterval(function () {
                    if (ref.sta > ref.totalize) {
                        //上传完毕后结束定时器
                        clearInterval(ref.uploadFileInterval)
                        //发送合并请求
                        ref.margeUploadFile ()
                        console.log("stop" + ref.sta);
                        return;
                    };
                    //分片名称
                    ref.fileSliceName = ref.fileName + "-slice-" + ref.count++
                    //分割文件 ,
                    var blob1 =  ref.fileObj.slice(ref.sta, ref.end);
                    var fd = new FormData();
                    fd.append('part', blob1);
                    fd.append('fileSliceName', ref.fileSliceName);
                    fd.append('fileSize', ref.totalize);
                    var xhr = new XMLHttpRequest();
                    xhr.open('POST',  ref.uploadUrl, true);
                    xhr.send(fd); //异步发送文件,不管是否成功, 会定期检测
    
                    xhr.onreadystatechange = function () {
                        if (xhr.readyState === 4 && xhr.status === 200) {
                            let ret = JSON.parse(xhr.response)
                            if (ret.code == 20000) {
                                //计算进度
                                let percent =  Math.ceil((ret.data*ref.blockSize/ ref.totalize) * 100)
                                if (percent > 100) {
                                    percent=100
                                    
                                }
                                bar.style.width = percent + '%';
                                bar.style.backgroundColor = 'red';
                                progressEl.innerHTML = percent + '%'
                            }
                        }
                }
    
    
                //起始位置等于上次上传的结束位置
                ref.sta =  ref.end;
                //结束位置等于上次上传的结束位置+每次上传的字节
                ref.end = ref.sta + ref.blockSize;
            
            }, 5)
    
        }
    
        margeUploadFile () {
                console.log("检测上传的文件完整性..........");
                var xhr = new XMLHttpRequest();
                //文件分片的名称/分片大小/总大小
                xhr.open('GET', this.margeUrl+ "/" + this.fileSliceName + "/" + this.blockSize + "/" + this.totalize, true);
                xhr.send(); //发送请求
                xhr.onreadystatechange = function () {
                    if (xhr.readyState === 4 && xhr.status === 200) {
                        let ret = JSON.parse(xhr.response)
                        if (ret.code == 20000) {
                            console.log("文件上传完毕");
                        } else {
                            console.log("上传完毕但是文件上传过程中出现了异常", ret);
                        }
                    }
                }
        
        }
    
    }
    export default FileSliceUpload;

    后端

    因为代码内部使用较多自己封装的工具类的原因,以下代码只提供原理的参考

    package com.controller.commontools.fIleupload;
    
    import com.alibaba.fastjson.JSON;
    import com.application.Result;
    import com.container.ArrayByteUtil;
    import com.encryption.hash.HashUtil;
    import com.file.FileUtils;
    import com.file.FileWebUpload;
    import com.file.ReadWriteFileUtils;
    import com.function.impl.ExecutorUtils;
    import com.path.ResourceFileUtil;
    import com.string.PatternCommon;
    import org.springframework.web.bind.annotation.*;
    
    import javax.servlet.http.HttpServletRequest;
    import java.io.File;
    import java.io.FileNotFoundException;
    import java.io.IOException;
    import java.io.RandomAccessFile;
    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.List;
    import java.util.concurrent.Future;
    import java.util.concurrent.atomic.AtomicBoolean;
    import java.util.stream.Collectors;
    
    @RestController
    @RequestMapping("/fileslice")
    public class FIleSliceUploadController {
    
        private  final  String identification="-slice-";
        private  final  String uploadslicedir="uploads"+File.separator+"slice"+File.separator;//分片目录
        private  final  String uploaddir="uploads"+File.separator+"real"+File.separator;//实际文件目录
        //获取分片
        @GetMapping("/testing/{fileName}/{fileSlicSize}/{fileSize}")
        public Result testing(@PathVariable String fileName,@PathVariable long fileSlicSize,@PathVariable long fileSize  ) throws Exception {
            String dir = fileNameMd5Dir(fileName,fileSize);
            String absoluteFilePathAndCreate = ResourceFileUtil.getAbsoluteFilePathAndCreate(uploadslicedir)+File.separator+dir;
            File file = new File(absoluteFilePathAndCreate);
            if (file.exists()) {
                List filesAll = FileUtils.getFilesAll(file.getAbsolutePath());
    
                if (filesAll.size()<2){
                    //分片缺少 删除全部分片文件 ,从新上传
                    FileUtils.delFilesAllReview(absoluteFilePathAndCreate,true);
                    return Result.Error();
                }
    
                //从小到大文件进行按照序号排序,和判断分片是否损坏
                List collect = fileSliceIsbadAndSort(file, fileSlicSize);
                //获取最后一个分片
                String fileSliceName = collect.get(collect.size() - 1);
                fileSliceName = new File(fileSliceName).getName();
                int code = fileId(fileSliceName);
                //服务器的分片总大小必须小于或者等于文件的总大小
                if ((code*fileSlicSize)<=fileSize) {
                    Result result = new Result();
                    String finalFileSliceName = fileSliceName;
                    String str = PatternCommon.renderString("{\"code\":\"$[code]\",\"fileSliceName\":\"${fileSliceName}\"}", new HashMap() {{
                        put("code", String.valueOf(code));
                        put("fileSliceName", finalFileSliceName);
                    }});
                    result.setData(JSON.parse(str));
                    return result;
                }else {
                    //分片异常 ,删除全部分片文件,从新上传
                    FileUtils.delFilesAllReview(absoluteFilePathAndCreate,true);
                    return Result.Error();
                }
            }
            //不存在
           return Result.Error();
        }
    
    
        @PostMapping(value = "/uploads")
        public Result uploads(HttpServletRequest request)  {
            String fileSliceName = request.getParameter("fileSliceName");
            long fileSize = Long.parseLong(request.getParameter("fileSize")); //文件大小
            String dir = fileSliceMd5Dir(fileSliceName,fileSize);
            String absoluteFilePathAndCreate = ResourceFileUtil.getAbsoluteFilePathAndCreate(uploadslicedir+dir);
            FileWebUpload.fileUpload(absoluteFilePathAndCreate,fileSliceName,request);
            int i = fileId(fileSliceName); //返回上传成功的文件id,用于前端计算进度
            Result result=new Result();
            result.setData(i);
            return result;
        }
    
    
        // 合并分片
        @GetMapping(value = "/merge-file-slice/{fileSlicNamee}/{fileSlicSize}/{fileSize}")
        public Result mergeFileSlice(@PathVariable String fileSlicNamee,@PathVariable long fileSlicSize,@PathVariable long fileSize ) throws Exception {
            int l =(int) Math.ceil((double) fileSize / fileSlicSize); //有多少个分片
            String dir = fileSliceMd5Dir(fileSlicNamee,fileSize); //分片所在的目录
           String absoluteFilePathAndCreate = ResourceFileUtil.getAbsoluteFilePathAndCreate(uploadslicedir+dir);
            File file=new File(absoluteFilePathAndCreate);
            if (file.exists()){
                List filesAll = FileUtils.getFilesAll(file.getAbsolutePath());
    
                //阻塞循环判断是否还在上传  ,解决前端进行ajax异步上传的问题
                int beforeSize=filesAll.size();
    
                while (true){
                     Thread.sleep(1000);
                     //之前分片数量和现在分片数据只差,如果大于1那么就在上传,那么继续
                     filesAll = FileUtils.getFilesAll(file.getAbsolutePath());
                    if (filesAll.size()-beforeSize>=1){
                        beforeSize=filesAll.size();
                        //继续检测
                        continue;
                    }
                    //如果是之前分片和现在的分片相等的,那么在阻塞2秒后检测是否发生变化,如果还没变化那么上传全部完成,可以进行合并了
                    //当然这不是绝对的,只能解决网络短暂的波动,因为有可能发生断网很长时间,网络恢复后文件恢复上传, 这个问题是避免不了的,所以我们在下面的代码进行数量的效验
                    // 因为我们不可能一直等着他网好,所以如果1~3秒内没有上传新的内容,那么我们默认判定上传完毕
                    if (beforeSize==filesAll.size()){
                        Thread.sleep(2000);
                        filesAll = FileUtils.getFilesAll(file.getAbsolutePath());
                        if (beforeSize==filesAll.size()){
                            break;
                        }
                    }
                }
                //分片数量效验
                if (filesAll.size()!=l){
                    //分片缺少 ,删除全部分片文件,从新上传
                    FileUtils.delFilesAllReview(absoluteFilePathAndCreate,true);
                    return Result.Error();
                }
                //获取实际的文件名称,组装路径
                String realFileName = realFileName(fileSlicNamee);
                String realFileNamePath = ResourceFileUtil.getAbsoluteFilePathAndCreate(uploaddir+ realFileName);
                //从小到大文件进行按照序号排序 ,和检查分片文件是否有问题
                List collect = fileSliceIsbadAndSort(file, fileSlicSize);
                int fileSliceSize = collect.size();
    
                List> futures = new ArrayList<>();
                // 将文件按照序号进行合并 ,算出Runtime.getRuntime().availableProcessors()个线程 ,每个线程需要读取多少分片, 和每个线程需要读取多少字节大小
                //有人会说一个分片一个线程不行吗,你想想如果上千或者上万分片的话,你创建这么多的线程需要多少时间,以及线程切换上下文切换和销毁需要多少时间? 
                // 就算使用线程池,也顶不住啊,你内存又有多大,能存下多少队列?,并发高的话直接怼爆
                int availableProcessors = Runtime.getRuntime().availableProcessors();
                //每个线程读取多少文件
                int readFileSize = (int)Math.ceil((double)fileSliceSize / availableProcessors);
                //每个线程需要读取的文件大小
                long readSliceSize = readFileSize * fileSlicSize;
                for (int i = 0; i < availableProcessors; i++) {
                    int finalI = i;
                    Future future =   ExecutorUtils.createFuture("FIleSliceUploadController",()->{
                        //每个线程需要读取多少字节
                        byte[] bytes=new byte[(int) readSliceSize];
                        int index=0;
                        for (int i1 = finalI *readFileSize,i2 = readFileSize*(finalI+1)>fileSliceSize?fileSliceSize:readFileSize*(finalI+1); i1 < i2; i1++) {
                            try ( RandomAccessFile r = new RandomAccessFile(collect.get(i1), "r");){
                                r.read(bytes, (int)(index*fileSlicSize),(int)fileSlicSize);
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                            index++;
                        }
    
                        if(finalI==availableProcessors-1){
                            //需要调整数组
                            bytes = ArrayByteUtil.getActualBytes(bytes);
                        }
    
                        try ( RandomAccessFile w = new RandomAccessFile(realFileNamePath, "rw");){
                            //当前文件写入的位置
                            w.seek(finalI*readSliceSize);
                            w.write(bytes);
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    });
                    futures.add(future);
                }
                //阻塞到全部线程执行完毕后
                ExecutorUtils.waitComplete(futures);
                //删除全部分片文件
                FileUtils.delFilesAllReview(absoluteFilePathAndCreate,true);
            }else {
                //没有这个分片相关的的目录
                return Result.Error();
            }
    
            return Result.Ok();
    
    
    
        }
    
    
    
    
    
    
        //获取分片文件的目录
        private String fileSliceMd5Dir(String fileSliceName,long fileSize){
            int i = fileSliceName.indexOf(identification) ;
            String substring = fileSliceName.substring(0, i);
            String dir = HashUtil.md5(substring+fileSize);
            return dir;
        }
        //通过文件名称获取文件目录
        private String fileNameMd5Dir(String fileName,long fileSize){
            return HashUtil.md5(fileName+fileSize);
        }
        //获取分片的实际文件名
        private String realFileName(String fileSliceName){
            int i = fileSliceName.indexOf(identification) ;
            String substring = fileSliceName.substring(0, i);
            return substring;
    
        }
        //获取文件序号
        private  int fileId(String fileSliceName){
            int i = fileSliceName.indexOf(identification)+identification.length() ;
            String fileId = fileSliceName.substring(i);
            return Integer.parseInt(fileId);
        }
    
    
    
      //判断是否损坏
      private List  fileSliceIsbadAndSort(File file,long fileSlicSize) throws Exception {
            String absolutePath = file.getAbsolutePath();
            List filesAll = FileUtils.getFilesAll(absolutePath);
            if (filesAll.size()<1){
                //分片缺少,删除全部分片文件 ,从新上传
                FileUtils.delFilesAllReview(absolutePath,true);
                throw  new Exception("分片损坏");
            }
            //从小到大文件进行按照序号排序
            List collect = filesAll.stream().sorted((a, b) -> fileId(a) - fileId(b)).collect(Collectors.toList());
            //判断文件是否损坏,将文件排序后,进行前后序号相差大于1那么就代表少分片了
            for (int i = 0; i < collect.size()-1; i++) {
                //检测分片的连续度
                if (fileId(collect.get(i)) - fileId(collect.get(i+1))!=-1) {
                    //分片损坏 删除全部分片文件 ,从新上传
                    FileUtils.delFilesAllReview(absolutePath,true);
                    throw  new Exception("分片损坏");
                }
                //检测分片的完整度
                if (new File(collect.get(i)).length()!=fileSlicSize) {
                    //分片损坏 删除全部分片文件 ,从新上传
                    FileUtils.delFilesAllReview(absolutePath,true);
                    throw  new Exception("分片损坏");
                }
            }
            return  collect;
        }
    }
    本文内容来源于网友投稿,如有侵权请联系删除。
    作者最新文章
    编程开发 Java
    上一篇: CentOS安装Apache、PHP、MySQL详解
    下一篇: Java如何使用Stream优化if中判断条件过多情况
    相关文章 更多
    链表删除节点的时间复杂度是多少及其详细分析
    链表删除节点的时间复杂度是多少及其详细分析

    详细分析链表删除节点的时间复杂度,深入探讨单链表与双向链表在不同已知前提下的查找与删除开销,并结合完整代码与清晰图解进行对比总结。

    codex如何配置模型参数及文件设置教程
    codex如何配置模型参数及文件设置教程

    想知道如何让AI写出的代码更贴合你的习惯?本文手把手教你在VS Code中调整Codex相关模型参数,通过修改配置文件优化温度值和令牌限制,解决代码建议不准确或响应慢的问题。

    Claude Code AI编程工具实力揭秘与编程助手实测
    Claude Code AI编程工具实力揭秘与编程助手实测

    通过实测展示Claude Code在终端中如何理解自然语言指令、自动修改代码文件并处理复杂编程任务,帮助开发者评估其实际辅助能力。

    winforms教程自学入门与基础开发步骤详解
    winforms教程自学入门与基础开发步骤详解

    本教程详细讲解如何使用Visual Studio创建WinForms项目,通过添加按钮和标签控件并编写点击事件代码,实现一个基础的计数器功能,适合C#初学者快速上手Windows窗体应用开发。

    Cursor自动补全设置教程教你快速开启代码补全功能
    Cursor自动补全设置教程教你快速开启代码补全功能

    详解Cursor编辑器中自动补全功能的开启与优化设置,涵盖Tab触发机制、上下文窗口调整及模型切换,帮助开发者解决补全延迟、干扰大等问题,提升编码流畅度。

    pandas的数据格式怎么转换和设置方法教程
    pandas的数据格式怎么转换和设置方法教程

    详解Pandas中数据格式转换的核心方法,包括astype强制转换、to_numeric容错处理及日期解析技巧,解决常见类型错误并提升数据处理效率。

    VS Code中文设置方法 简体语言包安装与切换教程
    VS Code中文设置方法 简体语言包安装与切换教程

    详细介绍在Visual Studio Code中安装Chinese (Simplified)语言包的方法,包括通过扩展市场搜索、安装及自动重启切换至简体中文界面的完整步骤,帮助开发者快速将编辑器本地化。

    cursor安装过程无法更改安装位置的解决方法
    cursor安装过程无法更改安装位置的解决方法

    针对Cursor安装包默认锁定C盘且无路径选择界面的问题,提供通过手动移动文件并创建目录联结(Symbolic Link)的解决方案,实现将软件安装在其他磁盘分区。

    rust下载安装教程详解及Windows环境配置方法
    rust下载安装教程详解及Windows环境配置方法

    详解Windows系统下Rust语言的安装步骤,重点解析rustup工具链管理机制,解决环境变量配置错误及MSVC链接器缺失问题,提供可复制的命令验证方法与常见报错的因果排查思路。

    vs code怎么配置 chat实用设置教程步骤
    vs code怎么配置 chat实用设置教程步骤

    详解VS Code中Chat插件的安装与核心配置步骤,重点解决API连接失败、响应慢等常见问题,通过优化上下文设置提升代码生成质量,适合希望集成AI辅助工具的开发者阅读。

    查看更多
    精品专题 更多
    装机必备
    装机必备

    正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

    Windows
    Windows

    正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

    macOS软件
    macOS软件

    正软商城macOS软件专区,精选适用于Mac电脑的办公、设计、影音、效率、开发和系统工具,提供软件功能介绍、macOS兼容版本、正版授权及购买下载服务。

    Mac软件 更多
    photoshop
    photoshop
    Windows、macOS 、 iPad

    Photoshop 2026 是 Adobe 推出的专业图像处理与视觉设计软件,支持 Windows、macOS 和 iPad 等平台,广泛应用于摄影修图、电商设计、平面海报、数字绘画及视觉合成等创作场景。

    Blender
    Blender
    Windows、macOS 和 Linux

    Blender 是一款免费开源、跨平台的专业 3D 创作软件,集建模、动画、渲染、视频编辑与视觉合成等功能于一体,广泛应用于影视动画、游戏设计和建筑可视化等领域。软件支持 Cycles 物理渲染器与 Eevee 实时渲染引擎,并提供多边形建模、骨骼绑定、物理模拟等专业工具。Blender 兼容 Windows、macOS 和 Linux 系统,安装包轻巧、运行流畅,依托活跃的全球开发者社区持续更新,是从初学者到专业创作者都值得选择的正版 3D 创作工具。

    灵活计算器
    灵活计算器
    macOS/iOS/Android

    灵活计算器是一款笔记式算数应用,支持实时计算、动态关联和云端同步功能。记录、整理和输出之间的过渡会更自然,适合长期写作、做笔记或持续沉淀个人内容。

    WINDOWS 更多
    3dmax(3ds max)
    3dmax(3ds max)
    Windows

    Autodesk 3ds Max 是一款专业的三维建模、动画与渲染软件,广泛应用于建筑可视化、游戏开发、影视动画、广告设计和产品展示等领域。

    photoshop
    photoshop
    Windows、macOS 、 iPad

    Photoshop 2026 是 Adobe 推出的专业图像处理与视觉设计软件,支持 Windows、macOS 和 iPad 等平台,广泛应用于摄影修图、电商设计、平面海报、数字绘画及视觉合成等创作场景。

    Blender
    Blender
    Windows、macOS 和 Linux

    Blender 是一款免费开源、跨平台的专业 3D 创作软件,集建模、动画、渲染、视频编辑与视觉合成等功能于一体,广泛应用于影视动画、游戏设计和建筑可视化等领域。软件支持 Cycles 物理渲染器与 Eevee 实时渲染引擎,并提供多边形建模、骨骼绑定、物理模拟等专业工具。Blender 兼容 Windows、macOS 和 Linux 系统,安装包轻巧、运行流畅,依托活跃的全球开发者社区持续更新,是从初学者到专业创作者都值得选择的正版 3D 创作工具。