文件上传之断点续传和跨端续传

我们要上传一个1G+的大文件,前端采用分片上传,但是由于某种原因比如要下班了,断电了或是网线被拔掉了,上传被迫中断。那么别急,我们有断点续传功能,你可以将大文件带回家慢慢从中断处继续上传,而不需要重新上传整个文件。

为了阅读和开发方便,我将文件上传系列相关文章章节列出来,建议从01节开始看起,文章内容按顺序紧紧相连:

本系列文章相关源码已经上传到github上,请参照下载:https://github.com/lrfbeyond/fast-uploader

断点续传原理及流程

文章讲解了《02.文件分片上传之前端文件分片》,我们将文件分片后,一片一片依次上传给后端。后端将这些分片文件存储在一个临时目录下,等待所有分片上传完毕再合并一个完整文件。

文件上传时,会携带文件唯一标识,也就是MD5值以及分片信息。后端按照md5保存分片,即名称为同一md5的分片属于同一个文件。(理论上也有md5值冲突的,但是几率很少)。

当某种原因导致上传中断后,后端会保存已经上传好的分片。

当前端发送继续上传请求时,后端会查找该文件是否有已经上传好的分片,如果有,那么将这些分片id返回给前端,告诉前端这些分片不必再上传了,从断点处继续上传剩余的分片。

所有分片上传完毕,合并文件。

前端发送续传请求

我们在上一篇文章《05.文件上传之秒传文件》讲到,在vue-simple-uploader的options选项中添加函数checkChunkUploadedByResponse(),该函数响应后台返回message信息,同时检测分片信息是否上传完整。上传分片前,前端会携带文件md5等信息先向后端发送一个get请求,这个checkChunkUploadedByResponse()就是用来响应这个get请求的。

// 服务器分片校验函数
  checkChunkUploadedByResponse: (chunk, message) => {
      let obj = JSON.parse(message);
      if (obj.isExist) {
          this.statusTextMap.success = '秒传文件';
          return true;
      }

      return (obj.uploaded || []).indexOf(chunk.offset + 1) >= 0
  },
知识兔

很显然,如果返回obj.isExist则表示文件已经存在,应该是秒传文件,该文件的所有后续上传操作就没了。

如果返回的是文件分片信息,类似:{"uploaded":[1,2,3,4,5,6,7,8,9,10,...]},表示已经上传过了该文件的这些分片。

那么接下来就是续传,返回接下来应该从第几个分片续传,(obj.uploaded || []).indexOf(chunk.offset + 1) >= 0意思是从断点分片处继续上传下一个分片。当然了,如果没有上传过分片,那就是从第一个分片起开始上传。

后端分析断点,接受续传

后端在接收前端发来的get请求,先检测md5,确定是否应该秒传文件,然后检测已经上传了哪些分片,将这些分片id合并成数组,返回给前端。

//检测断点和md5
public function checkFile()
{
    $identifier = $this->fileInfo['identifier'];
    $filePath = self::$tmpDir. DIRECTORY_SEPARATOR . $identifier; //临时分片文件路径
    $totalChunks = $this->fileInfo['totalChunks'];

    //检测文件md5是否已经存在
    $rs = $this->checkMd5($identifier, $this->fileInfo['totalSize']);
    if ($rs['isExist'] === true) {
        return $rs;
    }

    //检查分片是否存在
    $chunkExists = [];
    for ($index = 1; $index <= $totalChunks; $index++ ) {
        if (file_exists("{$filePath}_{$index}")) {
            array_push($chunkExists, $index);
        }
    }
    if (count($chunkExists) == $totalChunks) { //全部分片存在,则直接合成
        $this->merge();
    } else {
        $res['uploaded'] = $chunkExists;
        return $res;
    }
}
知识兔

在分片临时目录保存的是文件的分片,分片的命名形式:唯一标识_第几个分片。如:d816ed041157c4af2489148a1ffd60d4_29,表示md5值为d816ed041157c4af2489148a1ffd60d4第29个分片。这样大家就明白了,根据分片总数,遍历该文件的所有分片,将分片id即第几个分片追加到数组中$chunkExists,如果所有分片已经和分片总数相等时应该合并文件,否则返回字段:$res['uploaded'] = $chunkExists,最终给到前端的是json格式数据。

试验

最后我们来测试断点续传。

首先打开你的Chrome浏览器利器,运行上传页面,选择一个大点的文件,这里我选择了一个1.9GB的系统镜像文件,开始上传。

202203131822133987500000

现在,我们关闭这个浏览器,断开网络。你可以断电,重启电脑等方式模拟上传中断,然后把文件带回家。

然后使用另外一个浏览器,我临时用以下360浏览器,模拟跨浏览器场景。

注意重新选择同一个文件上传。在上传前检测md5值是必不可少的,大文件的md5计算速度也会相对较长一点。

计算完md5后,前端先是向后端发送一个get请求,看看是否是秒传,应急看看是否已经传过一部分了,应该断点续传。

很显然,后端返回了已经上传的分片,那么就该从断点处续传。

我们到服务端打开分片临时目录可以看到:

202203131822148496360001

注意图中红框部分,第一次上传后,分片id编号是135,时间是17:02,下一个分片id是136,时间是17:05。也就是说第二次上传的时候是从136开始的,之前的分片并没有被删除也没有被覆盖,最后等所有分片上传完毕就合并成一个完整的文件。

于是就实现了跨浏览器跨终端续传文件。

关于文件上传的总结

文件上传的关键是分片和计算md5,根据文件分片信息和md5值,可以实现分片上传、秒传文件、断点续传等功能。

不管是秒传文件还是续传文件,第一步计算md5是不能跨越的。

服务端文件的保存方式,一般设置一个临时目录保存分片信息,合并后移除分片,并将最终文件保存在另一个目录下。根据项目需求,文件保存的目录可以是在web目录下,也可以是在web访问不到的目录。

关于文件上传的系列文章就写到这里,接下来我准备给大家分享文件下载的事,您将了解到如何下载大文件、如何实现断点下载,如何异步下载文件,敬请关注。

计算机