浅谈文件断点续传和WebUploader的基本结合


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
});

开启分片上传后,插件会自动分片上传文件,接下来只需要在配置文件跳过和后端处理即可。官方回应在分片发送前会有监听的事件 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();
});

分片是否存在的判断,也有不同的方式,一种你可以每次计算分片的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;
}

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);
        }
    ...

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

3、参考链接


posted @ 2018-12-14 10:19  Dulk  阅读(2140)  评论(0编辑  收藏  举报