java实现文件断点续传、秒传

最近领导让做个文件断点续传的功能,解决某些用户因网络问题导致文件上传失败的问题。

之前就了解过只是一直没有真正使用,正好借这个机会,学习记录一下。

断点续传是什么?老生常谈,不了解的去百度一下,这里不做赘述。

实现思路:

前端实现:

  1. 前端获取文件,定义分片大小,利用file.slice(start,end);方法将文件分片,获取总分片数、当前分片序号、当前分片文件;
  2. 定义文件唯一标识,存入cookie中,当上传成功后再置为null;
  3. 将分片文件,分片序号,当前状态(这里定义start/run/end,三种状态)等参数通过FormData对象传给后端;
  4. 根据后端传回的分片序号,接着传输剩余分片文件,当分片序号==总分片数时,将状态(end)传给后端,通知后端合并分片文件。

后端实现:

  1. 后端接收数据后,先根据文件唯一标识和文件名进行MD5加密生成文件唯一标识key,再根据状态分别做处理;
  2. start状态:根据分片文件标识去缓存中先查找有无数据,存在则返回最大的分片序号给前端,让其接着上次断开的地方继续上传;没有则是第一次上传,将当前分片文件存入缓存;
    run状态:直接将分片文件存入缓存;
    end状态:将当前分片存入缓存,根据文件唯一标识key获取所有分片文件,利用RandomAccessFile合并分片文件,并清除缓存。
  3. 返回分片文件序号。

代码实现

前端代码

前端是利用XMLHttpRequest()进行文件传输,优点是可以异步传输文件。

var xhr = new XMLHttpRequest();
	var defaultChunkSize = (1024 * 1024);//确定分片大小
	var formData = new FormData(document.getElementById("id_uploadAttachmentFileForm")); //直接获取Form表单数据
	var fileSizeTotal = 0; // 定义上传文件大小
	var fileArr = [];	// 定义文件集合数组
	formData.forEach(item => {
		if(item instanceof File){
			fileArr.add(item);
			fileSizeTotal += item.size;
		}
	});
	// 分片上传
	upload(xhr, defaultChunkSize, 0, fileArr, 0, fileSizeTotal, fileNumber, 1);
/**
 * 分片上传
 * @param xhr
 * @param defaultChunkSize 默认分片大小
 * @param index 分片序列号
 * @param fileArr 上传文件集合
 * @param curFileSize 当前已上传文件大小
 * @param fileSizeTotal 上传文件总大小
 * @param number 上传文件序号
 */
function upload(xhr, defaultChunkSize, index, fileArr, curFileSize, fileSizeTotal, number) {
	let curFile = fileArr[number-1];
	let fileName = curFile.name;
	let fname = fileName.substring(0,fileName.lastIndexOf('.'));
	let fext = fileName.substring(fileName.lastIndexOf('.')+1);
	var indexSum = Math.ceil(curFile.size / defaultChunkSize); // 获取总切片数
	var star = index * defaultChunkSize;//切片的起点
	// 获取文件块的终止字节
	let end = (star + defaultChunkSize > curFile.size) ? curFile.size : (star + defaultChunkSize);
	//判断起点是否已经超过文件的长度,超过则返回
	if (star > curFile.size) {
		return;
	}
	var bool = curFile.slice(star, star + defaultChunkSize);//slice(分割起点,分割终点)是js切割文件的函数,
	var boolname = fname + index + fext
	var boolfile = new File([bool], boolname)//把分割后的快转成文件传输
	var formData = new FormData();
	// 判断传输的状态,传给后端
	if(index === indexSum-1 && number === fileArr.length){
		let fileNameList = [];
		for(let i=0; i < fileArr.length; i++){
			fileNameList.add(fileArr[i].name);
		}
		formData.append("fileNameList", fileNameList);
		formData.append("state","over");
	}else if(index > 0 && index < indexSum){
		formData.append("state","run");
	}else{
		formData.append("state","start");
	}
	// 设置一个文件上传唯一标识,存入cookie
	let cookie = getCookie("FILE_MARK_ID");
	if('null' === cookie){
		setCookie("FILE_MARK_ID",getUuid(32,16));
	}
	formData.append("FILE_MARK_ID",getCookie('FILE_MARK_ID'));	//文件标识
	formData.append("index", index);	//分片序号
	formData.append("defaultChunkSize", defaultChunkSize);	//分片大小
	formData.append("fileName", curFile.name);	//文件名称
	formData.append("file", boolfile);	//分片文件
	xhr.open("post", getRootPath() + '/xx/uploadFile', true); //发送请求
	xhr.send(formData);	//发送数据
	xhr.onloadend = function() {
		if (this.readyState === 4 && this.status === 200) {
			// 计算上传进度
			var loaded = Math.round((end + curFileSize) / fileSizeTotal * 100);
			var piece = Math.round((end + curFileSize) / 1024 / 1024) + 'M';
			var total = Math.round(fileSizeTotal / 1024 / 1024) + 'M';
			var size = piece + '/' + total;
			if(index < indexSum){
				if(this.responseText === (indexSum-1).toString()){
					if(number < fileArr.length){
						curFileSize += fileArr[number-1].size;
						number++;
						upload(xhr, defaultChunkSize, 0, fileArr, curFileSize, fileSizeTotal, number);
					}else{
						setCookie("FILE_MARK_ID",null);	// 上传完成设置文件标识为null
						// 设置进度条数值
						var fileTotal = Math.round(fileSizeTotal / 1024 / 1024) + 'M';
						$('#id_uploadAttachmentProcessFilePercent').progressbar('setValue', 100);
						$('#id_uploadAttachmentProcessFileSize').html(fileTotal + '/' + fileTotal);
						mini.get("id_uploadAttachmentProcessFileWindow").hide();
						mini.get(window.currentUploadAttachmentFileTableId).reload();
						mini.alert("全部文件上传成功");
					}
				}else{
					// 设置进度条数值
					$('#id_uploadAttachmentProcessFilePercent').progressbar('setValue', loaded);
					$('#id_uploadAttachmentProcessFileSize').html(size);
					index = parseInt(this.responseText);
					upload(xhr, defaultChunkSize, ++index, fileArr, curFileSize, fileSizeTotal, number);
				}
			}
		}
	};
	// 网络异常回调
	xhr.onerror = function () {
		document.getElementById("id_confirmwindow").style.display = "inline";
	}
	xhr.timeout = 5000;
	// 超时回调
	xhr.ontimeout = function () {
		document.getElementById("id_confirmwindow").style.display = "inline";
	}
}

上述代码实现多文件按顺序一起上传,其中还有很多需要优化的点。比如当前实现是针对一次上传的多文件生成的唯一标识,而不是单独的一个文件一个标识;文件上传进度条需要优化(因为最后需要合并分片文件及业务处理导致最后加载时间略长、断点续传时没有记录当前上传文件大小,进度条还是从0开始)

后端代码

缓存推荐使用Redis,方便高效;因为项目太老,我这边利用ConcurrentMap实现的本地缓存;如何实现参考 java实现本地缓存方案

// 利用传入的文件标识作为外层文件夹
File file = new File("D:/temp/"+FILE_MARK_ID+File.separator+file.getName());
// 获取文件的MD5加密key
String md5FileId = MD5Util.getMD5EncodedPassword(FILE_MARK_ID + file.getName());
// 从缓存中读取文件
LinkedList<HashMap<String,Object>> fileData = FileDataCacheUtil.getFileData(md5FileId);
if(CollectionUtils.isNotEmpty(fileData)){
	// 利用RandomAccessFile类进行分片文件合并
	RandomAccessFile raf = new RandomAccessFile(file,"rw");
	for (HashMap<String,Object> map : fileData) {
		Set<Map.Entry<String, Object>> entries = map.entrySet();
		Iterator<Map.Entry<String, Object>> iterator = entries.iterator();
		String keyIndex = "";
		MultipartFile fileValue = null;
		while (iterator.hasNext()){
			Map.Entry<String, Object> entry = iterator.next();
			keyIndex = entry.getKey();
			fileValue = (MultipartFile) entry.getValue();
		}
		raf.seek(Integer.parseInt(keyIndex) * fileDto.getDefaultChunkSize());//seek(int n)从n处处理
		byte[] bytes = fileValue.getBytes();//提区文件的字节流
		raf.write(bytes);//上面的seek方法已经把指针放在这里从这直接写入
	}
	// 关闭流
	raf.close();
	// 移除缓存
	FileDataCacheUtil.removeFileData(md5FileNameId);
}

后端的逻辑比较简单,这里只贴出核心代码,这里要求分片文件要严格按顺序传输

秒传功能的实现也很简单,根据上传的文件名称去数据库中查找,有则直接将文件地址返回前端。

网上也有很多实现,例如文件唯一性标识是在前端对整个二进制文件进行MD5加密,这种方式感觉更好,但是如果是大文件则有待商榷;

前端分片文件传输采用Web Worker多线程传输等等

你可能感兴趣的:(前端,Java学习,java,前端,断点续传,秒传)