登录
注册
开源
企业版
高校版
搜索
帮助中心
使用条款
关于我们
开源
企业版
高校版
私有云
模力方舟
AI 队友
登录
注册
Gitee 2025 年度开源项目评选中
代码拉取完成,页面将自动刷新
捐赠
捐赠前请先登录
取消
前往登录
扫描微信二维码支付
取消
支付完成
支付提示
将跳转至支付宝完成支付
确定
取消
Watch
不关注
关注所有动态
仅关注版本发行动态
关注但不提醒动态
3
Star
45
Fork
21
DreamCoders
/
CoderGuide
代码
Issues
1169
Pull Requests
0
Wiki
统计
流水线
服务
JavaDoc
PHPDoc
质量分析
Jenkins for Gitee
腾讯云托管
腾讯云 Serverless
悬镜安全
阿里云 SAE
Codeblitz
SBOM
我知道了,不再自动展开
更新失败,请稍后重试!
移除标识
内容风险标识
本任务被
标识为内容中包含有代码安全 Bug 、隐私泄露等敏感信息,仓库外成员不可访问
大文件怎么实现断点续传?
待办的
#IAG9PE
陌生人
拥有者
创建于
2024-07-29 16:07
<h2>一、是什么</h2> <p>不管怎样简单的需求,在量级达到一定层次时,都会变得异常复杂</p> <p>文件上传简单,文件变大就复杂</p> <p>上传大文件时,以下几个变量会影响我们的用户体验</p> <ul> <li>服务器处理数据的能力</li> <li>请求超时</li> <li>网络波动</li> </ul> <p>上传时间会变长,高频次文件上传失败,失败后又需要重新上传等等</p> <p>为了解决上述问题,我们需要对大文件上传单独处理</p> <p>这里涉及到分片上传及断点续传两个概念</p> <h4>分片上传</h4> <p>分片上传,就是将所要上传的文件,按照一定的大小,将整个文件分隔成多个数据块(Part)来进行分片上传</p> <p>如下图</p> <p><img src="https://static.ecool.fun//article/601be6fe-4b6e-420b-b88a-0e54be051d02.png" alt="" /></p> <p>上传完之后再由服务端对所有上传的文件进行汇总整合成原始的文件</p> <p>大致流程如下:</p> <ol> <li>将需要上传的文件按照一定的分割规则,分割成相同大小的数据块;</li> <li>初始化一个分片上传任务,返回本次分片上传唯一标识;</li> <li>按照一定的策略(串行或并行)发送各个分片数据块;</li> <li>发送完成后,服务端根据判断数据上传是否完整,如果完整,则进行数据块合成得到原始文件</li> </ol> <h4>断点续传</h4> <p>断点续传指的是在下载或上传时,将下载或上传任务人为的划分为几个部分</p> <p>每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传下载未完成的部分,而没有必要从头开始上传下载。用户可以节省时间,提高速度</p> <p>一般实现方式有两种:</p> <ul> <li>服务器端返回,告知从哪开始</li> <li>浏览器端自行处理</li> </ul> <p>上传过程中将文件在服务器写为临时文件,等全部写完了(文件上传完),将此临时文件重命名为正式文件即可</p> <p>如果中途上传中断过,下次上传的时候根据当前临时文件大小,作为在客户端读取文件的偏移量,从此位置继续读取文件数据块,上传到服务器从此偏移量继续写入文件即可</p> <h2>二、实现思路</h2> <p>整体思路比较简单,拿到文件,保存文件唯一性标识,切割文件,分段上传,每次上传一段,根据唯一性标识判断文件上传进度,直到文件的全部片段上传完毕</p> <p><img src="https://static.ecool.fun//article/a9b201e1-12dc-4598-a7a1-205a15d573f2.png" alt="" /></p> <p>下面的内容都是伪代码</p> <p>读取文件内容:</p> <pre><code class="language-js">const input = document.querySelector('input'); input.addEventListener('change', function() { var file = this.files[0]; });</code></pre> <p>可以使用<code>md5</code>实现文件的唯一性</p> <pre><code class="language-js">const md5code = md5(file);</code></pre> <p>然后开始对文件进行分割</p> <pre><code class="language-js">var reader = new FileReader(); reader.readAsArrayBuffer(file); reader.addEventListener("load", function(e) { //每10M切割一段,这里只做一个切割演示,实际切割需要循环切割, var slice = e.target.result.slice(0, 10*1024*1024); });</code></pre> <p>h5上传一个(一片)</p> <pre><code class="language-js">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) { //进度条 } }</code></pre> <p>这里给出常见的图片和视频的文件类型判断</p> <pre><code class="language-js">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); } }</code></pre> <p>调用方法如下</p> <pre><code class="language-js">checkFileType('(mov|mp4|avi)',file,function(fileType){ // fileType = mp4, // 如果file的类型不在枚举之列,则返回false });</code></pre> <p>上面上传文件的一步,可以改成:</p> <pre><code class="language-js">formdata.append('filename', md5code+'.'+fileType);</code></pre> <p>有了切割上传后,也就有了文件唯一标识信息,断点续传变成了后台的一个小小的逻辑判断</p> <p>后端主要做的内容为:根据前端传给后台的<code>md5</code>值,到服务器磁盘查找是否有之前未完成的文件合并信息(也就是未完成的半成品文件切片),取到之后根据上传切片的数量,返回数据告诉前端开始从第几节上传</p> <p>如果想要暂停切片的上传,可以使用<code>XMLHttpRequest</code>的 <code>abort</code>方法</p> <h2>三、使用场景</h2> <ul> <li>大文件加速上传:当文件大小超过预期大小时,使用分片上传可实现并行上传多个 Part, 以加快上传速度</li> <li>网络环境较差:建议使用分片上传。当出现上传失败的时候,仅需重传失败的Part</li> <li>流式上传:可以在需要上传的文件大小还不确定的情况下开始上传。这种场景在视频监控等行业应用中比较常见</li> </ul> <h2>小结</h2> <p>当前的伪代码,只是提供一个简单的思路,想要把事情做到极致,我们还需要考虑到更多场景,比如</p> <ul> <li>切片上传失败怎么办</li> <li>上传过程中刷新页面怎么办</li> <li>如何进行并行上传</li> <li>切片什么时候按数量切,什么时候按大小切</li> <li>如何结合 Web Work 处理大文件上传</li> <li>如何实现秒传</li> </ul> <p>人生又何尝不是如此,极致的人生体验有无限可能,越是后面才发现越是精彩 ~_~</p>
<h2>一、是什么</h2> <p>不管怎样简单的需求,在量级达到一定层次时,都会变得异常复杂</p> <p>文件上传简单,文件变大就复杂</p> <p>上传大文件时,以下几个变量会影响我们的用户体验</p> <ul> <li>服务器处理数据的能力</li> <li>请求超时</li> <li>网络波动</li> </ul> <p>上传时间会变长,高频次文件上传失败,失败后又需要重新上传等等</p> <p>为了解决上述问题,我们需要对大文件上传单独处理</p> <p>这里涉及到分片上传及断点续传两个概念</p> <h4>分片上传</h4> <p>分片上传,就是将所要上传的文件,按照一定的大小,将整个文件分隔成多个数据块(Part)来进行分片上传</p> <p>如下图</p> <p><img src="https://static.ecool.fun//article/601be6fe-4b6e-420b-b88a-0e54be051d02.png" alt="" /></p> <p>上传完之后再由服务端对所有上传的文件进行汇总整合成原始的文件</p> <p>大致流程如下:</p> <ol> <li>将需要上传的文件按照一定的分割规则,分割成相同大小的数据块;</li> <li>初始化一个分片上传任务,返回本次分片上传唯一标识;</li> <li>按照一定的策略(串行或并行)发送各个分片数据块;</li> <li>发送完成后,服务端根据判断数据上传是否完整,如果完整,则进行数据块合成得到原始文件</li> </ol> <h4>断点续传</h4> <p>断点续传指的是在下载或上传时,将下载或上传任务人为的划分为几个部分</p> <p>每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传下载未完成的部分,而没有必要从头开始上传下载。用户可以节省时间,提高速度</p> <p>一般实现方式有两种:</p> <ul> <li>服务器端返回,告知从哪开始</li> <li>浏览器端自行处理</li> </ul> <p>上传过程中将文件在服务器写为临时文件,等全部写完了(文件上传完),将此临时文件重命名为正式文件即可</p> <p>如果中途上传中断过,下次上传的时候根据当前临时文件大小,作为在客户端读取文件的偏移量,从此位置继续读取文件数据块,上传到服务器从此偏移量继续写入文件即可</p> <h2>二、实现思路</h2> <p>整体思路比较简单,拿到文件,保存文件唯一性标识,切割文件,分段上传,每次上传一段,根据唯一性标识判断文件上传进度,直到文件的全部片段上传完毕</p> <p><img src="https://static.ecool.fun//article/a9b201e1-12dc-4598-a7a1-205a15d573f2.png" alt="" /></p> <p>下面的内容都是伪代码</p> <p>读取文件内容:</p> <pre><code class="language-js">const input = document.querySelector('input'); input.addEventListener('change', function() { var file = this.files[0]; });</code></pre> <p>可以使用<code>md5</code>实现文件的唯一性</p> <pre><code class="language-js">const md5code = md5(file);</code></pre> <p>然后开始对文件进行分割</p> <pre><code class="language-js">var reader = new FileReader(); reader.readAsArrayBuffer(file); reader.addEventListener("load", function(e) { //每10M切割一段,这里只做一个切割演示,实际切割需要循环切割, var slice = e.target.result.slice(0, 10*1024*1024); });</code></pre> <p>h5上传一个(一片)</p> <pre><code class="language-js">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) { //进度条 } }</code></pre> <p>这里给出常见的图片和视频的文件类型判断</p> <pre><code class="language-js">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); } }</code></pre> <p>调用方法如下</p> <pre><code class="language-js">checkFileType('(mov|mp4|avi)',file,function(fileType){ // fileType = mp4, // 如果file的类型不在枚举之列,则返回false });</code></pre> <p>上面上传文件的一步,可以改成:</p> <pre><code class="language-js">formdata.append('filename', md5code+'.'+fileType);</code></pre> <p>有了切割上传后,也就有了文件唯一标识信息,断点续传变成了后台的一个小小的逻辑判断</p> <p>后端主要做的内容为:根据前端传给后台的<code>md5</code>值,到服务器磁盘查找是否有之前未完成的文件合并信息(也就是未完成的半成品文件切片),取到之后根据上传切片的数量,返回数据告诉前端开始从第几节上传</p> <p>如果想要暂停切片的上传,可以使用<code>XMLHttpRequest</code>的 <code>abort</code>方法</p> <h2>三、使用场景</h2> <ul> <li>大文件加速上传:当文件大小超过预期大小时,使用分片上传可实现并行上传多个 Part, 以加快上传速度</li> <li>网络环境较差:建议使用分片上传。当出现上传失败的时候,仅需重传失败的Part</li> <li>流式上传:可以在需要上传的文件大小还不确定的情况下开始上传。这种场景在视频监控等行业应用中比较常见</li> </ul> <h2>小结</h2> <p>当前的伪代码,只是提供一个简单的思路,想要把事情做到极致,我们还需要考虑到更多场景,比如</p> <ul> <li>切片上传失败怎么办</li> <li>上传过程中刷新页面怎么办</li> <li>如何进行并行上传</li> <li>切片什么时候按数量切,什么时候按大小切</li> <li>如何结合 Web Work 处理大文件上传</li> <li>如何实现秒传</li> </ul> <p>人生又何尝不是如此,极致的人生体验有无限可能,越是后面才发现越是精彩 ~_~</p>
评论 (
0
)
登录
后才可以发表评论
状态
待办的
待办的
进行中
已完成
已关闭
负责人
未设置
标签
Html/JS/CSS
未设置
标签管理
里程碑
未关联里程碑
未关联里程碑
Pull Requests
未关联
未关联
关联的 Pull Requests 被合并后可能会关闭此 issue
分支
未关联
未关联
master
开始日期   -   截止日期
-
置顶选项
不置顶
置顶等级:高
置顶等级:中
置顶等级:低
优先级
不指定
严重
主要
次要
不重要
参与者(1)
1
https://gitee.com/DreamCoders/CoderGuide.git
git@gitee.com:DreamCoders/CoderGuide.git
DreamCoders
CoderGuide
CoderGuide
点此查找更多帮助
搜索帮助
Git 命令在线学习
如何在 Gitee 导入 GitHub 仓库
Git 仓库基础操作
企业版和社区版功能对比
SSH 公钥设置
如何处理代码冲突
仓库体积过大,如何减小?
如何找回被删除的仓库数据
Gitee 产品配额说明
GitHub仓库快速导入Gitee及同步更新
什么是 Release(发行版)
将 PHP 项目自动发布到 packagist.org
评论
仓库举报
回到顶部
登录提示
该操作需登录 Gitee 帐号,请先登录后再操作。
立即登录
没有帐号,去注册