一 如何实现大文件的断点续传?

1.1 是什么

不管怎样简单的需求,在量级达到⼀定层次时,都会变得异常复杂

⽂件上传简单,⽂件变⼤就复杂

上传⼤⽂件时,以下⼏个变量会影响我们的⽤⼾体验

  • 服务器处理数据的能⼒
  • 请求超时
  • ⽹络波动

上传时间会变⻓,⾼频次⽂件上传失败,失败后⼜需要重新上传等等

为了解决上述问题,我们需要对⼤⽂件上传单独处理

这⾥涉及到分⽚上传及断点续传两个概念

1.1.1 分⽚上传

分⽚上传,就是将所要上传的⽂件,按照⼀定的⼤⼩,将整个⽂件分隔成多个数据块(Part)来进⾏分⽚上传

如下图

上传完之后再由服务端对所有上传的⽂件进⾏汇总整合成原始的⽂件

⼤致流程如下:

  1. 将需要上传的⽂件按照⼀定的分割规则,分割成相同⼤⼩的数据块;
  2. 初始化⼀个分⽚上传任务,返回本次分⽚上传唯⼀标识;
  3. 按照⼀定的策略(串⾏或并⾏)发送各个分⽚数据块;
  4. 发送完成后,服务端根据判断数据上传是否完整,如果完整,则进⾏数据块合成得到原始⽂件

1.1.2 断点续传

断点续传指的是在下载或上传时,将下载或上传任务⼈为的划分为⼏个部分

每⼀个部分采⽤⼀个线程进⾏上传或下载,如果碰到⽹络故障,可以从已经上传或下载的部分开始继续上传下载未完成的部分,⽽没有必要从头开始上传下载。⽤⼾可以节省时间,提⾼速度

⼀般实现⽅式有两种:

  • 服务器端返回,告知从哪开始
  • 浏览器端⾃⾏处理

上传过程中将⽂件在服务器写为临时⽂件,等全部写完了(⽂件上传完),将此临时⽂件重命名为正式⽂件即可

如果中途上传中断过,下次上传的时候根据当前临时⽂件⼤⼩,作为在客⼾端读取⽂件的偏移量,从此位置继续读取⽂件数据块,上传到服务器从此偏移量继续写⼊⽂件即可

1.2 实现思路

整体思路⽐较简单,拿到⽂件,保存⽂件唯⼀性标识,切割⽂件,分段上传,每次上传⼀段,根据唯⼀性标识判断⽂件上传进度,直到⽂件的全部⽚段上传完毕

下⾯的内容都是伪代码

读取⽂件内容:

const input = document.querySelector('input');
 input.addEventListener('change', function() {
 var file = this.files[0];
 });

可以使⽤ md5 实现⽂件的唯⼀性

const md5code = md5(file);

然后开始对⽂件进⾏分割

var reader = new FileReader();
 reader.readAsArrayBuffer(file);
 reader.addEventListener("load", function(e) {
 //每10M切割⼀段,这⾥只做⼀个切割演⽰,实际切割需要循环切割,
 var slice = e.target.result.slice(0, 10, 1024, 1024);
 });

h5上传⼀个(⼀⽚)

const formdata = new FormData();
 formdata.append('0', slice);
 //这⾥是有⼀个坑的,部分设备⽆法获取⽂件名称,和⽂件类型,这个在最后给出解决⽅案
 formdata.append('filename', file.filename);
 var xhr = new XMLHttpRequest();
 xhr.addEventListener('load', function() {
 //xhr.responseText
 });
 xhr.open('POST', '');
 xhr.send(formdata);
 xhr.addEventListener('progress', updateProgress);
 xhr.upload.addEventListener('progress', updateProgress);
 function updateProgress(event) {
 if (event.lengthComputable) {
 //进度条
 }
 }

这⾥给出常⻅的图⽚和视频的⽂件类型判断

function checkFileType(type, file, back) {
/**
type png jpg mp4 ...
file input.change=> this.files[0]
back callback(boolean)
*/
var args = arguments;
if (args.length != 3) {
back(0);
}
var type = args[0]; // type = '(png|jpg)' , 'png'
var file = args[1];
var back = typeof args[2] == 'function' ? args[2] : function() {};
if (file.type == '') {
// 如果系统⽆法获取⽂件类型,则读取⼆进制流,对⼆进制进⾏解析⽂件类型
var imgType = [
'ff d8 ff', //jpg
'89 50 4e', //png
'0 0 0 14 66 74 79 70 69 73 6F 6D', //mp4
'0 0 0 18 66 74 79 70 33 67 70 35', //mp4
'0 0 0 0 66 74 79 70 33 67 70 35', //mp4
'0 0 0 0 66 74 79 70 4D 53 4E 56', //mp4
'0 0 0 0 66 74 79 70 69 73 6F 6D', //mp4
'0 0 0 18 66 74 79 70 6D 70 34 32', //m4v
'0 0 0 0 66 74 79 70 6D 70 34 32', //m4v
'0 0 0 14 66 74 79 70 71 74 20 20', //mov
'0 0 0 0 66 74 79 70 71 74 20 20', //mov
'0 0 0 0 6D 6F 6F 76', //mov
'4F 67 67 53 0 02', //ogg
'1A 45 DF A3', //ogg
'52 49 46 46 x x x x 41 56 49 20', //avi (RIFF fileSize fileType LIST)(52 49 46 46,DC 6C 57
09,41 56 49 20,4C 49 53 54)
];
var typeName = [
'jpg',
'png',
'mp4',
'mp4',
'mp4',
'mp4',
'mp4',
'm4v',
'm4v',
'mov',
'mov',
'mov',
'ogg',
'ogg',
'avi',
];
var sliceSize = /png|jpg|jpeg/.test(type) ? 3 : 12;
var reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.addEventListener("load", function(e) {
var slice = e.target.result.slice(0, sliceSize);
reader = null;
if (slice && slice.byteLength == sliceSize) {
var view = new Uint8Array(slice);
var arr = [];
view.forEach(function(v) {
arr.push(v.toString(16));
});
view = null;
var idx = arr.join(' ').indexOf(imgType);
if (idx > -1) {
back(typeName[idx]);
} else {
arr = arr.map(function(v) {
if (i > 3 && i < 8) {
return 'x';
}
return v;
});
var idx = arr.join(' ').indexOf(imgType);
if (idx > -1) {
back(typeName[idx]);
} else {
back(false);
}
}
} else {
back(false);
}
});
} else {
var type = file.name.match(/\.(\w+)$/)[1];
back(type);
}
}

调⽤⽅法如下

checkFileType('(mov|mp4|avi)',file,function(fileType){
 // fileType = mp4,
 // 如果file的类型不在枚举之列,则返回false
 });

上⾯上传⽂件的⼀步,可以改成:

formdata.append('filename', md5code+'.'+fileType);

有了切割上传后,也就有了⽂件唯⼀标识信息,断点续传变成了后台的⼀个⼩⼩的逻辑判断

后端主要做的内容为:根据前端传给后台的 md5 值,到服务器磁盘查找是否有之前未完成的⽂件合并信息(也就是未完成的半成品⽂件切⽚),取到之后根据上传切⽚的数量,返回数据告诉前端开始从第⼏节上传

如果想要暂停切⽚的上传,可以使⽤ XMLHttpRequest 的 abort ⽅法

1.3 使⽤场景

  • ⼤⽂件加速上传:当⽂件⼤⼩超过预期⼤⼩时,使⽤分⽚上传可实现并⾏上传多个 Part, 以加快上传速度
  • ⽹络环境较差:建议使⽤分⽚上传。当出现上传失败的时候,仅需重传失败的Part
  • 流式上传:可以在需要上传的⽂件⼤⼩还不确定的情况下开始上传。这种场景在视频监控等⾏业应⽤中⽐较常⻅

1.4 总结

当前的伪代码,只是提供⼀个简单的思路,想要把事情做到极致,我们还需要考虑到更多场景,⽐如

  • 切⽚上传失败怎么办
  • 上传过程中刷新⻚⾯怎么办
  • 如何进⾏并⾏上传
  • 切⽚什么时候按数量切,什么时候按⼤⼩切
  • 如何结合 Web Worker 处理⼤⽂件上传
  • 如何实现秒传