浅谈之大文件分片上传

FileReader · curry · 于 3个月前 发布 · 89 次阅读

一.分片上传介绍

日常开发中,经常会有上传文件这类需求,对于小文件直接上传,无非修改一点nginx以及php.ini的配置,但是碰到大文件上传的话,浏览器直接卡死,或者占用大量系统的资源,这个时候传统的方式已然不行了,这里结合我最近做im里面的上传文件记录一下。

二.客户端的处理

客户端使用H5的FileReader对象进行异步的读取存储用户计算机上的文件,使用 File 或 Blob 对象指定要读取的文件或数据。这个对象提供了一下四种方法读取数据。

FileReader.readAsArrayBuffer()

开始读取指定的 Blob中的内容, 一旦完成, result 属性中保存的将是被读取文件的 ArrayBuffer 数据对象.

FileReader.readAsBinaryString() 

开始读取指定的Blob中的内容。一旦完成,result属性中将包含所读取文件的原始二进制数据。

FileReader.readAsDataURL()

开始读取指定的Blob中的内容。一旦完成,result属性中将包含一个data: URL格式的字符串以表示所读取文件的内容。

FileReader.readAsText()

开始读取指定的Blob中的内容。一旦完成,result属性中将包含一个字符串以表示所读取的文件内容。

你可以看到,具体读取的方式表现的类型是不同的。我demo使用的是readAsArrayBuffer,也就是异步按字节读取内容,最后生成一个ArrayBuffer对象发送给服务端.,看一下具体的一些客户端代码,这里我去除掉一些文件判断设置什么的,只放最重要的。

数据读取完之后呢

FileReader的事件处理:

FileReader.onload

处理load事件。该事件在读取操作完成时触发。

我只放了一个核心的,也就是当上面的读取成功之后会触发load事件,我们就可以在这里面进行发送的业务逻辑。

  ```javascript
 reader:null,      //本次读取数据对象
    step:1024 * 1024, //每次读取文件大小,字节数
    cuLoaded:0,      //当前已读的总数
    file:null ,      //当前读取的文件对象
    enableRead:true,  //标识是否可以读取文件 用来做暂停上传用的
    total:0,          //记录当前文件的总字节数
    startTime:null,   // 标识开始上传时间
    fileCount:0       //分片总数

//上传文件

 var self = this;
  uploadFile:function(){
    var fielBox=document.getElementById('myFile');
    fielBox.onchange=function () {  //
       self.file=this.files[0];      //当前读取的文件对象
       self.total=self.file.size;    //当前读取的文件大小
      self.bindReader()   
    }
  },
   //绑定reader
  bindReader:function(){
    this.cuLoaded=0;
    this.fileCount=0;
    this.startTime=new Date();
    this.enableRead=true;
    this.reader=new FileReader();
   // this.$layer.msg('正在上传',{icon:16,shade:[0.5]});
    //读取一段成功
    this.reader.onload=function (e) { //读取成功触发
      console.log('读取总数:' +e.loaded);  //当前读取的字节数,,即readBlob里面读取完触发onload
      if(this.enableRead==false){
        return false;
      }
      //根据当前缓冲区来控制客户端读取速度
      if(self.wSock.bufferedAmount>self.step*10){
        setTimeout(function () {
          //继续读取
          console.log('进入等待中');
          self.loadSuccess(e.loaded);
        },3)
      }else{
        //继续读取
        self.loadSuccess(e.loaded);
      }
    }
    //先读取
    this.readBlob();
  },
  
   //指定位置开始读取
  readBlob:function(){
    ++this.fileCount;
    //指定读取开始位置和结束位置读取文件
     var blob=this.file.slice(this.cuLoaded,this.cuLoaded+this.step); //每次设定只能获取最大的数据字节数
     this.reader.readAsArrayBuffer(blob); //当前读取字节作为ArrayBuffer对象发送
  },

  //读取文件成功处理
  loadSuccess:function(loaded){
    //将分段数据上传到服务器
    var blob=this.reader.result;  //当前发送的数据对象

    //WebSocket发送数据
    this.wSock.send(blob);
    //如果没有读完,继续
    this.cuLoaded += loaded;
    if(this.cuLoaded < this.total){
      this.readBlob();
    }else{
     //所有的上传成功,发送一个确认消息,后端对比一下
      var message={
        'type':12,
        'chatType':this.chatType,
        'contentType':2,
        'status':2,   //片发送完毕再发送一个结束标识
        'clientCount':this.fileCount,   //文件片数
        'to':this.target,
      }
      console.log(this.fileCount);
      this.wsSend(message);
      console.log('总共上传: '+ this.cuLoaded+ ',共用时:' + (new Date().getTime() - this.startTime.getTime())/ 1000);
    }

    console.log(this.cuLoaded / this.total)* 100;
  },


**我删了一些东西,我们来看下这里的代码,先来看下我们获取的对象信息,读取文件那就不用看了,具体看下从指定位置读取完数据触发FileReader的onload**

  //指定位置开始读取
  readBlob:function(){
   
    //指定读取开始位置和结束位置读取文件
     var blob=this.file.slice(this.cuLoaded,this.cuLoaded+this.step); //每次设定只能获取最大的数据字节数
     this.reader.readAsArrayBuffer(blob); //当前读取字节作为ArrayBuffer对象发送
  },

this.reader.onload=function (e) { //读取成功触发

      console.log(e);  
    }

所以为了防止已经send但是没发送出去,同时队列已经满了情况下做一个定时器.最后的loadsuccess 就是处理发送的一些逻辑了,如果当前已发送的字节数还小于总的字节数,那么继续读取发送直至发送完毕即可。(客户端的代码你还可以自己加停止发送,继续发送或者其他验证的信息,和服务端配合这样可以保证在一些特殊情况下,比如网络问题,不需要重新发送,而从上一次断开的位置继续发送)

三.服务端代码展示

其实也没什么,每次服务端收到文件,就按照自己定义的规则存放一个分片文件,等最后文件全部上传完毕,合并所有的分片文件,写入到新的文件中,然后删除分片即可。当然说的很简单,中间需要去验证文件..一些操作。下面的代码只是一个示例,忽略它的粗鄙。

//因为做的是swoole实现的im,swoole也不是这篇文章讨论了 //所以改了一下,放出很粗糙的文件合并代码

  public  static function mergeFile($data)
  {
        $filepath='xx/xx.xx';
        for($i=1;$i<=$data['serverCount'];$i++){
            $file=$base_path.$fileInfo[0].'_'.$i.'.txt';
            $res =file_get_contents($file);
           file_put_contents($filepath,$res,FILE_APPEND);
            @unlink($file);
        }
        return $filepath;
   }

这里我上传一个300多m的,每一次发1m左右,图中看到最终发送319个ArrayBuffer对象,看一下服务端,数量没错。合并分片之后源文件也还是一样的

所以流程总结一下就是:每次客户端通过这个对象,以及我们设置每次读取的最大字节数,读取你要上传的文件,形成一个ArrayBuffer对象作为一个分片发送给服务端,每次记录一下你读取的位置,直至最后一片内容也发送给服务端,文件上传完毕,服务端把每次读取的信息作为一个分片,写入一个文件,等到客户端最后一个分片上传完毕,检验文件完整性,合并所有的分片,形成的文件就是原上传文件。

以上是关于分片上传的一些小总结,具体的一些细节处理可以自己去动手。

本文由 curry 创作,采用 知识共享署名 3.0 中国大陆许可协议 进行许可。 可自由转载、引用,但需署名作者且注明文章出处。

共收到 0 条回复 jobs ss
没有找到数据。
添加回复 (需要登录)
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册
吴亲库里的深夜食堂