介绍
当下载一个很大的文件时,如果下载到一半暂停,如果继续下载呢?断点下载就是解决这个问题的。
具体原理:
利用indexedDb,将下载的数据存储到用户的本地中,这样用户就算是关电脑那么下次下载还是从上次的位置开始的
先去看看本地缓存中是否存在这个文件的分片数据,如果存在那么就接着上一个分片继续下载(起始位置) 下载前先去后端拿文件的大小,然后计算分多少次下载(n/(1024*1024*10)) (结束位置) 每次下载的数据放入一个Blob中,然后存储到本地indexedDB 当全部下载完毕后,将所有本地缓存的分片全部合并,然后给用户有很多人说必须使用content-length、Accept-Ranges、Content-Range还有Range。 但是这只是一个前后端的约定而已,所有没必须非要遵守,只要你和后端约定好怎么拿取数据就行
难点都在前端:
怎么存储 怎么计算下载多少次 怎么获取最后下载的分片是什么 怎么判断下载完成了 怎么保证下载的分片都是完整的 下载后怎么合并然后给用户
效果
前端代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
<!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 > < 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 > |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class BlobUtls{
// blob转文件并下载 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 // 触发点击 document.body.appendChild(link) link.click() // 移除 document.body.removeChild(link) }
} export default BlobUtls; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 |
//导包要从项目全路径开始,也就是最顶部 import BlobUtls from '/web-js/src/blob/BlobUtls.js' //导包 class FileSliceDownload{ #m1=1024*1024*10 //1mb 每次下载多少 #db //indexedDB库对象 #downloadUrl // 下载文件的地址 #fileSizeUrl // 获取文件大小的url #fileSiez=0 //下载的文件大小 #fileName // 下载的文件名称 #databaseName="dbDownload"; //默认库名称 #tableDadaName="tableDada" //用于存储数据的表 #tableInfoName="tableInfo" //用于存储信息的表 #fIleReadCount=0 //文件读取次数 #fIleStartReadCount=0//文件起始的位置 #barId = "bar"; //进度条id #progressId = "progress";//进度数值ID #percent=0 //百分比 #checkDownloadInterval=null; //检测下载是否完成定时器 #mergeInterval=null;//检测是否满足合并分片要求 #stop=false; //是否结束 //下载地址 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" // 只支持异步,默认使用 text 作为默认值。 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() //记录下载了多少个分片了 store2.put({primary: "count" ,count:i})
//调整进度条 let percent1= Math.ceil( (i/ this . #fIleReadCount)*100) if ( this . #percent<percent1){ this . #percent=percent1; } this . #dynamicProgress() resolve()
} } })
}
#mergeCallback(){ // 读取全部字节到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); //多个Blob进行合并 let fileBlob = new Blob(arrayBlobs); //合并后的数组转成?个Blob对象。 BlobUtls.downloadFileByBlob(fileBlob, this . #fileName)
//下载完毕后清除数据 this . #clear() }
},200) } #clear(){ let store2 = this . #getTransactionDadaStore() let store3 = this . #getTransactionInfoStore() store2.clear() //清除本地全下载的数据 store3. delete ( "count" ) //记录清除 this . #fIleStartReadCount=0 //起始位置 this . #db=null; this . #fileName=null; this . #fileSiez=0; this . #fIleReadCount=0 //文件读取次数 this . #fIleStartReadCount=0//文件起始的位置
}
//检测是否有分片在本地 #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){ //防止因为网络的原因导致分片损坏,所以不要最后一个分片 this . #fIleStartReadCount=count-1; } resolve(); } })
}
/** * 样式可以进行修改 * @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) } #dynamicProgress(){ //调整进度 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)
//自动调整分片,如果本地以下载了那么从上一次继续下载 await this . #checkSliceDoesIsExist() //拿到文件的大小 await this . #getFileSize() let begin=0; //开始读取的字节 let end= this . #m1; // 结束读取的字节 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) }
//定时检测存下载的分片数量是否够了 this . #checkDownloadInterval= setInterval(()=> { let store = this . #getTransactionDadaStore() let result = store.count() result.onsuccess = (data) => { if (data.target.result === this . #fIleReadCount) { clearInterval( this . #checkDownloadInterval); //如果分片够了那么进行合并下载 this . #mergeCallback() } } },200)
})()
}
}
export default FileSliceDownload; |
后端代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
package com.controller测试数据montools.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; //实际文件目录 // 获取文件的大小 @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 到什么位置结束 * @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; //读取文件的指定字节 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实现断点下载功能的示例代码的详细内容,更多关于Java断点下载的资料请关注其它相关文章!
原文链接:https://blog.csdn.net/weixin_45203607/article/details/124998112
查看更多关于Java实现断点下载功能的示例代码的详细内容...