【项目实战】大文件断点续传,搞起

news2024/12/27 13:07:29

今天给大家分享的又是一篇实战文章,也是最近私活里遇到的,万能的互联网给了我办法,分享一下。

背景

最近接到一个新的需求,需要上传2G左右的视频文件,用测试环境的OSS试了一下,上传需要十几分钟,再考虑到公司的资源问题,果断放弃该方案。

一提到大文件上传,我最先想到的就是各种网盘了,现在大家都喜欢将自己收藏的**「小电影」**上传到网盘进行保存。网盘一般都支持断点续传和文件秒传功能,减少了网络波动和网络带宽对文件的限制,大大提高了用户体验,让人爱不释手。

说到这,大家先来了解一下这几个概念:

  • 「文件分块」:将大文件拆分成小文件,将小文件上传\下载,最后再将小文件组装成大文件;
  • 「断点续传」:在文件分块的基础上,将每个小文件采用单独的线程进行上传\下载,如果碰到网络故障,可以从已经上传\下载的部分开始继续上传\下载未完成的部分,而没有必要从头开始上传\下载;
  • 「文件秒传」:资源服务器中已经存在该文件,其他人上传时直接返回该文件的URI。

RandomAccessFile

平时我们都会使用FileInputStreamFileOutputStreamFileReader以及FileWriterIO流来读取文件,今天我们来了解一下RandomAccessFile

它是一个直接继承Object的独立的类,底层实现中它实现的是DataInputDataOutput接口。该类支持随机读取文件,随机访问文件类似于文件系统中存储的大字节数组。

它的实现基于**「文件指针」**(一种游标或者指向隐含数组的索引),文件指针可以通过getFilePointer方法读取,也可以通过seek方法设置。

输入时从文件指针开始读取字节,并使文件指针超过读取的字节,如果写入超过隐含数组当前结尾的输出操作会导致扩展数组。该类有四种模式可供选择:

  • r: 以只读方式打开文件,如果执行写入操作会抛出IOException;
  • rw: 以读、写方式打开文件,如果文件不存在,则尝试创建文件;
  • rws: 以读、写方式打开文件,要求对文件内容或元数据的每次更新都同步写入底层存储设备;
  • rwd: 以读、写方式打开文件,要求对文件内容的每次更新都同步写入底层存储设备;

rw模式下,默认是使用buffer的,只有cache满的或者使用RandomAccessFile.close()关闭流的时候才真正的写到文件。

API

1、void seek(long pos):设置下一次读取或写入时的文件指针偏移量,通俗点说就是指定下次读文件数据的位置。
偏移量可以设置在文件末尾之外,只有在偏移量设置超出文件末尾后,才能通过写入更改文件长度;

2、native long getFilePointer():返回当前文件的光标位置;

3、native long length():返回当前文件的长度;

4、「写」方法

图片

5、readFully(byte[] b):这个方法的作用就是将文本中的内容填满这个缓冲区b。如果缓冲b不能被填满,那么读取流的过程将被阻塞,如果发现是流的结尾,那么会抛出异常;

6、FileChannel getChannel():返回与此文件关联的唯一FileChannel对象;

7、int skipBytes(int n):试图跳过n个字节的输入,丢弃跳过的字节;

8、RandomAccessFile的绝大多数功能,已经被JDK1.4的NIO的**「内存映射」**文件取代了,即把文件映射到内存后再操作,省去了频繁磁盘io

实操

文件分块

文件分块需要在前端进行处理,可以利用强大的js库或者现成的组件进行分块处理。需要确定分块的大小和分块的数量,然后为每一个分块指定一个索引值。

为了防止上传文件的分块与其它文件混淆,采用文件的md5值来进行区分,该值也可以用来校验服务器上是否存在该文件以及文件的上传状态。

  • 如果文件存在,直接返回文件地址;
  • 如果文件不存在,但是有上传状态,即部分分块上传成功,则返回未上传的分块索引数组;
  • 如果文件不存在,且上传状态为空,则所有分块均需要上传。
fileRederInstance.readAsBinaryString(file);
fileRederInstance.addEventListener("load", (e) => {
    let fileBolb = e.target.result;
    fileMD5 = md5(fileBolb);
    const formData = new FormData();
    formData.append("md5", fileMD5);
    axios
        .post(http + "/fileUpload/checkFileMd5", formData)
        .then((res) => {
            if (res.data.message == "文件已存在") {
                //文件已存在不走后面分片了,直接返回文件地址到前台页面
                success && success(res);
            } else {
                //文件不存在存在两种情况,一种是返回data:null代表未上传过 一种是data:[xx,xx] 还有哪几片未上传
                if (!res.data.data) {
                    //还有几片未上传情况,断点续传
                    chunkArr = res.data.data;
                }
                readChunkMD5();
            }
        })
        .catch((e) => {});
});

在调用上传接口前,通过slice方法来取出索引在文件中对应位置的分块。

const getChunkInfo = (file, currentChunk, chunkSize) => {
       //获取对应下标下的文件片段
       let start = currentChunk * chunkSize;
       let end = Math.min(file.size, start + chunkSize);
       //对文件分块
       let chunk = file.slice(start, end);
       return { start, end, chunk };
   };

之后调用上传接口完成上传。

断点续传、文件秒传

后端基于spring boot开发,使用redis来存储上传文件的状态和上传文件的地址。

如果文件完整上传,返回文件路径;部分上传则返回未上传的分块数组;如果未上传过返回提示信息。

在上传分块时会产生两个文件,一个是文件主体,一个是临时文件。临时文件可以看做是一个数组文件,为每一个分块分配一个值为127的字节。

校验MD5值时会用到两个值:

  • 文件上传状态:只要该文件上传过就不为空,如果完整上传则为true,部分上传返回false
  • 文件上传地址:如果文件完整上传,返回文件路径;部分上传返回临时文件路径。
/**
 * 校验文件的MD5
 **/
public Result checkFileMd5(String md5) throws IOException {
    //文件是否上传状态:只要该文件上传过该值一定存在
    Object processingObj = stringRedisTemplate.opsForHash().get(UploadConstants.FILE_UPLOAD_STATUS, md5);
    if (processingObj == null) {
        return Result.ok("该文件没有上传过");
    }
    boolean processing = Boolean.parseBoolean(processingObj.toString());
    //完整文件上传完成时为文件的路径,如果未完成返回临时文件路径(临时文件相当于数组,为每个分块分配一个值为127的字节)
    String value = stringRedisTemplate.opsForValue().get(UploadConstants.FILE_MD5_KEY + md5);
    //完整文件上传完成是true,未完成返回false
    if (processing) {
        return Result.ok(value,"文件已存在");
    } else {
        File confFile = new File(value);
        byte[] completeList = FileUtils.readFileToByteArray(confFile);
        List<Integer> missChunkList = new LinkedList<>();
        for (int i = 0; i < completeList.length; i++) {
            if (completeList[i] != Byte.MAX_VALUE) {
                //用空格补齐
                missChunkList.add(i);
            }
        }
        return Result.ok(missChunkList,"该文件上传了一部分");
    }
}

说到这,你肯定会问:当这个文件的所有分块上传完成之后,该怎么得到完整的文件呢?接下来我们就说一下分块合并的问题。

分块上传、文件合并

上边我们提到了利用文件的md5值来维护分块和文件的关系,因此我们会将具有相同md5值的分块进行合并,由于每个分块都有自己的索引值,所以我们会将分块按索引像插入数组一样分别插入文件中,形成完整的文件。

分块上传时,要和前端的分块大小、分块数量、当前分块索引等对应好,以备文件合并时使用,此处我们采用的是**「磁盘映射」**的方式来合并文件。

 //读操作和写操作都是允许的
RandomAccessFile tempRaf = new RandomAccessFile(tmpFile, "rw");
//它返回的就是nio通信中的file的唯一channel
FileChannel fileChannel = tempRaf.getChannel();

//写入该分片数据   分片大小 * 第几块分片获取偏移量
long offset = CHUNK_SIZE * multipartFileDTO.getChunk();
//分片文件大小
byte[] fileData = multipartFileDTO.getFile().getBytes();
//将文件的区域直接映射到内存
MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, offset, fileData.length);
mappedByteBuffer.put(fileData);
// 释放
FileMD5Util.freedMappedByteBuffer(mappedByteBuffer);
fileChannel.close();

每当完成一次分块的上传,还需要去检查文件的上传进度,看文件是否上传完成。

RandomAccessFile accessConfFile = new RandomAccessFile(confFile, "rw");
//把该分段标记为 true 表示完成
accessConfFile.setLength(multipartFileDTO.getChunks());
accessConfFile.seek(multipartFileDTO.getChunk());
accessConfFile.write(Byte.MAX_VALUE);

//completeList 检查是否全部完成,如果数组里是否全部都是(全部分片都成功上传)
byte[] completeList = FileUtils.readFileToByteArray(confFile);
byte isComplete = Byte.MAX_VALUE;
for (int i = 0; i < completeList.length && isComplete == Byte.MAX_VALUE; i++) {
    //与运算, 如果有部分没有完成则 isComplete 不是 Byte.MAX_VALUE
    isComplete = (byte) (isComplete & completeList[i]);
}
accessConfFile.close();

然后更新文件的上传进度到Redis中。

//更新redis中的状态:如果是true的话证明是已经该大文件全部上传完成
if (isComplete == Byte.MAX_VALUE) {
    stringRedisTemplate.opsForHash().put(UploadConstants.FILE_UPLOAD_STATUS, multipartFileDTO.getMd5(), "true");
    stringRedisTemplate.opsForValue().set(UploadConstants.FILE_MD5_KEY + multipartFileDTO.getMd5(), uploadDirPath + "/" + fileName);
} else {
    if (!stringRedisTemplate.opsForHash().hasKey(UploadConstants.FILE_UPLOAD_STATUS, multipartFileDTO.getMd5())) {
        stringRedisTemplate.opsForHash().put(UploadConstants.FILE_UPLOAD_STATUS, multipartFileDTO.getMd5(), "false");
    }
    if (!stringRedisTemplate.hasKey(UploadConstants.FILE_MD5_KEY + multipartFileDTO.getMd5())) {
        stringRedisTemplate.opsForValue().set(UploadConstants.FILE_MD5_KEY + multipartFileDTO.getMd5(), uploadDirPath + "/" + fileName + ".conf");
    }
}

总结

今天大体内容如上,大家实践起来,关注本专栏,还有更多实践,我们一起学习。

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

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

相关文章

Session

什么是Session&#xff1f; 服务器会给每个用户&#xff08;浏览器&#xff09;创建一个Session对象。一个Session独占一个浏览器&#xff0c;只要浏览器没有关闭&#xff0c;Session就一直存在。用户登陆后&#xff0c;整个网站都可以访问&#xff01;&#xff08;保存用户的…

掌握imgproc组件:opencv-直方图与匹配

直方图与匹配 1. 图像直方图概述2.直方图的计算与绘制2.1 计算直方图&#xff1a;calcHist()函数2.2 找寻最值&#xff1a;minMAxLoc()函数2.3 示例程序&#xff1a;绘制H-S直方图 3.直方图对比3.1 对比直方图&#xff1a;compareHist()函数3.2 示例程序&#xff1a;直方图对比…

Dubbo接口级服务发现-数据结构

目录 Dubbo服务治理易用性的原理&#xff1a; URL地址数据划分&#xff1a; Dubbo接口级服务发现---易用性的代价 Proposal&#xff0c;适应云原生、更大规模集群的服务发现类型。 Dubbo3应用级服务发现---基本原理 Dubbo负载均衡机制 常规负载均衡算法 负载均衡策略&am…

leetcode526. 优美的排列(回溯算法-java)

优美的排列 leetcode526. 优美的排列题目描述接替思路代码演示: 动态规划专题 leetcode526. 优美的排列 来源&#xff1a;力扣&#xff08;LeetCode&#xff09; 链接&#xff1a;https://leetcode.cn/problems/beautiful-arrangement 题目描述 假设有从 1 到 n 的 n 个整数。用…

【Python 随练】判断一个5位数是否为回文数

题目&#xff1a; 一个 5 位数&#xff0c;判断它是不是回文数。即 12321 是回文数 &#xff0c;个位与万位相同&#xff0c;十位与千位相同。 简介&#xff1a; 在本篇博客中&#xff0c;我们将解决一个编程问题&#xff1a;判断一个5位数是否为回文数。回文数是指从左到右…

数字IC前端学习笔记:近期最少使用(LRU)算法

相关文章 数字IC前端学习笔记&#xff1a;LSFR&#xff08;线性反馈移位寄存器&#xff09; 数字IC前端学习笔记&#xff1a;跨时钟域信号同步 数字IC前端学习笔记&#xff1a;信号同步和边沿检测 数字IC前端学习笔记&#xff1a;锁存器Latch的综合 数字IC前端学习笔记&am…

Java新特性-Functon接口

Java新特性-Functon接口 ⭐⭐⭐⭐⭐⭐ Github主页&#x1f449;https://github.com/A-BigTree 笔记链接&#x1f449;https://github.com/A-BigTree/Code_Learning ⭐⭐⭐⭐⭐⭐ Spring专栏&#x1f449;https://blog.csdn.net/weixin_53580595/category_12279588.html Sprin…

使用CRM系统如何让你的企业受益

随着市场的竞争日益加剧&#xff0c;企业需要更好地管理客户关系和提高销售。为此&#xff0c;很多企业已经开始使用CRM系统。CRM系统是一种管理客户关系的工具&#xff0c;可以帮助企业更好地了解客户需求&#xff0c;提高销售效率&#xff0c;提高客户满意度和忠诚度等。在这…

【ARM AMBA APB 入门 2 -- Debug APB总线介绍】

文章目录 1.1 DEBUG APB 介绍1.1.1 Debug APB interface 1.2 Debug APB 与 APB 的区别 1.1 DEBUG APB 介绍 ARM Debug APB是一种用于调试ARM处理器的总线协议。它是一种简化的、低功耗的调试接口&#xff0c;用于与处理器内部的调试逻辑进行通信。 ARM Debug APB具有以下特点…

使用Py2neo构建知识图谱(概念与实现)

知识图谱是一种用于描述实体之间关系的图形化知识表示方法&#xff0c;它将实体、属性和关系组织成一个大型的、半结构化的知识库。知识图谱的应用非常广泛&#xff0c;包括语义搜索、智能问答、个性化推荐、内容分发等领域。 知识图谱的构建方法有很多种&#xff0c;其中比较…

基于SpringBoot和Vue的医疗设备管理系统的设计与实现(源码+文档+报告)

伴随着网络技术的发展&#xff0c;网络巨头们也在各自的行业中展开了激烈的竞争。网络技术已经渗透到了社会的各个角落&#xff0c;移动支付、远程医疗和健康管理等多种网络技术的普及也日益广泛。同时&#xff0c;网络产业的崛起也极大地影响了传统产业。对医疗设备管理系统的…

Axure设计之动态柱状图教程(中继器)

中继器作为复杂的元件&#xff0c;通常被用来制作“高保真”的动态原型&#xff0c;以达到良好的视觉效果和交互效果。本文将教大家通过AxureRP9工具如何使用中继器设计动态柱状图表。 一、案例效果 下载地址&#xff1a; https://download.csdn.net/download/u010709330/8797…

多元分类预测 | Matlab鹈鹕算法(POA)优化混合核极限学习机(HKELM)分类预测,多特征输入模型,POA-HKELM分类预测

文章目录 效果一览文章概述部分源码参考资料效果一览 文章概述 多元分类预测 | Matlab鹈鹕算法(POA)优化混合核极限学习机(HKELM)分类预测,多特征输入模型,POA-HKELM分类预测 多特征输入单输出的二分类及多分类模型。程序内注释详细,直接替换数据就可以用。程序语言为matlab…

QT5.12安卓环境的搭建(转载保存留档查看)

1. QT环境搭建 Qt 5.12.6安装的时候 添加 “Android ARM64-v8a”&#xff0c;也可以添加 “Android ARMv7” 。 &#xff08;其它为搭建qgc时需要添加的&#xff09; 2. Android 环境搭建 QT官方的指导&#xff1a; Qt 5.12 Getting Started with Qt for Android Qt 5.15 …

C语言-函数栈帧(每次调用函数,开辟函数的大概流程)

思路&#xff1a; emm&#xff0c;有点懵&#xff0c;这一块&#xff0c;只知道个大概&#xff0c;如果后期我深挖的话&#xff0c;我再回头来补充上图括号的位置&#xff0c;便是每次创建&#xff0c;调用函数的时候&#xff0c;所产生的函数栈帧&#xff0c;也可以理解为每次…

(五)Qt 动态手势识别“左右滑动”以及实现翻页效果

系列文章目录 通过Qt实现手势识别控制软件操作相关系列技术方案 &#xff08;一&#xff09;Qt 将某控件、图案绘制在最前面的方法&#xff0c;通过QGraphicsScene模块实现 &#xff08;二&#xff09;Qt QGraphicsScene模块实现圆点绘制在所有窗体的最前方&#xff0c;实现圆…

Spring Boot 中的 SockJS

Spring Boot 中的 SockJS 在 Spring Boot 中&#xff0c;SockJS 是一个用于实现 WebSocket 的兼容性解决方案。本文将介绍 SockJS 的原理、使用方法和示例代码。 什么是 SockJS SockJS 是一种浏览器与服务器之间的通信协议&#xff0c;它可以在浏览器和服务器之间建立一个基于…

python--tornado服务

一、前言 Tornado是一个基于非阻塞I/O的Web框架&#xff0c;具有可以处理大量并发连接&#xff0c;适用于高并发的场景的高性能&#xff0c;轻量级和异步支持的优点。 本篇主要展示了使用 tornado http post 和 get 服务 二、代码 tornado http post 和 get 服务 # -*- codin…

视频拼接融合产品的产品与架构设计(一)

1视频拼接产品概述 将多达8个视频拼接在一起&#xff0c;拼成上帝视角的大图&#xff0c;并且可以共享最终生成的画面。 1.1 三维方案 三维方案是我在晚上思考的时候无意想到&#xff0c;这种方案应该是最接近事实的方案&#xff0c;多个画面应该是在三个维度&#xff0c;x&…