Java大文件下载方案(vue+饿了么):分片下载、断点续载!

前言

本篇文章是基于其他文章的基础上结合自己的理解写出来的,如果哪里有问题请指出!

详细教程

分片下载

1.什么是分片下载

分片下载是指将一个大的文件分成多个较小的部分(分片或块),然后并行地从服务器下载这些部分到客户端的过程。

2.分片下载的场景

1.大文件下载

2.网络环境环境不好,存在需要重新下载风险的场景.

断点续传

1、什么是断点续传

断点续传是在下载时,将下载任务(一个文件或一个压缩包)人为的划分为几个部分,每一个部分采用一个线程进行下载,如果碰到网络故障,可以从已经下载的部分开始继续下载未完成的部分,而没有必要从头开始上传或者下载。

实现流程步骤

1.本文实现的步骤
  • 前端(客户端)需要根据固定大小对文件进行分片,请求后端(服务端)时要带上分片开始位置,结束位置以及文件位置.

  • 服务端根据上分片开始位置,结束位置以及文件位置.将文件读取成流的形式返回前端(

  • 前端(客户端)需要将每次分片请求的流暂时存储到前端(这步是实现断点续传的核心步骤,本文是丢到indexedDB里面)最后读取完所有分片,组合为下载的文件。

2.分片上传/断点上传代码实现

a、前端框架使用的是vue+饿了么UI,进行分片下载。

b、后端实现文件读取,是用MappedByteBuffer实现。

前端(客户端)大概流程:

根据点击下载的文件获取行数据->通过特定字符串+id初始化indexedDB库,以及store->获取indexedDB库存储的数据->计算分片参数->循环调用分片下载请求->记录下载数据到indexedDB->分片下载成功->是?->合并分片下载的数据,返回下载文件,同时删除indexedDB库

前端代码片段(代码垃圾,大佬勿怪,有哪里需要改的帮忙指出下谢谢)



useIndexedDB.js

import {
	ref
} from 'vue';

export default function useIndexedDB() {
	const db = ref(null);
	const error = ref(null);


	// 初始化数据库
	const initializeDB = (databaseName, storeName, version) => {
		return new Promise((resolve, reject) => {
			let request = indexedDB.open(databaseName, version);

			request.onerror = (event) => {
				error.value = event.target.errorCode;
				reject(event.target.error);
			};

			request.onsuccess = (event) => {
				db.value = event.target.result;
				resolve(db.value);
			};

			request.onupgradeneeded = (event) => {
				let db = event.target.result;
				if (!db.objectStoreNames.contains(storeName)) {
					db.createObjectStore(storeName, {
						keyPath: 'id'
					});
				}
			};
		});
	};





	// 添加数据
	const addData = (objectStoreName, data) => {
		return new Promise((resolve, reject) => {
			let transaction = db.value.transaction([objectStoreName], 'readwrite');
			let objectStore = transaction.objectStore(objectStoreName);
			let request = objectStore.add(data);

			request.onsuccess = () => resolve(true);
			request.onerror = () => reject(false);
		});
	};

	// 查询数据
	const getData = (objectStoreName, key) => {
		return new Promise((resolve, reject) => {
			let transaction = db.value.transaction([objectStoreName], 'readonly');
			let objectStore = transaction.objectStore(objectStoreName);
			let request = objectStore.get(key);
			request.onsuccess = () => resolve(request.result);
			request.onerror = () => reject(null);
		});
	};

	// 查询数据
	const getAllData = (objectStoreName) => {
		return new Promise((resolve, reject) => {
			let transaction = db.value.transaction([objectStoreName], 'readonly');
			let objectStore = transaction.objectStore(objectStoreName);
			let request = objectStore.getAll();
			request.onsuccess = () => resolve(request.result);
			request.onerror = () => reject(null);
		});
	};

	// 删除数据
	const deleteData = (objectStoreName, key) => {
		return new Promise((resolve, reject) => {
			let transaction = db.value.transaction([objectStoreName], 'readwrite');
			let objectStore = transaction.objectStore(objectStoreName);
			let request = objectStore.delete(key);

			request.onsuccess = () => resolve(true);
			request.onerror = () => reject(false);
		});
	};

	// 删除数据库
	const deleteDatabase = (databaseName) => {
		const request = indexedDB.deleteDatabase(databaseName);
		db.value.close();
		request.onsuccess = (event) => {
			console.log(`Database ${databaseName} has been deleted successfully.`);
			db.value = null;
		};

		request.onerror = (event) => {
			console.error(`Error deleting database ${databaseName}:`, event.target.errorCode);
		};

		request.onblocked = (event) => {
			console.warn(`Delete operation is blocked for database ${databaseName}.`);
		};
	};

	// 清空整个 objectStore
	const clearObjectStore = (objectStoreName) => {
		return new Promise((resolve, reject) => {
			let transaction = db.value.transaction([objectStoreName], 'readwrite');
			let objectStore = transaction.objectStore(objectStoreName);
			let request = objectStore.clear();
			request.onsuccess = () => resolve(true);
			request.onerror = () => reject(false);
		});
	};



	// 请求额外的空间
	const requestAdditionalStorage = (quotaInBytes) => {
		return new Promise((resolve, reject) => {
			if (typeof navigator.webkitTemporaryStorage !== 'undefined') {
				// WebKit-based browsers (Chrome, Safari)
				navigator.webkitTemporaryStorage.requestQuota(quotaInBytes, function(grantedQuota) {
					console.log('Granted temporary storage quota:', grantedQuota);
					// 进行进一步的操作
				}, function(error) {
					console.error('Error requesting temporary storage quota:', error);
				});
			} else if (typeof navigator.webkitPersistentStorage !== 'undefined') {
				// WebKit-based browsers (Chrome, Safari)
				navigator.webkitPersistentStorage.requestQuota(quotaInBytes, function(grantedQuota) {
					console.log('Granted persistent storage quota:', grantedQuota);
					// 进行进一步的操作
				}, function(error) {
					console.error('Error requesting persistent storage quota:', error);
				});
			} else if (typeof indexedDB.requestQuota === 'function') {
				// Modern browsers that support IndexedDB 2.0
				indexedDB.requestQuota(quotaInBytes, function(grantedQuota) {
					console.log('Granted storage quota:', grantedQuota);
					// 进行进一步的操作
				}, function(error) {
					console.error('Error requesting storage quota:', error);
				});
			} else {
				console.warn('Storage quota request not supported.');
				// 在不支持的情况下采取备用方案
			}

		});
	};

	return {
		db,
		error,
		initializeDB,
		addData,
		getData,
		deleteData,
		clearObjectStore,
		requestAdditionalStorage,
		getAllData,
		deleteDatabase
	};
}

后端(服务端)大概流程:

获取到前端传递的参数->通过文件开始位置,结束位置,读取文件->流的形式返回前端.

后端代码片段(代码垃圾,大佬勿怪,有哪里需要改的帮忙指出下谢谢)

 public ResponseEntity  fileShardingDownload(FileDownloadRequest req) {
        // 根据文件路径获取文件
        File file = new File(req.getFilePath());
        if (!file.exists()) {
            throw new RuntimeException("文件丢失");
        }
        //获取文件偏移量
        long position = req.getStart();
        //获取文件读取大小
        long size= req.getEnd() - position;
        MappedByteBuffer mappedByteBuffer = null;
        try (FileChannel fileChannel = FileChannel.open(Paths.get(file.toURI()), StandardOpenOption.READ)) {

            // 映射文件到内存(从那个位置开始,读取多少数据.)
            mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, position, size);

            // 读取数据
            byte[] buffer = new byte[(int) size];
            // 将数据从 ByteBuffer 复制到 byte 数组
            mappedByteBuffer.get(buffer);

            // 转换为 InputStream
            InputStreamResource inputStreamResource = new InputStreamResource(new ByteArrayInputStream(buffer));

            return ResponseEntity.ok()
                    .contentType(MediaType.APPLICATION_OCTET_STREAM)
                    .contentLength(size)
                    .header("Content-Disposition", "attachment; filename=part-of-file.zip")
                    .body(inputStreamResource);

        } catch (IOException e) {
            log.error(e.getMessage(), e);
            throw new RuntimeException("文件下载失败!");
        } finally {
            //这是一个坑不关闭,会一直占用
            try {
                Method m = FileChannelImpl.class.getDeclaredMethod("unmap", MappedByteBuffer.class);
                m.setAccessible(true);
                m.invoke(FileChannelImpl.class, mappedByteBuffer);
            } catch (Exception e) {
                log.error(e.getMessage(), e);
            }
        }
    }
FileDownloadRequest类
@Data
public class FileDownloadRequest {

    /**
     * 文件名称
     */
    private String fileName;

    /**
     * 文件临时路径
     */

    private String filePath;

    /**
     * 文件大小
     */

    private String fileSize;


    /**
     * 当前分片开始位置
     */
    private Long start;

    /**
     * 当前分片结束位置
     */

    private Long end;

    /**
     * 当前分片位置
     */

    private Long offset;


}

总结

本文只提供分片下载断点续传思路,代码具体以项目逻辑为主.

你可能感兴趣的:(java,开发语言,javascript)