口水话介绍
对于上传和下载文件,虽然http没有限制文件的大小,但是受制于环境影响因素可能造成上传或者下载中断,为了避免重新下载或者上传所带来的用户砸电脑行为的发生,一般我们会对大文件的上传和下载才去断点续传的方案
上传流程:
-
上传前先将文件分成块
-
一块一块的上传,上传中断后重新上传,已上传的分块则不用再次上传
-
各分块全部上传完成,最后合并文件
下载流程同理
Java代码演示文件的分块与合并
-
文件的分块:大致思路
-
获取源文件的大小
-
设定一个单个分块的大小
-
从源文件中读取数据依次向分块中写入数据
-
@Test public void ChunkTest() throws IOException { File sourceFile = new File("F:\mySpace\myavi\merge.mp4"); String chunkPath = "F:\mySpace\myavi\chunk\"; File chunkFolder = new File(chunkPath); if(!chunkFolder.exists()){ chunkFolder.mkdirs(); } //分块大小1兆 long chunkSize = 1024*1024*1; //分块数量:文件大小 / 单个分块的大小,我们设置的1兆,对其向上取整 long chunkNum = (long) Math.ceil(sourceFile.length() * 1.0 / chunkSize ); if(chunkNum<=0){ chunkNum = 1; } //缓冲区大小 byte[] b = new byte[1024]; //使用RandomAccessFile访问文件 RandomAccessFile raf_read = new RandomAccessFile(sourceFile, "r"); //分块 for(int i=0;i<chunkNum;i++){ //创建分块文件 File file = new File(chunkPath+i); boolean newFile = file.createNewFile(); if(newFile){ //向分块文件中写数据 RandomAccessFile raf_write = new RandomAccessFile(file, "rw"); int len = -1; while((len = raf_read.read(b))!=-1){ raf_write.write(b,0,len); if(file.length()>chunkSize){ break; } } raf_write.close(); } } raf_read.close(); }
-
文件的合并:大致思路
-
这里需要注意的就是:分块与合并的顺序必须一致
-
@Test public void mergeTest() { try { //待合并的块文件目录,其下的所有文件采集 File chunkFoler = new File("F:\mySpace\myavi\chunk\"); File[] chunkFileArray = chunkFoler.listFiles(); //合并结果文件的创建 File mergeFile = new File("F:\mySpace\myavi\chunk\merge.mp4"); if (mergeFile.exists()){ mergeFile.delete(); } mergeFile.createNewFile(); //创建读写文件对象 RandomAccessFile write = new RandomAccessFile(mergeFile, "rw"); //指针指向文件顶端 write.seek(0); //创建缓冲区 byte[] b = new byte[1024]; //因为我们分块的时候是按照1 2 3 4 这样的顺序分的,所以合并的时候也要按照这个顺序来 //但是我们的listFiles()得到的所有的文件是没有顺序的,是乱的,所以需要排序 List<File> fileList = Arrays.asList(chunkFileArray); Collections.sort(fileList, new Comparator<File>() { @Override public int compare(File o1, File o2) { if (Integer.parseInt(o1.getName()) < Integer.parseInt(o2.getName())){ return -1; } return 1; } }); for (File file : fileList) { int len = -1; RandomAccessFile read = new RandomAccessFile(file, "rw"); while ((len = read.read(b)) != -1){ write.write(b,0,len); } } }catch (Exception e){ e.printStackTrace(); } }
Web Uploader + 后端思路
这次我们使用Html5来演示分块上传,Web Uploader完成大文件上传功能演示
官网:http://fexteam.gz01.bdysite.com/webuploader/
-
上传流程:
勾子方法
webupload中提供了很多的勾子方法,比如:
before-send-file
在开始对文件分块儿之前调用,可以做一些上传文件前的准备工作,比如检查文件目录是否创建完成等。
before-send
在上传文件分块之前调用此方法,可以请求服务端检查分块是否存在,如果已存在则此分块儿不再上传。
after-send-file
在所有分块上传完成后触发,可以请求服务端合并分块文件。
后端思路
业务层代码我就不上了,我就写个思路:
首先我们理一下思路,Webuploader在上传过程会调用四个API接口,他们分别是:
文件上传前的注册
分块文件上传前的分块检查
分块文件的上传
上传完成,通知合并,以及后续保存数据到数据、删除分块数据等...
解释一下:
我们的文件储存方式的路径分三级目录的,根据文件MD5值确定他的资源存放路径
比如MD5值为:c5b1545123343
那么该资源的第一级目录为Md5的第一个字符:c
第二级目录为Md5的第二个字符:5
第三级目录为MD5的值:c5b1545123343
-
第一个模块:文件上传前的注册和相关资源文件目录的初始化
根据MD5的值去查询该路径下是否有资源
根据MD5去数据查询是否有资源数据
在都没有的情况下,创建我们的资源三级目录
-
第二个模块:分块上传前检查分块文件是否存在
根据MD5得到相应的资源存放路径
三级目录 + 待检查的分块id信息,得到具体的分块文件数据
这里我们分块都是以 1、2、3这样的方式上传的的,核实待上传分块是否已经存在
-
第三个模块:分块文件上传
根据MD5得到三级目录,根据分块数据id得到文件名称,确定资源的绝对路径
然后就是流的:IOUtils.copy(inputStream, fileOutputStream); 写入数据
-
第四个模块:分块数据的合并已经后续其他操作
根据MD5得带确定的资源存放路径,获取该路径下的所有分块数据:File[ ]
获取到的所有分块并不是按照我们想要的顺序排列的,合成会失败
转成集合,根据名字(1、2、3...),将文件排序
创建合成后的文件的路径以及名称
然后就是文件的合并,可以参考上面我写的Demo代码
合并后我们会对这个合并文件的MD5和参数MD5再做一个校验:
if (fileMd5.equals(DigestUtils.md5Hex(new FileInputStream(mergeFilePath))))