首页 > 编程语言 >通过Java实现文件断点续传功能

通过Java实现文件断点续传功能

时间:2023-10-12 13:55:52浏览次数:54  
标签:断点续传 Java String 文件 分片 fileSliceName import 上传

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

什么是断点续传

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

  • 解决上传大文件服务器内存不够的问题
  • 解决如果因为其他因素导致上传终止的问题,并且刷新浏览器后仍然能够续传,重启浏览器(关闭浏览器后再打开)仍然能够继续上传,重启电脑后仍然能够上传
  • 检测上传过程中因网络波动导致文件出现了内容丢失那么需要自动检测并且从新上传

解决方案

前端

  • 需要进行分割上传的文件
  • 需要对上传的分片文件进行指定文件序号
  • 需要监控上传进度,控制进度条
  • 上传完毕后需要发送合并请求

Blob 对象,操作文件

后端

  • 上传分片的接口
  • 合并分片的接口
  • 获取分片的接口
  • 其他工具方法,用于辅助

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

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

效果演示

先找到需要上传的文件

 

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

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

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

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

 

以上就是断点续传的核心原理,但是还需处理一些异常情况:

  • 文件上传过程中网络波动导致流丢失一部分(比对大小)
  • 文件上传过程中,服务器丢失分片 (比对分片的连续度)
  • 文件被篡改内容(比对大小)

效验核心代

 

参考代码

前端

<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>

<body>

<h1>html5大文件断点切割上传</h1>
<div id="progressBar"></div>

<input id="file" name="mov" type="file" />
<input id="btn" type="button" value="点我上传" />
<input id="btn1" type="button" value="点我停止上传" />

<script type="module">
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()
})

</script>


</body>

</html>

 

//大文件分片上传,比如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<String> filesAll = FileUtils.getFilesAll(file.getAbsolutePath());

if (filesAll.size()<2){
//分片缺少 删除全部分片文件 ,从新上传
FileUtils.delFilesAllReview(absoluteFilePathAndCreate,true);
return Result.Error();
}

//从小到大文件进行按照序号排序,和判断分片是否损坏
List<String> 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<String, String>() {{
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<String> 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<String> collect = fileSliceIsbadAndSort(file, fileSlicSize);
int fileSliceSize = collect.size();

List<Future<?>> 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<String> fileSliceIsbadAndSort(File file,long fileSlicSize) throws Exception {
String absolutePath = file.getAbsolutePath();
List<String> filesAll = FileUtils.getFilesAll(absolutePath);
if (filesAll.size()<1){
//分片缺少,删除全部分片文件 ,从新上传
FileUtils.delFilesAllReview(absolutePath,true);
throw new Exception("分片损坏");
}
//从小到大文件进行按照序号排序
List<String> 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实现文件断点续传功能的文章就介绍到这了

 

参考文章:http://blog.ncmem.com/wordpress/2023/10/12/%e9%80%9a%e8%bf%87java%e5%ae%9e%e7%8e%b0%e6%96%87%e4%bb%b6%e6%96%ad%e7%82%b9%e7%bb%ad%e4%bc%a0%e5%8a%9f%e8%83%bd/

欢迎入群一起讨论

 

标签:断点续传,Java,String,文件,分片,fileSliceName,import,上传
From: https://www.cnblogs.com/songsu/p/17759305.html

相关文章

  • python_批量处理excel文件
    情况pipinstallopenpyxlpipinstallxlrd数据Excel文件的格式为xls和xlsx,pandas读取excel文件需要安装依赖库xlrd和openpyxl。!注意:当xlrd>=2.0时,只支持xls格式,不再支持xlsx。数据代码importpandasaspdimportosimportjsonif__name__=="__main__":e......
  • 用 Java 徒手写一个抽奖系统,拿去用吧
    1、概述项目开发中经常会有抽奖这样的营销活动的需求,例如:积分大转盘、刮刮乐、老虎机等等多种形式,其实后台的实现方法是一样的,本文介绍一种常用的抽奖实现方法。整个抽奖过程包括以下几个方面:奖品奖品池抽奖算法奖品限制奖品发放2、奖品奖品包括奖品、奖品概率和限制、奖品记录。奖......
  • Java常用类,这一次帮你总结好
    常用类概述:内部类Object类包装类数学类时间类字符串StringBuilder和StringBufferDecimalFormat-   一、内部类  -概念:在一个类内部再定义一个完整的类。一般情况下类与类之间是相互独立的,内部类的意思就是打破这种独立思想,让一个类成为另一个类的内部信息,和成员变量、成......
  • JAVA
    1.JVM相关对于刚刚接触Java的人来说,JVM相关的知识不一定需要理解很深,对此里面的概念有一些简单的了解即可。不过对于一个有着3年以上Java经验的资深开发者来说,不会JVM几乎是不可接受的。JVM作为java运行的基础,很难相信对于JVM一点都不了解的人可以把java语言吃得很透。我在面......
  • Linux 中awk命令根据索引文件批量提取列和行
     001、批量提取列,根据索引index.txt文件批量提取2、4、8、9列[root@pc1test2]#lsa.txtindex.txt[root@pc1test2]#cata.txt##测试文件00100200300400500600700800901001101201301401501601701801902002102202302402502602......
  • 几个 Java 性能调优技巧,YYDS
    大多数开发者认为性能优化是一个复杂的话题,它需要大量的工作经验和相关知识理论。好吧,这也不完全错。优化一个应用做到性能最优化可能不是件容易的任务,但是这并不意味着你没有相关的知识就什么也做不了。这里有一些易于遵循的建议和最佳实践可以帮助你创建一个性能良好的应用程序。......
  • Java 18 要来了,你不会还在用Java 8吧
    Java开发工具包(JDK)18将于2022年3月22日发布。新版本的标准Java将有九个新特性,该特性集已于12月9日冻结,进入Rampdown第一阶段。值得注意的是:JDK17是一个长期支持(LTS)版本,将获得Oracle至少八年的支持,但JDK18将是一个短期功能版本,只支持六个月。可以在......
  • Java通过itext解析PDF中的关键字得到坐标进行插入印章图片或签名
    需求因需提高公司运转效率,提倡去无纸化操作,减少人力等前提;通过系统将审核通过后的pdf文档进行盖电子印章或电子签名等功能;测试效果如下:图1图2实现思路因如上图1中,存在动态表格,所以文档的布局是随数据而变的,可能是多页,可能是一页,且内容上下浮动,所以得通过解析文档内容,通过......
  • 外发文件怎么保存
    文件外发是企业日常业务中常见的场景,外发整个流程涉及外发的渠道、外发文件的大小、外发的效率、外发的合规性、文件的保存和管理等一系列的过程。外发文件的保存可以从两个角度着手:一、接收方接收方首先要对接收到的文件进行分级和分类,确认文件的类别和保密等级,根据涉密程度,按......
  • 报错解决:java.security.InvalidKeyException: Illegal key size(微信支付v3遇到的问
    前言在使用微信支付v3生成jar包后本地测试没有问题在开发小程序支付功能的时候:本地开发好好的,放在linux服务器上运行时碰到报错原因是因为微信支付256位秘钥加密解密策略 可能会导致某些jdk的版本加密解密出现问题解决首先观察你这个目录下的文件根据文件内容做判断看下......