当前位置: 首页 > 科技观察

上传大文件时如何实现即时传输?

时间:2023-03-12 00:41:33 科技观察

文件上传是一个常见的话题。当文件比较小时,可以直接将文件转成字节流上传到服务器。但是,如果文件比较大,请按正常方式上传。这不是一个好办法,毕竟很少有人能忍受,当文件上传中途中断时,继续上传却又要从头开始上传是一种很不爽的体验。有没有更好的上传体验?答案是肯定的,就是下面要介绍的几种上传方式:二次传输1.什么是二次传输通俗地说,当你上传要上传的东西时,服务器会先做MD5校对测试,如果有相同的服务器上的东西,它会直接给你一个新地址。其实你下载的是服务器上的同一个文件。如果想秒传,其实只要改一下MD5,就是对文件本身做一些修改(改名字不行),比如文本文件,如果多加几个字,MD5会变,不会秒传。2、本文实现的二次传输的核心逻辑使用redis的set方法存储文件上传状态,其中key为文件上传的md5,value为上传是否完成的flag。b.当标志位为真时,上传完成。这时候如果有相同的文件上传,就会进入第二次传输逻辑。如果标志位为false,表示上传还没有完成。这时需要调用set方法保存块号文件记录路径,其中key为上传文件的md5加上固定前缀,value为块号文件记录路径。分片上传1.什么是分片上传?分块上传是指将要上传的文件按照一定的大小分成多个数据块(我们称之为Part),分别上传。上传后然后服务器将所有上传的文件聚合整合成原始文件。2、分片上传场景下,上传大文件的网络环境不好,需要断点续传的场景存在重传风险。1、什么是复工?任务(一个文件或一个压缩包)被人为的分成了几个部分,每个部分使用一个线程进行上传或下载。如果遇到网络故障,可以从已经上传或下载的部分继续上传或下载未完成的部分。部分而无需从头开始上传或下载。本文断点续传主要针对断点续传场景。2.应用场景断点续传可以看作是分段上传的衍生,所以断点续传可以用在可以分段上传的场景中。3、实现断点续传的核心逻辑在分片上传过程中,如果由于系统崩溃或网络中断等异常因素导致上传中断,客户端需要记录上传进度。稍后支持再次上传时,您可以从上次上传中断的地方继续上传。为了避免客户端上传后的进度数据被删除,重新从头开始上传的问题,服务端也可以提供相应的接口,方便客户端查询上传的分片数据,让客户端知道上传的数据。分片数据,以便从下一个分片数据继续上传。4、实现流程步骤a,方案1,常规步骤是将待上传的文件按照一定的切分规则分成大小相同的数据块;初始化一个分片上传任务,返回本次分片上传的唯一标识;按照一定的策略(串行或并行)发送每个分片数据块;发送完成后,服务器判断上传的数据是否完整,如果完整则合成数据块得到原始文件。b方案二,本文实现的步骤前端(client)需要按照固定大小对文件进行分片,请求后端(server)时,必须带上分片号和大小。服务端创建一个conf文件记录分块位置,conf文件的长度为分片总数,每上传一个分片就往conf文件中写入一个127,则未上传的位置为默认0,上传的是Byte.MAX_VALUE127(这一步是实现断点续传和二次传的核心步骤)服务端根据请求数据中给出的段号和大小计算起始位置每个段(段大小固定且相同),并将读取的文件段数据写入文档。5、分片上传/断点上传代码实现a前端使用百度提供的webuploader插件进行分片。因为本文主要介绍服务端代码实现,webuploader如何分片,具体实现可以查看以下链接:http://fex.baidu.com/webuploader/getting-started.htmlb后端实现文件写入在两种方式,一种是使用RandomAccessFile,如果对RandomAccessFile不熟悉,可以查看以下链接:https://blog.csdn.net/dimudan2015/article/details/81910690另一种是使用MappedByteBuffer,有需要的朋友对MappedByteBuffer不熟悉的,可以查看以下链接了解:https://www.jianshu.com/p/f90866dcbffc后端写操作的核心代码1.RandomAccessFile实现@UploadMode(mode=UploadModeEnum.RANDOM_ACCESS)@Slf4jpublicclassRandomAccessUploadStrategyextendsSliceUploadTemplate{@AutowiredprivateFilePathUtilfilePathUtil;@Value("${upload.chunkSize}")privatelongdefaultChunkSize;@Overridepublicbooleanupload(FileUploadRequestDTOparam){RandomAccessFileaccessTmpFile=null;try{StringuploadDirPath=filePathUtil.getPath(parFiletmpFile=super.createTmpFile(param);accessTmpFile=newRandomAccessFile(tmpFile,"rw");//这个必须和前端设置的值一致longchunkSize=Objects.isNull(param.getChunkSize())?defaultChunkSize*1024*1024:param.getChunkSize();longoffset=chunkSize*param.getChunk();//定位片段的偏移量accessTmpFile.seek(offset);//写入分片数据accessTmpFile.write(param.getFile().getBytes());booleanisOk=super.checkAndSetUploadProgress(param,uploadDirPath);返回正常;}catch(IOExceptione){log.error(e.getMessage(),e);}最后{FileUtil.close(accessTmpFile);}返回假;}}2。MappedByteBuffer实现@UploadMode(mode=UploadModeEnum.MAPPED_BYTEBUFFER)@Slf4jpublicclassMappedByteBufferUploadStrategyextendsSliceUploadwitemplate{FilePathUtilfilePathUtil;@Value("${upload.chunkSize}")privatelongdefaultChunkSize;@Overridepublicbooleanupload(FileUploadRequestDTOparam){RandomAccessFiletempRaf=null;文件通道fileChannel=null;MappedByteBuffermappedByteBuffer=null;尝试{StringuploadDirPath=filePathUtil.getPath(param);文件tmpFile=super.createTmpFile(param);tempRaf=newRandomAccessFile(tmpFile,"rw");fileChannel=tempRaf.getChannel();longchunkSize=Objects.isNull(param.getChunkSize())?defaultChunkSize*1024*1024:param.getChunkSize();//写入该切片数据longoffset=chunkSize*param.getChunk();byte[]fileData=param.getFile().getBytes();mappedByteBuffer=fileChannel.map(FileChannel.MapMode.READ_WRITE,offset,fileData.length);mappedByteBuffer.put(文件数据);booleanisOk=super.checkAndSetUploadProgress(param,uploadDirPath);返回正常;}catch(IOExceptione){log.error(e.getMessage(),e);}最后{FileUtil.freedMappedByteBuffer(mappedByteBuffer);FileUtil.close(文件通道);FileUtil.close(tempRaf);}返回假;}}3.文件操作内核模板类代码@Slf4jpublicabstractclassSliceUploadTemplateimplementsSliceUploadStrategy{publicabstractbooleanupload(FileUploadRequestDTOparam);protectedFilecreateTmpFile(FileUploadRequestDTOparam){FilePathUtilfilePathUtil=SpringContextHolder.getBean(FilePathUtil.class);param.setPath(FileUtil.withoutHeadAndTailDiagonal(param.getPath()));StringfileName=param.getFile().getOriginalFilename();StringuploadDirPath=filePathUtil.getPath(param);字符串tempFileName=文件名+"_tmp";文件tmpDir=新文件(uploadDirPath);文件tmpFile=newFile(uploadDirPath,tempFileName);如果(!tmpDir.exists()){tmpDir.mkdirs();}返回tmpFile;}@OverridepublicFileUploadDTOsliceUpload(FileUploadRequestDTOparam){booleanisOk=this.upload(param);if(isOk){文件tmpFile=this.createTmp文件(参数);FileUploadDTOfileUploadDTO=this.saveAndFileUploadDTO(param.getFile().getOriginalFilename(),tmpFile);返回文件上传DTO;}Stringmd5=FileMD5Util.getFileMD5(param.getFile());Mapmap=newHashMap<>();map.put(param.getChunk(),md5);返回FileUploadDTO.builder().chunkMd5Info(map).build();}/***检查和修改文件上传进度*/publicbooleancheckAndSetUploadProgress(FileUploadRequestDTOparam,StringuploadDirPath){StringfileName=param.getFile().getOriginalFilename();文件confFile=newFile(uploadDirPath,fileName+".conf");字节完成=0;RandomAccessFileaccessConfFile=null;尝试{accessConfFile=newRandomAccessFile(confFile,"rw");//将此段标记为true表示完成System.out.println("setpart"+param.getChunk()+"complete");//创建conf文件的长度为分片总数。每次上传片段时,都会将127写入conf文件。未上传的位置默认为0,已上传。传递的是Byte.MAX_VALUE127accessConfFile.setLength(param.getChunks());accessConfFile.seek(param.getChunk());accessConfFile.write(Byte.MAX_VALUE);都是127(所有部分都上传成功)byte[]completeList=FileUtils.readFileToByteArray(confFile);isComplete=Byte.MAX_VALUE;for(inti=0;i