如何使用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> <h2 id="html-大文件断点切割上传">html5大文件断点切割上传</h2> <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實作檔案的斷點續傳功能?的詳細內容。更多資訊請關注PHP中文網其他相關文章!

熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

Video Face Swap
使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

熱工具

記事本++7.3.1
好用且免費的程式碼編輯器

SublimeText3漢化版
中文版,非常好用

禪工作室 13.0.1
強大的PHP整合開發環境

Dreamweaver CS6
視覺化網頁開發工具

SublimeText3 Mac版
神級程式碼編輯軟體(SublimeText3)

Java 8引入了Stream API,提供了一種強大且表達力豐富的處理數據集合的方式。然而,使用Stream時,一個常見問題是:如何從forEach操作中中斷或返回? 傳統循環允許提前中斷或返回,但Stream的forEach方法並不直接支持這種方式。本文將解釋原因,並探討在Stream處理系統中實現提前終止的替代方法。 延伸閱讀: Java Stream API改進 理解Stream forEach forEach方法是一個終端操作,它對Stream中的每個元素執行一個操作。它的設計意圖是處

PHP是一種廣泛應用於服務器端的腳本語言,特別適合web開發。 1.PHP可以嵌入HTML,處理HTTP請求和響應,支持多種數據庫。 2.PHP用於生成動態網頁內容,處理表單數據,訪問數據庫等,具有強大的社區支持和開源資源。 3.PHP是解釋型語言,執行過程包括詞法分析、語法分析、編譯和執行。 4.PHP可以與MySQL結合用於用戶註冊系統等高級應用。 5.調試PHP時,可使用error_reporting()和var_dump()等函數。 6.優化PHP代碼可通過緩存機制、優化數據庫查詢和使用內置函數。 7

PHP和Python各有優勢,選擇應基於項目需求。 1.PHP適合web開發,語法簡單,執行效率高。 2.Python適用於數據科學和機器學習,語法簡潔,庫豐富。

PHP適合web開發,特別是在快速開發和處理動態內容方面表現出色,但不擅長數據科學和企業級應用。與Python相比,PHP在web開發中更具優勢,但在數據科學領域不如Python;與Java相比,PHP在企業級應用中表現較差,但在web開發中更靈活;與JavaScript相比,PHP在後端開發中更簡潔,但在前端開發中不如JavaScript。

PHP和Python各有優勢,適合不同場景。 1.PHP適用於web開發,提供內置web服務器和豐富函數庫。 2.Python適合數據科學和機器學習,語法簡潔且有強大標準庫。選擇時應根據項目需求決定。

膠囊是一種三維幾何圖形,由一個圓柱體和兩端各一個半球體組成。膠囊的體積可以通過將圓柱體的體積和兩端半球體的體積相加來計算。本教程將討論如何使用不同的方法在Java中計算給定膠囊的體積。 膠囊體積公式 膠囊體積的公式如下: 膠囊體積 = 圓柱體體積 兩個半球體體積 其中, r: 半球體的半徑。 h: 圓柱體的高度(不包括半球體)。 例子 1 輸入 半徑 = 5 單位 高度 = 10 單位 輸出 體積 = 1570.8 立方單位 解釋 使用公式計算體積: 體積 = π × r2 × h (4

PHPhassignificantlyimpactedwebdevelopmentandextendsbeyondit.1)ItpowersmajorplatformslikeWordPressandexcelsindatabaseinteractions.2)PHP'sadaptabilityallowsittoscaleforlargeapplicationsusingframeworkslikeLaravel.3)Beyondweb,PHPisusedincommand-linescrip

PHP成為許多網站首選技術棧的原因包括其易用性、強大社區支持和廣泛應用。 1)易於學習和使用,適合初學者。 2)擁有龐大的開發者社區,資源豐富。 3)廣泛應用於WordPress、Drupal等平台。 4)與Web服務器緊密集成,簡化開發部署。
