欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2024-02-19 17:54:40
...

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

背景介绍
这个项目是在朋友的一次面试中,面试人提出了一个问题.
我有一个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
版权声明:本文为博主原创文章,转载请附上博文链接!