• 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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    /**
     * 分片上传
     * @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";
    	}
    }
    
    • 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

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

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

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

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

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

  • 相关阅读:
    14天学习训练营导师课程-Pygame学习笔记-Part2(第九艺术的召唤)
    禁忌搜索算法在求解取送货路径问题中的应用
    elasticsearch 7.7.0 单节点配置x-pack
    C语言指针笔试题讲解
    毕业设计-基于机器视觉的口罩佩戴检测识别
    第二次授课内容
    MATLAB中split函数使用
    帆软FineReport本地部署springboot
    Minecraft 1.16.5 生化8 模组 1.9版本 1.18版本同步
    Day04NTFS安全权限、文件共享服务器、破解winxp系统密码
  • 原文地址:https://blog.csdn.net/weixin_43407520/article/details/126054351