Webupload+nio实现大文件分片、断点续传

目录
背景介绍
项目介绍
使用说明
获取代码
需要知识点
启动项目
项目示范
核心讲解
功能分析
分块上传
秒传功能
断点续传
总结

背景介绍
这个项目是在朋友的一次面试中,面试人提出了一个问题.
我有一个100M的文件,然后我的宽带只有10M,我应该如何处理用户上传的文件?
根据这个问题,我小试牛刀,写了这个项目.

期间查阅了资料,借鉴了Fourwen的项目的前端框架和md写法.

再次感谢.

项目介绍
项目采用如下:

上层: Java, JDK8, Tomcat8,
服务端: Jsp, 原生
前端: webuploader, bootstrap, jquery
来进行开发,

针对文件的上传,一般可以考虑的功能点有

断点续传 在断网或者在暂停的情况下,能够在上传断点中继续上传。

分块上传 也是断点续传的基础之一,把大文件通过前端分块,然后后台在组在一起。

文件秒传 服务中已经有人上传过文件,其他人再上传这个文件直接记录并放回成功。

其他功能 下面这些功能归类到其他,是因为它们基本都是通过WebUploader来实现的,很简单。

  • 多线程上传 多个线程上传不同的块文件。
  • 文件进度显示 显示文件的上传完成情况。
    1
    2

使用说明

获取代码
GitHub:https://github.com/ck-wizard/BigFileUpload
不会经常更新,下一步会做一个集合公司内部网址的项目.

需要知识点
项目使用nio来进行文件的读取和创建
使用原生web来开发,不使用任何框架
使用Apache提供的fileupload来实现上传数据的获取
使用Apache提供的codec来实现md5加密
并发的理解

启动项目

项目示范

功能分析
分块上传可以说是我们整个项目的基础,像断点续传、暂停这些都是需要用到分块。
分块这块相对来说比较简单。前端是采用了webuploader,分块等基础功能已经封装起来,使用方便。
借助webUpload提供给我们的文件API,前端就显得异常简单。

var uploader = WebUploader.create({

    // swf文件路径
    swf: '${ctx}/webuploader-0.1.5/Uploader.swf',

    // 文件接收服务端。
    server: '${ctx}/upload.do',
    //文件上传请求的参数表,每次发送都会发送此对象中的参数
    formData: {
        md5: ''
    },

    // 选择文件的按钮。可选。
    // 内部根据当前运行是创建,可能是input元素,也可能是flash.
    pick: '#picker',

    // 不压缩image, 默认如果是jpeg,文件上传前会压缩一把再上传!
    resize: false,

    chunked: true, // 分块
    chunkSize: 1 * 1024 * 1024, // 字节 1M分块
    threads: 3, //开启线程
    auto: false,

    // 禁掉全局的拖拽功能。这样不会出现图片拖进页面的时候,把图片打开。
    disableGlobalDnd: true,
    fileNumLimit: 1024,
    fileSizeLimit: 200 * 1024 * 1024,    // 200 M
    fileSingleSizeLimit: 100 * 1024 * 1024    // 100 M
});

上传的文件会被发送到upload.do这个Controller,在里面的逻辑如下:

判断是文件上传请求,如果是继续,否则退出
使用fileupload jar包解析request请求上传的基础信息
使用FileUploadBean包装上传的基础信息.
拼装父目录,校验是否存在
4.1 不存在就创建
4.2 存在就进入检验
4.2.1 检查md5值是否匹配, 应该建立数据库,存储文件信息才是更快 更好的解决办法.
4.2.2 若匹配直接返回成功.
4.2.3 若不成功,删除源文件再次读取

写入该分片数据到指定目录
写入规则如下:
// 0.读取上传文件到数组
// 1.写到本地
// 1.记录分片数,检查分片数
// 2.当对应的md5读取数量达到对应的文件后,合并文件
// 3.删除临时文件

完成
功能分析

分块上传
分块上传可以说是我们整个项目的基础,像断点续传、暂停这些都是需要用到分块。
分块这块相对来说比较简单。前端是采用了webuploader,分块等基础功能已经封装起来,使用方便。
借助webUpload提供给我们的文件API,前端就显得异常简单。

   var uploader = WebUploader.create({

        // swf文件路径
        swf: '${ctx}/webuploader-0.1.5/Uploader.swf',

        // 文件接收服务端。
        server: '${ctx}/upload.do',
        //文件上传请求的参数表,每次发送都会发送此对象中的参数
        formData: {
            md5: ''
        },

        // 选择文件的按钮。可选。
        // 内部根据当前运行是创建,可能是input元素,也可能是flash.
        pick: '#picker',

        // 不压缩image, 默认如果是jpeg,文件上传前会压缩一把再上传!
        resize: false,

        chunked: true, // 分块
        chunkSize: 1 * 1024 * 1024, // 字节 1M分块
        threads: 3, //开启线程
        auto: false,

        // 禁掉全局的拖拽功能。这样不会出现图片拖进页面的时候,把图片打开。
        disableGlobalDnd: true,
        fileNumLimit: 1024,
        fileSizeLimit: 200 * 1024 * 1024,    // 200 M
        fileSingleSizeLimit: 100 * 1024 * 1024    // 100 M
    });

服务器先创建一个md5文件夹,然后按照上传的文件名进行一套规范命名,写入到一个临时文件中.
然后记录这个临时文件.

// 规范命名
String fileName = param.getName();
String uploadDirPath = finalDirPath + param.getMd5();
String tempFileName = fileName + "_" + param.getChunk() + "_tmp";
Path tmpDir = Paths.get(uploadDirPath);
// 写入临时文件
Path path = Paths.get(uploadDirPath, tempFileName);
byte[] fileData = FileUtils.read(param.getFile(), 2048);
Files.write(path, fileData, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
FileUtils.authorizationAll(path);
// 记录
FileBean fileBean;
if(fileMap.containsKey(param.getMd5())) {
    fileBean = fileMap.get(param.getMd5());
} else {
    fileBean = new FileBean(param.getName(), param.getChunks(), param.getMd5());
    fileMap.put(param.getMd5(), fileBean);
}
fileBean.setChunk(param.getChunk());

然后当文件分片都上传完成后,在把分片合并为一个文件,并且删除所有临时文件.

if(fileBean.isLoadComplate()) {
// 合并文件..
Path realFile = Paths.get(uploadDirPath, fileBean.getName());
realFile = Files.createFile(realFile);
// 设置权限
FileUtils.authorizationAll(realFile);
for(int i = 0 ; i < fileBean.getChunks(); i++) {
    // 获取每个分片
    tempFileName = fileName + "_" + i + "_tmp";
    Path itemPath = Paths.get(uploadDirPath, tempFileName);
    byte[] bytes = Files.readAllBytes(itemPath);
    Files.write(realFile, bytes, StandardOpenOption.APPEND);
    //写完后删除掉临时文件.
    Files.delete(itemPath);
}
logger.info("合并文件{}成功", fileName);
}

秒传功能
上传文件是若发现父目录已经创建,并且目录下有上传的文件名,那么进行md5比较,若相同,直接返回,若不相同,删除目录文件,重新上传.

if (!Files.exists(tmpDir)) {
    Files.createDirectory(tmpDir);
} else {
    // 文件夹已存在
    // 1.检查是否有文件,有进入2, 没有进3
    Path localPath = Paths.get(uploadDirPath, fileName);

    // 2.检查md5值是否匹配, 应该建立数据库,存储文件信息才是更快 更好的解决办法.
    // 2.1.若匹配直接返回成功.
    // 2.2 若不成功,删除源文件再次读取
    if(Files.exists(localPath)) {
        String nowMd5 = DigestUtils.md5Hex(Files.newInputStream(localPath, StandardOpenOption.READ));
        if(StringUtils.equals(param.getMd5(), nowMd5)) {
            // 比较相等,那么直接返回成功.
            logger.info("已检测到重复文件{},并且比较md5相等,已直接返回", fileName);
            return;
        } else {
            // 删除
            logger.info("已经存在的文件的md5不匹配上传上来的文件的md5,删除后重新下载");
            Files.delete(localPath);
        }
    }
    // 3. 直接写入到具体目录下.
}

断点续传
断点续传,就是在文件上传的过程中发生了中断,人为因素(暂停)或者不可抗力(断网或者网络差)导致了文件上传到一半失败了。然后在环境恢复的时候,重新上传该文件,而不至于是从新开始上传的。

文件上传时,获取分片大小,同服务器目录存储的分片大小进行比较,若一直,直接返回成功.

//写入该分片数据
Path path = Paths.get(uploadDirPath, tempFileName);
//文件上传时,获取是否有分片,如果有直接返回.
if(!Files.exists(path)) {
    // 不存在
    byte[] fileData = FileUtils.read(param.getFile(), 2048);
    try {
        Files.write(path, fileData, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
    } catch (IOException e) {
        // 删除上传的文件
        Files.delete(path);
        throw e;
    }
    FileUtils.authorizationAll(path);
} else {
    return;
}

总结
选择使用原生是为了锻炼自己不要忘记基础,前前后后写了3天,复习了不少文件相关的操作,并且对lambda表达式和流
有了进一步了解,还是很满足的.

在并发的情况下进行文件上传,在使用一个实例的成员变量进行存储的时候,在方法上面使用synchronized或代码段加synchronized
或Lock或使用AtomInteger去进行并发操作,都没能达到正确统计的目的.最后使用ConcurrentHashMap才完成了正确的计数.

作者:ck_wizard
来源:CSDN
原文:https://blog.csdn.net/ckingwizard/article/details/79361715
版权声明:本文为博主原创文章,转载请附上博文链接!

你可能感兴趣的:(大文件上传,大文件上传)