通過(guò)Java實(shí)現(xiàn)文件斷點(diǎn)續(xù)傳功能
什么是斷點(diǎn)續(xù)傳
用戶(hù)上傳大文件,網(wǎng)絡(luò)差點(diǎn)的需要?dú)v時(shí)數(shù)小時(shí),萬(wàn)一線路中斷,不具備斷點(diǎn)續(xù)傳的服務(wù)器就只能從頭重傳,而斷點(diǎn)續(xù)傳就是,允許用戶(hù)從上傳斷線的地方繼續(xù)傳送,這樣大大減少了用戶(hù)的煩惱。
- 解決上傳大文件服務(wù)器內(nèi)存不夠的問(wèn)題
- 解決如果因?yàn)槠渌蛩貙?dǎo)致上傳終止的問(wèn)題,并且刷新瀏覽器后仍然能夠續(xù)傳,重啟瀏覽器(關(guān)閉瀏覽器后再打開(kāi))仍然能夠繼續(xù)上傳,重啟電腦后仍然能夠上傳
- 檢測(cè)上傳過(guò)程中因網(wǎng)絡(luò)波動(dòng)導(dǎo)致文件出現(xiàn)了內(nèi)容丟失那么需要自動(dòng)檢測(cè)并且從新上傳
解決方案
前端
- 需要進(jìn)行分割上傳的文件
- 需要對(duì)上傳的分片文件進(jìn)行指定文件序號(hào)
- 需要監(jiān)控上傳進(jìn)度,控制進(jìn)度條
- 上傳完畢后需要發(fā)送合并請(qǐng)求
后端
- 上傳分片的接口
- 合并分片的接口
- 獲取分片的接口
- 其他工具方法,用于輔助
前端端需要注意的就是: 文件的切割,和進(jìn)度條
后端需要注意的就是: 分片存儲(chǔ)的地方和如何進(jìn)行合并分片
效果演示
先找到需要上傳的文件

當(dāng)我們開(kāi)始上傳進(jìn)度條就會(huì)發(fā)生變化,當(dāng)我們點(diǎn)擊停止上傳那么進(jìn)度條就會(huì)停止

我們后端會(huì)通過(guò)文件名+文件大小進(jìn)行MD5生成對(duì)應(yīng)的目錄結(jié)果如下:

當(dāng)前端上傳文件達(dá)到100%時(shí)候就會(huì)發(fā)送文件合并請(qǐng)求,然后我們后端這些分片都將被合并成一個(gè)文件

通過(guò)下圖可以看到所有分片都沒(méi)有了,從而合并出來(lái)一個(gè)文件

以上就是斷點(diǎn)續(xù)傳的核心原理,但是還需處理一些異常情況:
- 文件上傳過(guò)程中網(wǎng)絡(luò)波動(dòng)導(dǎo)致流丟失一部分(比對(duì)大小)
- 文件上傳過(guò)程中,服務(wù)器丟失分片 (比對(duì)分片的連續(xù)度)
- 文件被篡改內(nèi)容(比對(duì)大小)
效驗(yàn)核心代

參考代碼
前端
<!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大文件斷點(diǎn)切割上傳</h1>
<div id="progressBar"></div>
<input id="file" name="mov" type="file" />
<input id="btn" type="button" value="點(diǎn)我上傳" />
<input id="btn1" type="button" value="點(diǎn)我停止上傳" />
<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的壓縮包,或者視頻等,這些文件太大了 (需要后端配合進(jìn)行)
class FileSliceUpload{
constructor(testingUrl, uploadUrl, margeUrl,progressUrl, fileSelect) {
this.testingUrl = testingUrl; // 檢測(cè)文件上傳的url
this.uploadUrl = uploadUrl;//文件上傳接口
this.margeUrl = margeUrl; // 合并文件接口
this.progressUrl = progressUrl; //進(jìn)度接口
this.fileSelect = fileSelect;
this.fileObj = null;
this.totalize = null;
this.blockSize = 1024 * 1024; //每次上傳多少字節(jié)1mb(最佳)
this.sta = 0; //起始位置
this.end = this.sta + this.blockSize; //結(jié)束位置
this.count = 0; //分片個(gè)數(shù)
this.barId = "bar"; //進(jìn)度條id
this.progressId = "progress";//進(jìn)度數(shù)值ID
this.fileSliceName = ""; //分片文件名稱(chēng)
this.fileName = "";
this.uploadFileInterval = null; //上傳文件定時(shí)器
}
/**
* 樣式可以進(jìn)行修改
* @param {*} progressId 需要將進(jìn)度條添加到那個(gè)元素下面
*/
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)
}
//續(xù)傳 在上傳前先去服務(wù)器檢測(cè)之前是否有上傳過(guò)這個(gè)文件,如果還有返回上傳的的分片,那么進(jìn)行續(xù)傳
// 將當(dāng)前服務(wù)器上傳的最后一個(gè)分片會(huì)從新上傳, 避免因?yàn)榫W(wǎng)絡(luò)的原因?qū)е路制瑩p壞
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
//計(jì)算起始位置和結(jié)束位置
this.sta = this.blockSize * this.count
//計(jì)算結(jié)束位置
this.end = this.sta + this.blockSize
} else {
this.sta = 0; //從頭開(kāi)始
this.end = this.sta + this.blockSize;
this.count = 0; //分片個(gè)數(shù)
}
}
}
}
stopUploadFile () {
clearInterval(this.uploadFileInterval)
}
// 文件上傳(單文件)
startUploadFile () {
// 進(jìn)度條
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;
//查詢(xún)是否存在之前上傳過(guò)此文件,然后繼續(xù)
this.sequelFile()
let ref = this; //拿到當(dāng)前對(duì)象的引用,因?yàn)槭窃诋惒街惺褂胻his就是他本身而不是class
this.uploadFileInterval = setInterval(function () {
if (ref.sta > ref.totalize) {
//上傳完畢后結(jié)束定時(shí)器
clearInterval(ref.uploadFileInterval)
//發(fā)送合并請(qǐng)求
ref.margeUploadFile ()
console.log("stop" + ref.sta);
return;
};
//分片名稱(chēng)
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); //異步發(fā)送文件,不管是否成功, 會(huì)定期檢測(cè)
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
let ret = JSON.parse(xhr.response)
if (ret.code == 20000) {
//計(jì)算進(jìn)度
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 + '%'
}
}
}
//起始位置等于上次上傳的結(jié)束位置
ref.sta = ref.end;
//結(jié)束位置等于上次上傳的結(jié)束位置+每次上傳的字節(jié)
ref.end = ref.sta + ref.blockSize;
}, 5)
}
margeUploadFile () {
console.log("檢測(cè)上傳的文件完整性..........");
var xhr = new XMLHttpRequest();
//文件分片的名稱(chēng)/分片大小/總大小
xhr.open('GET', this.margeUrl+ "/" + this.fileSliceName + "/" + this.blockSize + "/" + this.totalize, true);
xhr.send(); //發(fā)送請(qǐng)求
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("上傳完畢但是文件上傳過(guò)程中出現(xiàn)了異常", ret);
}
}
}
}
}
export default FileSliceUpload;
后端
因?yàn)榇a內(nèi)部使用較多自己封裝的工具類(lèi)的原因,以下代碼只提供原理的參考
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;//實(shí)際文件目錄
//獲取分片
@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();
}
//從小到大文件進(jìn)行按照序號(hào)排序,和判斷分片是否損壞
List<String> collect = fileSliceIsbadAndSort(file, fileSlicSize);
//獲取最后一個(gè)分片
String fileSliceName = collect.get(collect.size() - 1);
fileSliceName = new File(fileSliceName).getName();
int code = fileId(fileSliceName);
//服務(wù)器的分片總大小必須小于或者等于文件的總大小
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,用于前端計(jì)算進(jìn)度
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); //有多少個(gè)分片
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());
//阻塞循環(huán)判斷是否還在上傳 ,解決前端進(jìn)行ajax異步上傳的問(wèn)題
int beforeSize=filesAll.size();
while (true){
Thread.sleep(1000);
//之前分片數(shù)量和現(xiàn)在分片數(shù)據(jù)只差,如果大于1那么就在上傳,那么繼續(xù)
filesAll = FileUtils.getFilesAll(file.getAbsolutePath());
if (filesAll.size()-beforeSize>=1){
beforeSize=filesAll.size();
//繼續(xù)檢測(cè)
continue;
}
//如果是之前分片和現(xiàn)在的分片相等的,那么在阻塞2秒后檢測(cè)是否發(fā)生變化,如果還沒(méi)變化那么上傳全部完成,可以進(jìn)行合并了
//當(dāng)然這不是絕對(duì)的,只能解決網(wǎng)絡(luò)短暫的波動(dòng),因?yàn)橛锌赡馨l(fā)生斷網(wǎng)很長(zhǎng)時(shí)間,網(wǎng)絡(luò)恢復(fù)后文件恢復(fù)上傳, 這個(gè)問(wèn)題是避免不了的,所以我們?cè)谙旅娴拇a進(jìn)行數(shù)量的效驗(yàn)
// 因?yàn)槲覀儾豢赡芤恢钡戎W(wǎng)好,所以如果1~3秒內(nèi)沒(méi)有上傳新的內(nèi)容,那么我們默認(rèn)判定上傳完畢
if (beforeSize==filesAll.size()){
Thread.sleep(2000);
filesAll = FileUtils.getFilesAll(file.getAbsolutePath());
if (beforeSize==filesAll.size()){
break;
}
}
}
//分片數(shù)量效驗(yàn)
if (filesAll.size()!=l){
//分片缺少 ,刪除全部分片文件,從新上傳
FileUtils.delFilesAllReview(absoluteFilePathAndCreate,true);
return Result.Error();
}
//獲取實(shí)際的文件名稱(chēng),組裝路徑
String realFileName = realFileName(fileSlicNamee);
String realFileNamePath = ResourceFileUtil.getAbsoluteFilePathAndCreate(uploaddir+ realFileName);
//從小到大文件進(jìn)行按照序號(hào)排序 ,和檢查分片文件是否有問(wèn)題
List<String> collect = fileSliceIsbadAndSort(file, fileSlicSize);
int fileSliceSize = collect.size();
List<Future<?>> futures = new ArrayList<>();
// 將文件按照序號(hào)進(jìn)行合并 ,算出Runtime.getRuntime().availableProcessors()個(gè)線程 ,每個(gè)線程需要讀取多少分片, 和每個(gè)線程需要讀取多少字節(jié)大小
//有人會(huì)說(shuō)一個(gè)分片一個(gè)線程不行嗎,你想想如果上千或者上萬(wàn)分片的話,你創(chuàng)建這么多的線程需要多少時(shí)間,以及線程切換上下文切換和銷(xiāo)毀需要多少時(shí)間?
// 就算使用線程池,也頂不住啊,你內(nèi)存又有多大,能存下多少隊(duì)列?,并發(fā)高的話直接懟爆
int availableProcessors = Runtime.getRuntime().availableProcessors();
//每個(gè)線程讀取多少文件
int readFileSize = (int)Math.ceil((double)fileSliceSize / availableProcessors);
//每個(gè)線程需要讀取的文件大小
long readSliceSize = readFileSize * fileSlicSize;
for (int i = 0; i < availableProcessors; i++) {
int finalI = i;
Future<?> future = ExecutorUtils.createFuture("FIleSliceUploadController",()->{
//每個(gè)線程需要讀取多少字節(jié)
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){
//需要調(diào)整數(shù)組
bytes = ArrayByteUtil.getActualBytes(bytes);
}
try ( RandomAccessFile w = new RandomAccessFile(realFileNamePath, "rw");){
//當(dāng)前文件寫(xiě)入的位置
w.seek(finalI*readSliceSize);
w.write(bytes);
} catch (IOException e) {
e.printStackTrace();
}
});
futures.add(future);
}
//阻塞到全部線程執(zhí)行完畢后
ExecutorUtils.waitComplete(futures);
//刪除全部分片文件
FileUtils.delFilesAllReview(absoluteFilePathAndCreate,true);
}else {
//沒(méi)有這個(gè)分片相關(guān)的的目錄
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;
}
//通過(guò)文件名稱(chēng)獲取文件目錄
private String fileNameMd5Dir(String fileName,long fileSize){
return HashUtil.md5(fileName+fileSize);
}
//獲取分片的實(shí)際文件名
private String realFileName(String fileSliceName){
int i = fileSliceName.indexOf(identification) ;
String substring = fileSliceName.substring(0, i);
return substring;
}
//獲取文件序號(hào)
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("分片損壞");
}
//從小到大文件進(jìn)行按照序號(hào)排序
List<String> collect = filesAll.stream().sorted((a, b) -> fileId(a) - fileId(b)).collect(Collectors.toList());
//判斷文件是否損壞,將文件排序后,進(jìn)行前后序號(hào)相差大于1那么就代表少分片了
for (int i = 0; i < collect.size()-1; i++) {
//檢測(cè)分片的連續(xù)度
if (fileId(collect.get(i)) - fileId(collect.get(i+1))!=-1) {
//分片損壞 刪除全部分片文件 ,從新上傳
FileUtils.delFilesAllReview(absolutePath,true);
throw new Exception("分片損壞");
}
//檢測(cè)分片的完整度
if (new File(collect.get(i)).length()!=fileSlicSize) {
//分片損壞 刪除全部分片文件 ,從新上傳
FileUtils.delFilesAllReview(absolutePath,true);
throw new Exception("分片損壞");
}
}
return collect;
}
}
到此這篇關(guān)于通過(guò)Java實(shí)現(xiàn)文件斷點(diǎn)續(xù)傳功能的文章就介紹到這了,更多相關(guān)Java斷點(diǎn)續(xù)傳內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
springboot+redis實(shí)現(xiàn)簡(jiǎn)單的熱搜功能
這篇文章主要介紹了springboot+redis實(shí)現(xiàn)一個(gè)簡(jiǎn)單的熱搜功能,通過(guò)代碼介紹了過(guò)濾不雅文字的過(guò)濾器,代碼簡(jiǎn)單易懂,對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-05-05
Java 實(shí)戰(zhàn)練手項(xiàng)目之醫(yī)院預(yù)約掛號(hào)系統(tǒng)的實(shí)現(xiàn)流程
讀萬(wàn)卷書(shū)不如行萬(wàn)里路,只學(xué)書(shū)上的理論是遠(yuǎn)遠(yuǎn)不夠的,只有在實(shí)戰(zhàn)中才能獲得能力的提升,本篇文章手把手帶你用java+SpringBoot+Maven+Vue+mysql實(shí)現(xiàn)一個(gè)醫(yī)院預(yù)約掛號(hào)系統(tǒng),大家可以在過(guò)程中查缺補(bǔ)漏,提升水平2021-11-11
springboot配置kafka批量消費(fèi),并發(fā)消費(fèi)方式
文章介紹了如何在Spring Boot中配置Kafka進(jìn)行批量消費(fèi),并發(fā)消費(fèi),需要注意的是,并發(fā)量必須小于等于分區(qū)數(shù),否則會(huì)導(dǎo)致線程空閑,文章還總結(jié)了創(chuàng)建Kafka分區(qū)的命令,并鼓勵(lì)讀者分享經(jīng)驗(yàn)2024-12-12
Java規(guī)則引擎easy-rules詳細(xì)介紹
本文主要介紹了Java規(guī)則引擎easy-rules詳細(xì)介紹,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-01-01
Jenkins環(huán)境搭建實(shí)現(xiàn)過(guò)程圖解
這篇文章主要介紹了Jenkins環(huán)境搭建實(shí)現(xiàn)過(guò)程圖解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-09-09
SpringBoot項(xiàng)目上高并發(fā)問(wèn)題的解決方案
本章演示在springboot項(xiàng)目中的高并發(fā)demo,演示導(dǎo)致的問(wèn)題,以及單機(jī)部署下的解決方案和集群部署下的解決方式以及分布式下的解決方案,文中通過(guò)圖文結(jié)合的方式講解的非常詳細(xì),需要的朋友可以參考下2024-06-06

