目前的 web 大文件上传和下载存在的缺陷
- 文件在上传或者下载过程中遇到网络异常,或者浏览器卡死,下载页面刷新等情况后,只能重新开始上传或者下载,耗费大量时间,也就是不支持断点续传
- 相同文件曾经上传或者下载过,二次操作还需要同样的上传或者下载,不支持秒传
- 通常后端会限制单个文件上传大小,遇到超过上传阈值无法进行文件上传
web大文件切片上传
切片上传
切片上传是指将一个大文件切割为若干个小文件,分为多个请求依次上传,后台再将文件碎片拼接为一个完整的文件,即使某个碎片上传失败,也不会影响其它文件碎片,只需要重新上传失败的部分就可以了。而且多个请求一起发送切片文件,提高了传输速度的上限,同时后端在接收到所有切片文件后,再将所有切片合并为原始上传的大文件,这样就达到将一个大文件通过切片上传的方式快速上传到服务器。
要实现切片上传需要前后端一起配合,前后端需要各自解决几个技术难点。
前端:文件如何切片、生成前后端需要识别当前上传文件的唯一识别码hash值(md5计算)
后端:合并所有切片为真实大文件,提供普通文件上传相关接口
大概流程图如下所示
前端具体实现:
- 文件切片,使用File.slice分割大文件为很多个Blob二进制大文件
- 生成文件的唯一hash值,使用spark-md5开源库(耗时)
- 所有切片上传完毕,通知后端合并文件
后端具体实现:主要提供3个接口
- 提供给前端判断文件是否已经上传过(秒传功能)
- 提供接收切片文件上传操作并保存碎片
- 提供对同一个文件的所有切片合并的功能
测试:
- 切片文件上传后,检查是否合并成功?合并文件是否能正常查看?
- 断点续传场景(文件上传一半,暂停&刷新页面&或后台服务停止),检查是否合并成功?合并文件是否能正常查看?确认是否真的断点续传,查看切片是否有重新上传(传一半,记录已经上传切片创建时间,待续传后,再次查看同一个切片文件的创建时间是否更新)
- 是否能秒传
思考:切片上传可能存在的问题及优化空间?
1: 上传完所有切片后,在合并之前,某个切片丢失?如何处理?
根据实际对合并后的文件进行md5检测判断切片前后是否一致
2: 能否进一步加快整个大文件上传速度?
目前发现上传切片前文件hash值计算需要花费比较长的时间,hash计算时间长度由上传文件大小决定,因此有人提出了抽样文件hash计算。
原理如下:
- 文件切成2M的切片
- 第一个和最后一个切片全部内容,其他切片的取 首中尾三个地方各2个字节
- 这种抽样 hash的结果,就是文件存在,有小概率误判,但是如果不存在,是100%准的
总结:
通过大文件切片上传确实在实际应用中有一些优势,比如断点续传,秒传等,时间应用开发也不复杂,可以应用在实际业务中。目前社区已经存在一些成熟的大文件上传解决方案,如七牛SDK,腾讯云SDK等,也许并不需要我们手动去实现一个简陋的大文件上传库,但是了解其原理还是十分有必要的。
三:web大文件切片下载
- 服务端切片,前端分片下载
- 切片如何存储,cookie,localStorage, 存放本地磁盘,内存,websql, indexDb
- 切片合并
- 是否支持断点续载,是否支持秒载
解决问题1: 需要客户端分段请求,服务端分段返回,需要使用HTTP Range
1、什么是Range?
当用户在听一首歌的时候,如果听到一半(网络下载了一半),网络断掉了,用户需要继续听的时候,文件服务器不支持断点的话,则用户需要重新下载这个文件。而Range支持的话,客户端应该记录了之前已经读取的文件范围,网络恢复之后,则向服务器发送读取剩余Range的请求,服务端只需要发送客户端请求的那部分内容,而不用整个文件发送回客户端,以此节省网络带宽。
2、HTTP1.1规范的Range是怎样一个约定呢?
HTTP Range 请求允许我们从服务器上只发送HTTP消息的一部分到客户端。
首先客户端会发起一个带有Range: bytes=0-xxx的请求,如果服务端支持 Range,则会在响应头中添加Accept-Ranges: bytes来表示支持 Range 的请求,之后客户端才可能发起带 Range 的请求。
服务端通过请求头中的Range: bytes=0-xxx 来判断是否是进行 Range 处理,如果这个值存在而且有效,则只发回请求的那部分文件内容,响应的状态码变成206,表示Partial Content,并设置Content-Range。如果无效,则返回416状态码,表明Request Range Not Satisfiable。如果请求头中不带 Range,那么服务端则正常响应,也不会设置 Content-Range 等。
Range的格式为:
Range:(unit=first byte pos)-[last byte pos]
即Range: 单位(如bytes)= 开始字节位置-结束字节位置。
我们来举个例子,假设我们开启了多线程下载,需要把一个5000byte的文件分为4个线程进行下载。
- Range: bytes=0-1199 头1200个字节
- Range: bytes=1200-2399 第二个1200字节
- Range: bytes=2400-3599 第三个1200字节
- Range: bytes=3600-5000 最后的1400字节
服务器给出响应:
第1个响应
- Content-Length:1200
- Content-Range:bytes 0-1199/5000
第2个响应
- Content-Length:1200
- Content-Range:bytes 1200-2399/5000
第3个响应
- Content-Length:1200
- Content-Range:bytes 2400-3599/5000
第4个响应
- Content-Length:1400
- Content-Range:bytes 3600-5000/5000
解决第二个问题,切片保存
使用indexDB存储httpRange分段下载的数据,格式blob, ArrayBuffer。
解决第三个问题,切片保存
从indexDB库里面拿到待合并所有ArrayBuffer,由于不能直接操作 ArrayBuffer 对象,所以我们需要先把ArrayBuffer 对象转换为 Uint8Array 对象
Uint8Array 对象是 ArrayBuffer 的一个数据类型(8 位不带符号整数),再将Uint8Array转换为blob,前端再利用blob API创建blob下载链接。
测试:
- 切片文件下载后,检查是否合并成功?合并文件是否能正常查看?
- 断点续传场景(文件下载一半,暂停&刷新页面&或后台服务停止),检查是否合并成功?合并文件是否能正常查看?确认是否真的断点续下载,查看切片是否有重新下载?
- 是否能秒下载
讨论:
切片下载和单文件下载速度是否有优势?
四、其他领域应用场景
还有一些领域比如视频直播,使用传统的视频文件直接上传做不到直播效果,而通过下面讲到的切片分段上传就能实现效果,即前端大文件(视频)分片上传,后端通过ffmpeg将视频转为m3u8(多个ts文件),实现视频资源一边录制(一边上传)一边播放
视频切片传递靠的是流媒体传输协议,常见的流媒体传输协议如下:rtp,rtmp,flv,hls等等
hls (HTTP Live Streaming) 是一种基于HTTP的流媒体网络传输协议;工作原理是把视频文件拆分为一段段短小、有序的小文件,浏览器端通过一个特殊的 .m3u8 索引文件来进行视频的请求,而每次只请求其中的某一个小文件,不需要请求一个完整的视频文件。
讲人话就是,把一个大视频,切割成一个个小视频,并用一个小本本把这一个个小视频的名称和顺序给记录下来,浏览器对照着这个小本本上列的记录来发送请求。
举个栗子:假设 test.mp3 的总时长为 60s,我们可以将它切割成 12个小视频,每个小视频的时长为 5s
test.mp3 => [test1.mp3, test2.mp3, testn.mp3, ···, test12.mp3]
我们的小本本 .m3u8 内容为
test1.mp3 00:01~05:00
test2.mp3 05:01~10:00
test3.mp3 10:01~15:00
···
test12.mp3 55:01~60:00
那么,浏览器就会根据你当前视频的播放进度,去 .m3u8 内寻找对应的小视频,然后将它请求下来,再塞到 video 标签内,这样就实现了视频的切片播放。一般情况下,浏览器会提前把下一段小视频也给请求下来。下面是步骤
- 视频采用切片上传,通过调用后端切片上传接口进行上传
- 切片上传结束后通过合并切片接口进行合并成为完整的视频
- 调用ffmpeg工具进行视频转m3u8格式形成ts切片
- ts切片多线程上传至云服务器(测试就直接在测试服务器处理)
将 ts 视频切割成一个个小视频,我们需要选择是以时长切割,还是以大小切割,并且,切割完成后,我们会得到一个 .m3u8 的索引文件 ffmpeg -i 预热赛视频.ts -c copy -map 0 -f segment -segment_list chunk/index.m3u8 -segment_time 10 chunk/test-%04d.ts 5. 返回m3u8格式文件地址,前端集成播放器进行播放。