0、写在前面的话

上篇博客已经是在8月了,期间到底发生了什么,只有我自己知道,反正就是心情特别糟糕,生活状态工作状态学习状态都十分不好,还有心思进取吗,No!现在状态好起来了,生活又充满了希望 :D 

前两周在写视频管理相关的功能,说是要在原来的项目上进行拓展。结果今天领导给我说客户那边还没定,只做技术上研究就行了,不用写具体功能代码(我都写了好吗?)于是突然时间有腾出来,今天整理一下把内容写一些。

要努力努力,为了更好的人为了更好的生活。

1、断点续传的两种方式

1.1 RandomAccessFile

客户端给一个已经上传的位置标记,然后服务器端就可以在指定的位置进行处理。这个断点位置的读取,就要用到RandomAccessFile类,该类不同于InputStream和OutputStream,它既可以对文件进行读也可以进行写,两个重要方法:
  • long getFilePoint():返回文件记录指针的当前位置,不指定指针的位置默认是0
  • void seek(long pos):设置文件指针偏移,即将文件记录指针定位到pos位置

至于position位置如何去处理,就看各自的想法了。1)你可以将位置存在浏览器(比如localStorage),下次传输的时候前端从残缺位置切割文件 blob.slice() 只传输剩余的部分,后端直接接收接着写入服务器即可;2)也可以前端把文件完整传输,同时带上position参数,由后端通过 RandomAccessFile 在指定位置开始读取内容即可。

至于客户端和服务端之间文件的一致性,多使用md5进行校验。

1.2 分片处理

H5中新增了File API,可以通过使用 slice() 方法生成只有某段文件内容。这个方法就为断点续传提供了新的方式,就是分片处理,假设一个文件是100M大小,那么每次传输我只需要传送10M,按序发送10次请求即可。某个分片传送失败,那么从这个分片再继续发送即可,后端则对分片文件进行合并成完整文件。

其实方式和1.1提到的是类似的,不过每次传输的数据单位量更大一些,完整文件交给后端进行合并。

2、WebUploader的分片断点续传

WebUploader的选项中支持直接开启分片上传:
var uploader = WebUploader.Uploader({
swf: 'path_of_swf/Uploader.swf', // 开起分片上传
chunked: true,
// 分片大小,默认5M
chunkSize: 5242880,
// 分片出错后(如网络原因等造成出错)的重传次数
chunkRetry: 2,
// 上传并发数
threads: 1
});
x
 
1
var uploader = WebUploader.Uploader({
2
    swf: 'path_of_swf/Uploader.swf',
3

4
    // 开起分片上传
5
    chunked: true,
6
    // 分片大小,默认5M
7
    chunkSize: 5242880,
8
    // 分片出错后(如网络原因等造成出错)的重传次数
9
    chunkRetry: 2,
10
    // 上传并发数
11
    threads: 1
12
});

开启分片上传后,插件会自动分片上传文件,接下来只需要在配置文件跳过和后端处理即可。官方回应在分片发送前会有监听的事件 uploadBeforeSend,在这个方法的callback里面如果返回的是一个promise,且此promise被reject了,那么此分片就跳过了。(实际上该方式在自测和咨询网友时发现,并没有什么用,即便按照官方说明,分片也没有跳过,仍然往后端进行了请求发送,同时也附带有文件
webUploader.on('uploadBeforeSend', function(block, data){
data.fileMd5 = block.file.wholeMd5;
var deferred = WebUploader.Deferred();
var chunk = data.chunk;
var existChunks = block.file.existChunks;
//后端返回了已存在分片的数组,这里判断要发送的分片是否已存在
if(existChunks && existChunks.indexOf(chunk) != -1) {
//console.log("分片存在,已跳过:" + chunk);
deferred.reject();
} else {
deferred.resolve();
}
return deferred.promise();
});
 
1
webUploader.on('uploadBeforeSend', function(block, data){
2
    data.fileMd5 = block.file.wholeMd5;
3
    var deferred = WebUploader.Deferred();
4
    var chunk = data.chunk;
5
    var existChunks = block.file.existChunks;
6
    //后端返回了已存在分片的数组,这里判断要发送的分片是否已存在
7
    if(existChunks && existChunks.indexOf(chunk) != -1) {
8
        //console.log("分片存在,已跳过:" + chunk);
9
        deferred.reject();
10
    } else {
11
        deferred.resolve();
12
    }
13
    return deferred.promise();
14
});

分片是否存在的判断,也有不同的方式,一种你可以每次计算分片的md5值发送给后端,如果服务器已存在则跳过,否则就发送;另一种就是只向服务器查询一次获取已经存在的分片,然后在浏览器端进行比对,但如此需要考虑分片是否并发传输,进行相应处理。

我采用的方式是:先对文件进行md5计算,在服务器端创建和md5值同名的文件夹,每次上传的分片存放在对应文件夹,文件名即分片的序号,比如某文件夹中可能存在文件 0, 1, 2, 3... 前端发送分片前请求后端数据,后端将已经存在的分片名数组返回前端,前端进行跳过处理,同时后端在接收分片也要做是否存在的判断,已存在的话就不再进行读写操作,直到最后分片到达,则进行分片的按序合并即可。

public boolean uploadChunk() throws ChunkUploadException {
HttpServletRequest request = ServletActionContext.getRequest();
//封装源文件信息
FileInfo srcFileInfo = VideoUtil.getUploadFileInfoByStruts(request, "file");
//获取同时上传的文件其他属性
Map<String, String> params = getVideoParams(request); if (params.get("fileMd5") == null || "".equals(params.get("fileMd5"))) {
throw new ChunkUploadException("文件md5值未传递");
}
//存放
File temp = new File(getTempPath(params.get("fileMd5")) + "/" + srcFileInfo.getCurChunk());
if (!temp.exists()) {
try {
VideoUtil.copy(srcFileInfo.getFile(), temp);
} catch (IOException e) {
throw new ChunkUploadException("分片上传失败: chunkNum" + params.get("chunk"));
}
}
//如果是最后分片
return !srcFileInfo.isChunked() || srcFileInfo.getCurChunk() == srcFileInfo.getChunkSize() - 1;
}
x
 
1
public boolean uploadChunk() throws ChunkUploadException {
2
    HttpServletRequest request = ServletActionContext.getRequest();
3
    //封装源文件信息
4
    FileInfo srcFileInfo = VideoUtil.getUploadFileInfoByStruts(request, "file");
5
    //获取同时上传的文件其他属性
6
    Map<String, String> params = getVideoParams(request);
7

8
    if (params.get("fileMd5") == null || "".equals(params.get("fileMd5"))) {
9
        throw new ChunkUploadException("文件md5值未传递");
10
    }
11
    //存放
12
    File temp = new File(getTempPath(params.get("fileMd5")) + "/" + srcFileInfo.getCurChunk());
13
    if (!temp.exists()) {
14
        try {
15
            VideoUtil.copy(srcFileInfo.getFile(), temp);
16
        } catch (IOException e) {
17
            throw new ChunkUploadException("分片上传失败: chunkNum" + params.get("chunk"));
18
        }
19
    }
20
    //如果是最后分片
21
    return !srcFileInfo.isChunked() || srcFileInfo.getCurChunk() == srcFileInfo.getChunkSize() - 1;
22
}

public String upload() {
boolean isLastChunk = false;
try {
isLastChunk = uploadChunk();
} catch (ChunkUploadException e) {
e.printStackTrace();
AjaxSupport.sendFailText(null, e.getMessage());
return AJAX_RESULT;
} //不是最后的分片,直接返回成功响应
if (!isLastChunk) {
AjaxSupport.sendSuccessText("chunk uploaded", "success");
return AJAX_RESULT;
}
//最后切片
else {
HttpServletRequest request = ServletActionContext.getRequest();
//封装源文件信息
FileInfo srcFileInfo = VideoUtil.getUploadFileInfoByStruts(request, "file");
//获取同时上传的文件其他属性
Map<String, String> params = getVideoParams(request);
//获取合并文件的文件名
String filename = UUID.randomUUID().toString() + "." + srcFileInfo.getFileType(); //合并文件
File tempDir = new File(getTempPath(params.get("fileMd5")));
File[] tempfileArr = tempDir.listFiles();
File storeFile = new File(getStorePath() + "/" + filename);
try {
VideoUtil.merge(tempfileArr, storeFile);
}
...
x
 
1
public String upload() {
2
    boolean isLastChunk = false;
3
    try {
4
        isLastChunk = uploadChunk();
5
    } catch (ChunkUploadException e) {
6
        e.printStackTrace();
7
        AjaxSupport.sendFailText(null, e.getMessage());
8
        return AJAX_RESULT;
9
    }
10

11
    //不是最后的分片,直接返回成功响应
12
    if (!isLastChunk) {
13
        AjaxSupport.sendSuccessText("chunk uploaded", "success");
14
        return AJAX_RESULT;
15
    } 
16
    //最后切片
17
    else {
18
        HttpServletRequest request = ServletActionContext.getRequest();
19
        //封装源文件信息
20
        FileInfo srcFileInfo = VideoUtil.getUploadFileInfoByStruts(request, "file");
21
        //获取同时上传的文件其他属性
22
        Map<String, String> params = getVideoParams(request);
23
        //获取合并文件的文件名
24
        String filename = UUID.randomUUID().toString() + "." + srcFileInfo.getFileType();
25

26
        //合并文件
27
        File tempDir = new File(getTempPath(params.get("fileMd5")));
28
        File[] tempfileArr = tempDir.listFiles();
29
        File storeFile = new File(getStorePath() + "/" + filename);
30
        try {
31
            VideoUtil.merge(tempfileArr, storeFile);
32
        }
33
    ...

最后,实际上这种方式断点续传仍然存在很多细节没有考虑,比如多线程,同个浏览器两个tab发送同一文件时如何处理?

3、参考链接


浅谈文件断点续传和WebUploader的基本结合的更多相关文章

  1. SpringBoot: 浅谈文件上传和访问的坑 (MultiPartFile)

    本次的项目环境为 SpringBoot 2.0.4, JDK8.0. 服务器环境为CentOS7.0, Nginx的忘了版本. 前言 SpringBoot使用MultiPartFile接收来自表单的f ...

  2. 浅谈 举家搬迁静态文件到CDN

    由于七牛CDN最近做活动,对于标准用户可以免费使用如下优惠 10 GB 存储空间 10 G/月 下载流量 10 万次/月 PUT/DELETE 请求 100 万次/月 GET 请求 以上这些指标直接就 ...

  3. 浅谈qmake之pro、pri、prf、prl文件

    浅谈qmake之pro.pri.prf.prl文件 转载自:http://blog.csdn.net/dbzhang800/article/details/6348432 尽管每次和cmake对比起来 ...

  4. 浅谈JS中的!=、== 、!==、===的用法和区别 JS中Null与Undefined的区别 读取XML文件 获取路径的方式 C#中Cookie,Session,Application的用法与区别? c#反射 抽象工厂

    浅谈JS中的!=.== .!==.===的用法和区别   var num = 1;     var str = '1';     var test = 1;     test == num  //tr ...

  5. c#Winform程序调用app.config文件配置数据库连接字符串 SQL Server文章目录 浅谈SQL Server中统计对于查询的影响 有关索引的DMV SQL Server中的执行引擎入门 【译】表变量和临时表的比较 对于表列数据类型选择的一点思考 SQL Server复制入门(一)----复制简介 操作系统中的进程与线程

    c#Winform程序调用app.config文件配置数据库连接字符串 你新建winform项目的时候,会有一个app.config的配置文件,写在里面的<connectionStrings n ...

  6. 浅谈Linux下/etc/passwd文件

    浅谈Linux 下/etc/passwd文件 看过了很多渗透测试的文章,发现在很多文章中都会有/etc/passwd这个文件,那么,这个文件中到底有些什么内容呢?下面我们来详细的介绍一下. 在Linu ...

  7. 浅谈头文件(.h)和源文件(.cpp)的区别

    浅谈头文件(.h)和源文件(.cpp)的区别 本人原来在大一写C的时候,都是所有代码写在一个文件里一锅乱煮.经过自己开始写程序之后,发现一个工程只有一定是由多个不同功能.分门别类展开的文件构成的.一锅 ...

  8. Android性能优化的浅谈

    一.概要: 本文主要以Android的渲染机制.UI优化.多线程的处理.缓存处理.电量优化以及代码规范等几方面来简述Android的性能优化 二.渲染机制的优化: 大多数用户感知到的卡顿等性能问题的最 ...

  9. HTTP浅谈

    HTTP浅谈 1···什么是HTTP? HTTP协议就是超文本传输协议(HyperText Transfer Protocol),通俗理解是浏览器和web服务器传输数据格式的协议,HTTP协议是一个应 ...

随机推荐

  1. Javascript 对象 - 数学对象

    数学对象 JavaScript中提供了math对象,math对象包含一些常用的属相和方法.Math对象与Array对象.String对象.Data对象不同,没有构造函数,因此不能创建Math对象.可直 ...

  2. python模块--collections

    python的内建模块collections有几个关键的数据结构,平常在使用的时候,开发者可以直接调用,不需要自己重复制造轮子,这样可以提高开发效率. 1. deque双端队列 平常我们使用的pyth ...

  3. mysql之连接查询、联合查询、子查询

    本文内容: 连接查询 联合查询 子查询 from子查询 where子查询 exists子查询 首发日期:2018-04-11 连接查询: 连接查询就是将多个表联合起来查询,连接查询方式有内连接.外连接 ...

  4. SQL 数据插入、删除 大数据

    --测试表 CREATE TABLE [dbo].[Employee] ( [EmployeeNo] INT PRIMARY KEY, [EmployeeName] [nvarchar](50) NU ...

  5. Linux 最小化安装后IP的配置(DHCP获取IP地址)

    图形化Linux的DHCP好配置,我就不讲了.主要将一下Linux最小化安装后IP的配置: linux最小化安装后没有ifconfig这个命令: yum install net-tools.x86_6 ...

  6. SQL Server中如何识别、查找未使用的索引(unused indexes)

    在SQL Server中,索引是优化SQL性能的一大法宝.但是由于各种原因,索引会被当做"银弹"滥用,一方面有些开发人员(甚至是部分数据库管理员)有一些陋习,不管三七二十一,总是根 ...

  7. SQLServer2016 AlwaysOn AG基于工作组的搭建笔记

    最近搭建了一套SQLServer2016 AlwaysOn AG. (后记:经实际测试,使用SQLServer2012 也同样可以在Winserver2016上搭建基于工作组的AlwaysOn AG, ...

  8. 如何阅读luajit的代码——用vs调试篇

    为什么要看luajit的源码 作为目前最快的脚本语言之一,luajit确实是一个杰作,但相比原生lua仅仅几万行的代码而言,luajit却可以说是巨无霸.更要命的是,luajit之所以快,是因为大量使 ...

  9. python爬虫之小说网站--下载小说(正则表达式)

    python爬虫之小说网站--下载小说(正则表达式) 思路: 1.找到要下载的小说首页,打开网页源代码进行分析(例:https://www.kanunu8.com/files/old/2011/244 ...

  10. creo2.0安装方法和图文详解教程

    Creo2.0是由PTC公司2012年8月底推出的全新CAD设计软件包,整合了PTC公司的三个软件Pro/Engineer的参数化技术.CoCreate的直接建模技术和ProductView的三维可视 ...