Java實(shí)現(xiàn)斷點(diǎn)下載功能的示例代碼
介紹
當(dāng)下載一個(gè)很大的文件時(shí),如果下載到一半暫停,如果繼續(xù)下載呢?斷點(diǎn)下載就是解決這個(gè)問題的。
具體原理:
利用indexedDb,將下載的數(shù)據(jù)存儲(chǔ)到用戶的本地中,這樣用戶就算是關(guān)電腦那么下次下載還是從上次的位置開始的
- 先去看看本地緩存中是否存在這個(gè)文件的分片數(shù)據(jù),如果存在那么就接著上一個(gè)分片繼續(xù)下載(起始位置)
- 下載前先去后端拿文件的大小,然后計(jì)算分多少次下載(n/(1024*1024*10)) (結(jié)束位置)
- 每次下載的數(shù)據(jù)放入一個(gè)Blob中,然后存儲(chǔ)到本地indexedDB
- 當(dāng)全部下載完畢后,將所有本地緩存的分片全部合并,然后給用戶
有很多人說必須使用content-length、Accept-Ranges、Content-Range還有Range。 但是這只是一個(gè)前后端的約定而已,所有沒必須非要遵守,只要你和后端約定好怎么拿取數(shù)據(jù)就行
難點(diǎn)都在前端:
- 怎么存儲(chǔ)
- 怎么計(jì)算下載多少次
- 怎么獲取最后下載的分片是什么
- 怎么判斷下載完成了
- 怎么保證下載的分片都是完整的
- 下載后怎么合并然后給用戶
效果
前端代碼
<!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> <Button id="but">下載</Button> <Button id="stop">暫停</Button> <script type="module"> import FileSliceDownload from '/src/file/FileSliceDownload.js' let downloadUrl = "http://localhost:7003/fileslice/dwnloadsFIleSlice" let fileSizeUrl = "http://localhost:7003/fileslice/fIleSliceDownloadSize" let fileName = "Downloads.zip" let but = document.querySelector("#but") let stop = document.querySelector("#stop") let fileSliceDownload = new FileSliceDownload(downloadUrl, fileSizeUrl); fileSliceDownload.addProgress("#progressBar") but.addEventListener("click", function () { fileSliceDownload.startDownload(fileName) }) stop.addEventListener("click", function () { fileSliceDownload.stop() }) </script> </body> </html>
class BlobUtls{ // blob轉(zhuǎn)文件并下載 static downloadFileByBlob(blob, fileName = "file") { let blobUrl = window.URL.createObjectURL(blob) let link = document.createElement('a') link.download = fileName || 'defaultName' link.style.display = 'none' link.href = blobUrl // 觸發(fā)點(diǎn)擊 document.body.appendChild(link) link.click() // 移除 document.body.removeChild(link) } } export default BlobUtls;
//導(dǎo)包要從項(xiàng)目全路徑開始,也就是最頂部 import BlobUtls from '/web-js/src/blob/BlobUtls.js' //導(dǎo)包 class FileSliceDownload{ #m1=1024*1024*10 //1mb 每次下載多少 #db //indexedDB庫對(duì)象 #downloadUrl // 下載文件的地址 #fileSizeUrl // 獲取文件大小的url #fileSiez=0 //下載的文件大小 #fileName // 下載的文件名稱 #databaseName="dbDownload"; //默認(rèn)庫名稱 #tableDadaName="tableDada" //用于存儲(chǔ)數(shù)據(jù)的表 #tableInfoName="tableInfo" //用于存儲(chǔ)信息的表 #fIleReadCount=0 //文件讀取次數(shù) #fIleStartReadCount=0//文件起始的位置 #barId = "bar"; //進(jìn)度條id #progressId = "progress";//進(jìn)度數(shù)值ID #percent=0 //百分比 #checkDownloadInterval=null; //檢測(cè)下載是否完成定時(shí)器 #mergeInterval=null;//檢測(cè)是否滿足合并分片要求 #stop=false; //是否結(jié)束 //下載地址 constructor(downloadUrl,fileSizeUrl) { this.check() this.#downloadUrl=downloadUrl; this.#fileSizeUrl=fileSizeUrl; } check(){ let indexedDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB ; if(!indexedDB){ alert('不支持'); } } //初始化 #init(fileName){ return new Promise((resolve,reject)=>{ this.#fileName=fileName; this.#percent=0; this.#stop=false; const request = window.indexedDB.open(this.#databaseName, 1) request.onupgradeneeded = (e) => { const db = e.target.result if (!db.objectStoreNames.contains(this.#tableDadaName)) { db.createObjectStore(this.#tableDadaName, { keyPath: 'serial',autoIncrement:false }) db.createObjectStore(this.#tableInfoName, { keyPath: 'primary',autoIncrement:false }) } } request.onsuccess = e => { this.#db = e.target.result resolve() } }) } #getFileSize(){ return new Promise((resolve,reject)=>{ let ref=this; var xhr = new XMLHttpRequest(); //同步 xhr.open("GET", this.#fileSizeUrl+"/"+this.#fileName,false) xhr.send() if (xhr.readyState === 4 && xhr.status === 200) { let ret = JSON.parse(xhr.response) if (ret.code === 20000) { ref.#fileSiez=ret.data } resolve() } }) } #getTransactionDadaStore(){ let transaction = this.#db.transaction([this.#tableDadaName], 'readwrite') let store = transaction.objectStore(this.#tableDadaName) return store; } #getTransactionInfoStore(){ let transaction = this.#db.transaction([this.#tableInfoName], 'readwrite') let store = transaction.objectStore(this.#tableInfoName) return store; } #setBlob(begin,end,i,last){ return new Promise((resolve,reject)=>{ var xhr = new XMLHttpRequest(); xhr.open("GET", this.#downloadUrl+"/"+this.#fileName+"/"+begin+"/"+end+"/"+last) xhr.responseType="blob" // 只支持異步,默認(rèn)使用 text 作為默認(rèn)值。 xhr.send() xhr.onload = ()=> { if (xhr.status === 200) { let store= this.#getTransactionDadaStore() let obj={serial:i,blob:xhr.response} //添加分片到用戶本地的庫中 store.add(obj) let store2= this.#getTransactionInfoStore() //記錄下載了多少個(gè)分片了 store2.put({primary:"count",count:i}) //調(diào)整進(jìn)度條 let percent1= Math.ceil( (i/this.#fIleReadCount)*100) if(this.#percent<percent1){ this.#percent=percent1; } this.#dynamicProgress() resolve() } } }) } #mergeCallback(){ // 讀取全部字節(jié)到blob里,處理合并 let arrayBlobs = []; let store1 = this.#getTransactionDadaStore() //按順序找到全部的分片 for (let i = 0; i <this.#fIleReadCount; i++) { let result= store1.get(IDBKeyRange.only(i)) result.onsuccess=(data)=>{ arrayBlobs.push(data.target.result.blob) } } //分片合并下載 this.#mergeInterval= setInterval(()=> { if(arrayBlobs.length===this.#fIleReadCount){ clearInterval(this.#mergeInterval); //多個(gè)Blob進(jìn)行合并 let fileBlob = new Blob(arrayBlobs);//合并后的數(shù)組轉(zhuǎn)成?個(gè)Blob對(duì)象。 BlobUtls.downloadFileByBlob(fileBlob,this.#fileName) //下載完畢后清除數(shù)據(jù) this. #clear() } },200) } #clear(){ let store2 = this.#getTransactionDadaStore() let store3 = this.#getTransactionInfoStore() store2.clear() //清除本地全下載的數(shù)據(jù) store3.delete("count")//記錄清除 this.#fIleStartReadCount=0 //起始位置 this.#db=null; this.#fileName=null; this.#fileSiez=0; this.#fIleReadCount=0 //文件讀取次數(shù) this.#fIleStartReadCount=0//文件起始的位置 } //檢測(cè)是否有分片在本地 #checkSliceDoesIsExist(){ return new Promise((resolve,reject)=>{ let store1 = this.#getTransactionInfoStore() let result= store1.get(IDBKeyRange.only("count")) result.onsuccess=(data)=>{ let count= data.target.result?.count if(count){ //防止因?yàn)榫W(wǎng)絡(luò)的原因?qū)е路制瑩p壞,所以不要最后一個(gè)分片 this.#fIleStartReadCount=count-1; } resolve(); } }) } /** * 樣式可以進(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) } #dynamicProgress(){ //調(diào)整進(jìn)度 let bar = document.getElementById(this.#barId) let progressEl = document.getElementById(this.#progressId) bar.style.width = this.#percent + '%'; bar.style.backgroundColor = 'red'; progressEl.innerHTML = this.#percent + '%' } stop(){ this.#stop=true; } startDownload(fileName){ //同步代碼塊 ;(async ()=>{ //初始化 await this.#init(fileName) //自動(dòng)調(diào)整分片,如果本地以下載了那么從上一次繼續(xù)下載 await this.#checkSliceDoesIsExist() //拿到文件的大小 await this.#getFileSize() let begin=0; //開始讀取的字節(jié) let end=this.#m1; // 結(jié)束讀取的字節(jié) let last=false; //是否是最后一次讀取 this.#fIleReadCount= Math.ceil( this.#fileSiez/this.#m1) for (let i = this.#fIleStartReadCount; i < this.#fIleReadCount; i++) { if(this.#stop){ return } begin=i*this.#m1; end=begin+this.#m1 if(i===this.#fIleReadCount-1){ last=true; } //添加分片 await this.#setBlob(begin,end,i,last) } //定時(shí)檢測(cè)存下載的分片數(shù)量是否夠了 this.#checkDownloadInterval= setInterval(()=> { let store = this.#getTransactionDadaStore() let result = store.count() result.onsuccess = (data) => { if (data.target.result === this.#fIleReadCount) { clearInterval(this.#checkDownloadInterval); //如果分片夠了那么進(jìn)行合并下載 this.#mergeCallback() } } },200) })() } } export default FileSliceDownload;
后端代碼
package com.controller.commontools.fileDownload; import com.application.Result; import com.container.ArrayByteUtil; import com.file.FileWebDownLoad; import com.file.ReadWriteFileUtils; import com.path.ResourceFileUtil; import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletResponse; import java.io.BufferedOutputStream; import java.io.File; import java.io.OutputStream; import java.net.URLEncoder; @RestController @RequestMapping("/fileslice") public class FIleSliceDownloadController { private final String uploaddir="uploads"+ File.separator+"real"+File.separator;//實(shí)際文件目錄 // 獲取文件的大小 @GetMapping("/fIleSliceDownloadSize/{fileName}") public Result getFIleSliceDownloadSize(@PathVariable String fileName){ String absoluteFilePath = ResourceFileUtil.getAbsoluteFilePathAndCreate(uploaddir)+File.separator+fileName; File file= new File(absoluteFilePath); if(file.exists()&&file.isFile()){ return Result.Ok(file.length(),Long.class); } return Result.Error(); } /** * 分段下載文件 * @param fileName 文件名稱 * @param begin 從文件什么位置開始讀取 * @param end 到什么位置結(jié)束 * @param last 是否是最后一次讀取 * @param response */ @GetMapping("/dwnloadsFIleSlice/{fileName}/{begin}/{end}/{last}") public void dwnloadsFIleSlice(@PathVariable String fileName, @PathVariable long begin, @PathVariable long end, @PathVariable boolean last, HttpServletResponse response){ String absoluteFilePath = ResourceFileUtil.getAbsoluteFilePathAndCreate(uploaddir)+File.separator+fileName; File file= new File(absoluteFilePath); try(OutputStream toClient = new BufferedOutputStream(response.getOutputStream())) { long readSize = end - begin; //讀取文件的指定字節(jié) byte[] bytes = new byte[(int)readSize]; ReadWriteFileUtils.randomAccessFileRead(file.getAbsolutePath(),(int)begin,bytes); if(readSize<=file.length()||last){ bytes=ArrayByteUtil.getActualBytes(bytes); //去掉多余的 } response.setContentType("application/octet-stream"); response.addHeader("Content-Length", String.valueOf(bytes.length)); response.setHeader("Content-Disposition", "attachment;filename*=UTF-8''" + URLEncoder.encode(fileName, "UTF-8")); toClient.write(bytes); } catch (Exception e) { e.printStackTrace(); } } }
以上就是Java實(shí)現(xiàn)斷點(diǎn)下載功能的示例代碼的詳細(xì)內(nèi)容,更多關(guān)于Java斷點(diǎn)下載的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Spring Boot 中實(shí)現(xiàn)跨域的多種方式小結(jié)
Spring Boot提供了多種方式來實(shí)現(xiàn)跨域請(qǐng)求,開發(fā)者可以根據(jù)具體需求選擇適合的方法,在配置時(shí),要確保不僅考慮安全性,還要兼顧應(yīng)用的靈活性和性能,本文給大家介紹Spring Boot 中實(shí)現(xiàn)跨域的多種方式,感興趣的朋友一起看看吧2024-01-01Java實(shí)現(xiàn)月餅的制作、下單和售賣功能
這篇文章主要介紹了Java實(shí)現(xiàn)月餅的制作、下單和售賣,借此機(jī)會(huì),我們用Lambda實(shí)現(xiàn)一遍月餅制作,下單,售賣的開發(fā)設(shè)計(jì)模式,主要有制作月餅的工廠模式,結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2022-09-09Java的Netty進(jìn)階之Future和Promise詳解
這篇文章主要介紹了Java的Netty進(jìn)階之Future和Promise詳解,Netty 是基于 Java NIO 的異步事件驅(qū)動(dòng)的網(wǎng)絡(luò)應(yīng)用框架,使用 Netty 可以快速開發(fā)網(wǎng)絡(luò)應(yīng)用,Netty 提供了高層次的抽象來簡(jiǎn)化 TCP 和 UDP 服務(wù)器的編程,但是你仍然可以使用底層的 API,需要的朋友可以參考下2023-11-11java從mysql導(dǎo)出數(shù)據(jù)的具體實(shí)例
這篇文章主要介紹了java從mysql導(dǎo)出數(shù)據(jù)的具體實(shí)例,有需要的朋友可以參考一下2013-12-12SpringBoot Import及自定義裝配實(shí)現(xiàn)方法解析
這篇文章主要介紹了SpringBoot Import及自定義裝配實(shí)現(xiàn)方法解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-08-08java高并發(fā)InterruptedException異常引發(fā)思考
這篇文章主要為大家介紹了java高并發(fā)InterruptedException異常引發(fā)思考,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08java?LockSupport實(shí)現(xiàn)原理示例解析
這篇文章主要為大家介紹了java?LockSupport實(shí)現(xiàn)原理示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01Java接口的作用_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要介紹了Java接口的作用,涉及到接口的規(guī)范相關(guān)知識(shí),需要的的朋友參考下2017-04-04Java對(duì)象在內(nèi)存中的布局是如何實(shí)現(xiàn)的?
Java對(duì)象在內(nèi)存中屬于oop-klass二分模型,即對(duì)象的實(shí)例數(shù)據(jù)和對(duì)象類型的元數(shù)據(jù)(字段定義、方法、常量池等元數(shù)據(jù))是分開存儲(chǔ)的.而由于JVM對(duì)對(duì)象內(nèi)相同寬度的字段分配在一起,所以只要指定了字段類型分配的順序,就可以計(jì)算出每種類型字段相對(duì)于當(dāng)前對(duì)象的偏移起始位置2021-06-06