本文介绍了“如何使用上传大文件的方法:二次传输、断点续传、片段上传”的相关知识。很多人在实际案例的操作中会遇到这样的困难。让边肖带领你学习如何处理这些情况。希望大家认真阅读,学点东西!
秒传
1、什么是秒传
一般来说,当你上传要上传的东西时,服务器会先做MD5验证。如果服务器上有同样的东西,它会直接给你一个新地址。事实上,你下载的都是服务器上的同一个文件。如果想秒传,其实只要改MD5就行了,只需要修改文件本身(不能改名字)。例如,如果您在文本文件中添加几个单词,MD5将会改变,并且不会在几秒钟内通过。
2、本文实现的秒传核心逻辑
A.使用redis的set方法存储文件上传状态,其中key为文件上传的md5,value为上传是否完成的标志位。
b、当标志位为真表示上传已经完成,如果此时上传的是同一文件,则进入第二传输逻辑。如果标志位为假,则表示上传尚未完成。此时需要调用set方法保存块号文件记录的路径,key为上传的文件md5添加固定前缀,value为块号文件记录的路径。
分片上传
1.什么是分片上传
分段上传是指将要上传的文件按照一定的大小划分成多个数据块(我们称之为Part)单独上传。上传后,服务器会将所有上传的文件汇总整合成原始文件。
2.分片上传的场景
1.上传大文件。
2.网络环境不好,有需要重传风险的场景。
超文本传送协议(Hyper Text Transport Protocol的缩写)
1、什么是断点续传
断点续传是在下载或上传时人为地将下载或上传任务(一个文件或一个压缩包)分成几个部分,每个部分由一个线程上传或下载。如果出现网络故障,您可以从已经上传或下载的部分继续上传或下载未完成的部分,而不必从头开始上传或下载。本文的断点续传主要针对断点上传场景。
2、应用场景
断点续传可以看作是片段上传的衍生,所以可以使用片段上传场景,也可以使用断点续传。
3、实现断点续传的核心逻辑
在片段上传过程中,如果因为系统崩溃或网络中断等异常因素导致上传中断,客户端需要记录上传进度。以后支持再次上传时,可以从上次上传中断的地方继续上传。
为了避免上传后删除客户端的进度数据而导致从头开始重新上传的问题,服务器还可以提供相应的接口,方便客户端查询上传的分片数据,让客户端知道上传的分片数据,从下一个分片数据继续上传。
4、实现流程步骤
一、方案一、常规步骤。
将待上传的文件按照一定的划分规则划分为大小相同的数据块;
初始化一个片段上传任务,并返回该片段上传的唯一标识符;
按照一定的策略(串行或并行)发送每个分片的数据块;
发送后,服务器判断数据上传是否完成,如果完成,则合成数据块,得到原始文件。
b、方案二、本文实现的步骤。
前端(客户端)需要按照固定的大小对文件进行切片,后端(服务器)应该用切片序列号和大小进行请求。
在服务器上创建一个conf文件来记录块的位置。conf文件的长度是块的总数。上传的每个块都用127写入,因此未上传的位置默认为0,上传的位置为字节。MAX_VALUE 127(这一步是实现断点续传和二次传输的核心步骤)。
服务器根据请求数据中给定的片段号和每个片段的块大小(片段大小固定且相同)计算起始位置,并将其与读取的文件片段数据一起写入文件。
5、分片上传/断点上传代码实现
A.前端使用百度提供的网络上传器插件进行切片。本文主要介绍了服务器代码的实现,以及如何对网络上传器进行分片。具体实现可以在下面的链接:中找到。
http://fex.baidu.com/webuploader/getting-started.html
b、后端使用两种方式写文件,一种是使用RandomAccessFile,如果不熟悉RandomAccessFile,可以查看下面的链接:
https://blog.csdn.net/dimudan2015/article/details/81910690
另一种是使用MappedByteBuffer。不熟悉MappedByteBuffer的朋友可以查看以下链接了解:
>
https://www.jianshu.com/p/f90866dcbffc
后端进行写入操作的核心代码
a、RandomAccessFile实现方式
@UploadMode(mode = UploadModeEnum.RANDOM_ACCESS) @Slf4j public class RandomAccessUploadStrategy extends SliceUploadTemplate { @Autowired private FilePathUtil filePathUtil; @Value("${upload.chunkSize}") private long defaultChunkSize; @Override public boolean upload(FileUploadRequestDTO param) { RandomAccessFile accessTmpFile = null; try { String uploadDirPath = filePathUtil.getPath(param); File tmpFile = super.createTmpFile(param); accessTmpFile = new RandomAccessFile(tmpFile, "rw"); //这个必须与前端设定的值一致 long chunkSize = Objects.isNull(param.getChunkSize()) ? defaultChunkSize * 1024 * 1024 : param.getChunkSize(); long offset = chunkSize * param.getChunk(); //定位到该分片的偏移量 accessTmpFile.seek(offset); //写入该分片数据 accessTmpFile.write(param.getFile().getBytes()); boolean isOk = super.checkAndSetUploadProgress(param, uploadDirPath); return isOk; } catch (IOException e) { log.error(e.getMessage(), e); } finally { FileUtil.close(accessTmpFile); } return false; } }
b、MappedByteBuffer实现方式
@UploadMode(mode = UploadModeEnum.MAPPED_BYTEBUFFER) @Slf4j public class MappedByteBufferUploadStrategy extends SliceUploadTemplate { @Autowired private FilePathUtil filePathUtil; @Value("${upload.chunkSize}") private long defaultChunkSize; @Override public boolean upload(FileUploadRequestDTO param) { RandomAccessFile tempRaf = null; FileChannel fileChannel = null; MappedByteBuffer mappedByteBuffer = null; try { String uploadDirPath = filePathUtil.getPath(param); File tmpFile = super.createTmpFile(param); tempRaf = new RandomAccessFile(tmpFile, "rw"); fileChannel = tempRaf.getChannel(); long chunkSize = Objects.isNull(param.getChunkSize()) ? defaultChunkSize * 1024 * 1024 : param.getChunkSize(); //写入该分片数据 long offset = chunkSize * param.getChunk(); byte[] fileData = param.getFile().getBytes(); mappedByteBuffer = fileChannel .map(FileChannel.MapMode.READ_WRITE, offset, fileData.length); mappedByteBuffer.put(fileData); boolean isOk = super.checkAndSetUploadProgress(param, uploadDirPath); return isOk; } catch (IOException e) { log.error(e.getMessage(), e); } finally { FileUtil.freedMappedByteBuffer(mappedByteBuffer); FileUtil.close(fileChannel); FileUtil.close(tempRaf); } return false; } }
c、文件操作核心模板类代码
@Slf4j public abstract class SliceUploadTemplate implements SliceUploadStrategy { public abstract boolean upload(FileUploadRequestDTO param); protected File createTmpFile(FileUploadRequestDTO param) { FilePathUtil filePathUtil = SpringContextHolder.getBean(FilePathUtil.class); param.setPath(FileUtil.withoutHeadAndTailDiagonal(param.getPath())); String fileName = param.getFile().getOriginalFilename(); String uploadDirPath = filePathUtil.getPath(param); String tempFileName = fileName + "_tmp"; File tmpDir = new File(uploadDirPath); File tmpFile = new File(uploadDirPath, tempFileName); if (!tmpDir.exists()) { tmpDir.mkdirs(); } return tmpFile; } @Override public FileUploadDTO sliceUpload(FileUploadRequestDTO param) { boolean isOk = this.upload(param); if (isOk) { File tmpFile = this.createTmpFile(param); FileUploadDTO fileUploadDTO = this.saveAndFileUploadDTO(param.getFile().getOriginalFilename(), tmpFile); return fileUploadDTO; } String md5 = FileMD5Util.getFileMD5(param.getFile()); Map<Integer, String> map = new HashMap<>(); map.put(param.getChunk(), md5); return FileUploadDTO.builder().chunkMd5Info(map).build(); } /** * 检查并修改文件上传进度 */ public boolean checkAndSetUploadProgress(FileUploadRequestDTO param, String uploadDirPath) { String fileName = param.getFile().getOriginalFilename(); File confFile = new File(uploadDirPath, fileName + ".conf"); byte isComplete = 0; RandomAccessFile accessConfFile = null; try { accessConfFile = new RandomAccessFile(confFile, "rw"); //把该分段标记为 true 表示完成 System.out.println("set part " + param.getChunk() + " complete"); //创建conf文件文件长度为总分片数,每上传一个分块即向conf文件中写入一个127,那么没上传的位置就是默认0,已上传的就是Byte.MAX_VALUE 127 accessConfFile.setLength(param.getChunks()); accessConfFile.seek(param.getChunk()); accessConfFile.write(Byte.MAX_VALUE); //completeList 检查是否全部完成,如果数组里是否全部都是127(全部分片都成功上传) byte[] completeList = FileUtils.readFileToByteArray(confFile); isComplete = Byte.MAX_VALUE; for (int i = 0; i < completeList.length && isComplete == Byte.MAX_VALUE; i++) { //与运算, 如果有部分没有完成则 isComplete 不是 Byte.MAX_VALUE isComplete = (byte) (isComplete & completeList[i]); System.out.println("check part " + i + " complete?:" + completeList[i]); } } catch (IOException e) { log.error(e.getMessage(), e); } finally { FileUtil.close(accessConfFile); } boolean isOk = setUploadProgress2Redis(param, uploadDirPath, fileName, confFile, isComplete); return isOk; } /** * 把上传进度信息存进redis */ private boolean setUploadProgress2Redis(FileUploadRequestDTO param, String uploadDirPath, String fileName, File confFile, byte isComplete) { RedisUtil redisUtil = SpringContextHolder.getBean(RedisUtil.class); if (isComplete == Byte.MAX_VALUE) { redisUtil.hset(FileConstant.FILE_UPLOAD_STATUS, param.getMd5(), "true"); redisUtil.del(FileConstant.FILE_MD5_KEY + param.getMd5()); confFile.delete(); return true; } else { if (!redisUtil.hHasKey(FileConstant.FILE_UPLOAD_STATUS, param.getMd5())) { redisUtil.hset(FileConstant.FILE_UPLOAD_STATUS, param.getMd5(), "false"); redisUtil.set(FileConstant.FILE_MD5_KEY + param.getMd5(), uploadDirPath + FileConstant.FILE_SEPARATORCHAR + fileName + ".conf"); } return false; } } /** * 保存文件操作 */ public FileUploadDTO saveAndFileUploadDTO(String fileName, File tmpFile) { FileUploadDTO fileUploadDTO = null; try { fileUploadDTO = renameFile(tmpFile, fileName); if (fileUploadDTO.isUploadComplete()) { System.out .println("upload complete !!" + fileUploadDTO.isUploadComplete() + " name=" + fileName); //TODO 保存文件信息到数据库 } } catch (Exception e) { log.error(e.getMessage(), e); } finally { } return fileUploadDTO; } /** * 文件重命名 * * @param toBeRenamed 将要修改名字的文件 * @param toFileNewName 新的名字 */ private FileUploadDTO renameFile(File toBeRenamed, String toFileNewName) { //检查要重命名的文件是否存在,是否是文件 FileUploadDTO fileUploadDTO = new FileUploadDTO(); if (!toBeRenamed.exists() || toBeRenamed.isDirectory()) { log.info("File does not exist: {}", toBeRenamed.getName()); fileUploadDTO.setUploadComplete(false); return fileUploadDTO; } String ext = FileUtil.getExtension(toFileNewName); String p = toBeRenamed.getParent(); String filePath = p + FileConstant.FILE_SEPARATORCHAR + toFileNewName; File newnewFile = new File(filePath); //修改文件名 boolean uploadFlag = toBeRenamed.renameTo(newFile); fileUploadDTO.setMtime(DateUtil.getCurrentTimeStamp()); fileUploadDTO.setUploadComplete(uploadFlag); fileUploadDTO.setPath(filePath); fileUploadDTO.setSize(newFile.length()); fileUploadDTO.setFileExt(ext); fileUploadDTO.setFileId(toFileNewName); return fileUploadDTO; } }
“如何使用大文件上传:秒传、断点续传、分片上传方法”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注网站,小编将为大家输出更多高质量的实用文章!
内容来源网络,如有侵权,联系删除,本文地址:https://www.230890.com/zhan/37470.html