0.需求背景
- 文件过大,单次文件流数据过多
- 需要有下载进度
- 需要提高下载速度
1.大文件下载的解决思路
- 获取文件大小,根据实际网络情况设置分片大小,确定份数
- 根据分片的大小索引,获取分片的流数据
- 所有的分片下载后,合并成最终文件
2.核心代码参考
前端 vue3
const downFile = async () => { progress.value = 0; finalFine.value = 0; //获取文件的大小 let {fileName, contentLength} = await getFileSize(); //根据大小,去获取 let RANGE_SIZE = 1024 * 1024 let total = Math.ceil(contentLength / RANGE_SIZE);//向上取整 let fileArr = [] for (let i = 0; i < total; i++) { let start = i * RANGE_SIZE let end = start + RANGE_SIZE - 1 let range = end >= contentLength ? contentLength - start : RANGE_SIZE fileArr.push({ index: i, start: start, end: end, range: range, chunk: undefined, downloadTimes: 0 }) } getFileChunkArray(fileArr, total, fileName);
} async function getFileSize() { //获取文件的大小 let response = await axios({ method: 'get', url: 'bigfile/getFileSize', params: {} }) let result = response.data; let contentLength = result.contentLength; let fileName = result.fileName; console.log("contentLength: ", contentLength); return {fileName: fileName, contentLength: contentLength}
} const getFileChunkArray = async (fileArr, total, fileName) => { let pool = [] // 并发池 const max = 10 // 并发池异步操作的数量 ★ 设置太大或者太小,都不合适,会引起并发问题 let failList = [] // 下载失败的文件列表 let finish = 0 // 下载完成的数量 if (fileArr.length === 0) { fineList.sort((a, b) => a.index - b.index); let arrayBufferArray = fineList.map(item => item.chunk); const blob = new Blob(arrayBufferArray, {type: 'application/octet-stream'}); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = fileName; a.click(); } for (const item of fileArr) { let index = item.index let start = item.start let end = item.end let range = item.range let downloadTimes = item.downloadTimes if (downloadTimes > 5) { console.log("异常,已执行超过5次 ----- ") break } console.log("range: ", index, `bytes=${start}-${end}`, range); let res = axios({ method: 'get', url: 'bigfile/download', params: { index: index, start: start, end: end, range: range, }, responseType: 'blob' }) // 把下载文件的异步操作放入并发池里 pool.push(res) if (pool.length === max) { // 每当并发池跑完一个任务,就再塞入一个任务 await Promise.race(pool) } res.then((response) => { let clip = response.data; fineList.push({...item, chunk: clip}) finalFine.value++ progress.value = Math.round((finalFine.value) / total * 10000) / 100 // console.log('response: ', response) console.log('下载进度 ----- ', finalFine.value, total, progress.value + '%') }).catch((response) => { console.log('response-error: ', response) // 请求失败,从并发池里移除,添加到失败的文件列表 const index = pool.findIndex(it => it === res) pool.splice(index, 1) item.downloadTimes++; failList.push(item) }).finally(() => { finish++ // 如果请求都完成了,递归调用自己,把下载失败的文件列表再下载一次 if (finish === fileArr.length) { getFileChunkArray(failList, total, fileName); } }) }
}
后端java
/** * 获取文件大小 * * @param request 请求 * @return {@link JSONObject } * @author Ysy **/@RequestMapping(value = "/getFileSize", method = RequestMethod.GET)
public JSONObject getFileSize(HttpServletRequest request) { JSONObject result = new JSONObject(); String filePath = "D:\\" + downloadFile; System.out.println(filePath); File file = new File(filePath); long contentLength = 0; if (file.exists()) { contentLength = file.length(); } result.put("contentLength", contentLength); result.put("fileName", downloadFile); return result;
} /** * 下载 * * @param request 请求 * @param response 响应 * @param index 指数 * @param start 开始 * @param end 结束 * @param range 范围 * @return {@link ResponseEntity } * @author Ysy **/@RequestMapping(value = "download", method = RequestMethod.GET)
public ResponseEntity download(HttpServletRequest request, HttpServletResponse response, @RequestParam(value = "index", defaultValue = "") long index, @RequestParam(value = "start", defaultValue = "") long start, @RequestParam(value = "end", defaultValue = "") long end, @RequestParam(value = "range", defaultValue = "") int range
) throws IOException { String filePath = "D:\\" + downloadFile; File file = new File(filePath); if (!file.exists()) { } System.out.println(index); // 返回 ResponseEntity try ( FileInputStream fis = new FileInputStream(file); FileChannel channel = fis.getChannel()) { // 设置起始位置 channel.position(start); ByteBuffer buffer = ByteBuffer.allocate(range); int bytesRead = channel.read(buffer); if (bytesRead > 0) { buffer.flip(); byte[] data = new byte[bytesRead]; buffer.get(data); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); headers.setContentLength(bytesRead); return new ResponseEntity<>(data, headers, HttpStatus.OK); } else { return new ResponseEntity<>(HttpStatus.NO_CONTENT); } }
}
效果
4.案例源码
百度网盘 链接:https://pan.baidu.com/s/15TZCM4dfgDRRBneyHJEkaQ
码微信小程序。获取 提取码
分片下载和A标签下载对比
1. 基本概念
- 分片下载:
- 分片下载是一种将文件分割成多个部分(片)进行下载的技术。它通过多线程或多任务的方式,同时从服务器获取文件的不同部分。例如,一个大文件被分成10个部分,程序可以同时开启多个连接,分别下载这10个部分,然后在本地将这些部分组合成完整的文件。这种方式可以充分利用网络带宽,提高下载速度。在一些专业的下载工具(如迅雷)中,就广泛应用了分片下载技术。
- A标签下载:
<a>
标签是HTML中的超链接标签。当使用<a>
标签进行下载时,用户点击链接后,浏览器会根据链接指向的资源类型和服务器的配置,尝试以默认的方式下载文件。例如,<a href="example.pdf" download>下载文件</a>
,当用户点击这个链接时,浏览器会开始下载example.pdf
文件。它的下载过程相对比较简单直接,通常是单线程的,由浏览器直接处理下载请求。
1. 下载速度
- 分片下载:
- 由于采用多线程或多任务方式,多个分片可以同时下载,能够更好地利用网络带宽。特别是在下载大型文件和网络环境较好的情况下,分片下载可以显著提高下载速度。例如,对于一个1GB的文件,如果服务器和网络允许,分片下载可以同时从服务器获取多个部分,每个部分都能占用一定的带宽进行传输,从而加快整体下载进度。
- A标签下载:
- 通常是单线程下载,速度主要取决于网络带宽以及服务器对该文件下载的限制。对于小文件,单线程下载可能足够快,但对于大型文件,其下载速度可能会受到限制,因为它无法像分片下载那样同时获取文件的多个部分来充分利用带宽。
2. 资源占用情况
- 分片下载:
- 因为涉及多个线程或任务同时运行,会占用较多的系统资源,如CPU和内存。多个线程的调度和文件分片的管理都需要一定的系统资源支持。例如,在进行大量分片下载时,可能会导致计算机的CPU使用率升高,内存占用量增大,从而可能会影响计算机的其他操作性能。
- A标签下载:
- 资源占用相对较少,主要是浏览器本身的资源占用。它只是通过浏览器的单线程下载机制进行文件下载,不需要额外的复杂线程管理和文件分片组合操作,所以对系统资源的需求比较低。
3. 稳定性和可靠性
- 分片下载:
- 相对复杂的下载过程可能会导致一些稳定性问题。例如,如果其中一个分片的下载出现错误(如网络中断、服务器故障等),可能需要重新下载该分片或者采取一些错误恢复措施。不过,好的分片下载工具会有相应的机制来处理这些情况,比如重试下载出错的分片一定次数后,如果还不行,可能会从其他服务器或节点获取该分片。
- A标签下载:
- 比较简单直接,相对更稳定。如果下载过程中出现问题(如网络中断),浏览器通常会提供简单的恢复机制,如重新开始下载或者提示用户是否继续等。因为它没有复杂的分片管理,所以出现错误后的恢复相对比较容易理解和操作。
4. 适用场景
- 分片下载:
- 适用于大型文件的下载,尤其是在需要快速下载并且网络条件允许的情况下。例如,下载大型的软件安装包、高清视频文件等。同时,在一些需要对下载过程进行更精细控制的场景下也很有用,比如暂停、恢复各个分片的下载,或者设置不同分片的优先级等。
- A标签下载:
- 适合于小文件的下载,如文档、图片等。对于一些简单的文件下载需求,不需要复杂的下载工具,使用
<a>
标签就可以方便地实现文件下载。而且,在网页开发中,对于大多数普通的文件下载链接,<a>
标签是一种简单直接的实现方式。