在网络应用中,大文件上传是一个技术挑战。本文详细解析了大文件上传的核心原理,并探讨了多种实现方案。从基本的文件分割、断点续传到复杂的并行上传,文章涵盖了一系列技术细节和最佳实践,包括如何处理网络波动、提高数据传输效率等关键问题。此外,还介绍了相关的前端和后端技术支持。无论是开发者还是架构师,这篇文章都将提供有力的技术指导和实战参考,帮助读者高效解决大文件上传问题。
在今年的敏捷团队建设中,我通过Suite执行器实现了一键自动化单元测试。Juint除了Suite执行器还有哪些执行器呢?由此我的Runner探索之旅开始了!
一般,我们传送大文件是指传送大于100M的文件,而普通文件是指小于100M,常见的是20M、30M和50M,两者主要的区别在于文件大小上,还有传送速度上。
一般普通“邮件附件”只能发20M、30M,50M的文件,而几百M的照片、文件、设计图等大文件传送起来就不是那么容易了。
理解,首先 MCube 会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将目标页面展示到屏幕。
普通文件上传只需要注意两点
1.指定上传的接口地址。
2.将请求头的Content-Type设置成:multipart/form-data,将文件对象以二进制流的形式传给后端
大文件上传时会遇到的问题
1.前后端上传请求超时限制,一次性传输大小限制。
2.网络抖动等,失败后需要重新上传。
3.http1.1版本, TCP连接默认是open的,所有请求都通过同一个连接进行数据传输,如果前面的请求被阻塞了,后面的请求也得不到响应,也叫HTTP/1.1 中的队头阻塞问题,除非建立多个连接,但是多个连接会浪费资源。
4.无进度条,用户体验极差。
理解,首先 MCube 会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将目标页面展示到屏幕。
获取文件的二进制内容,然后对其内容拆分成指定大小的切片文件,最后将每个切片上传到服务端即可。
流程:获取文件 ➡️ 分片 ➡️ 上传
需要优化的点
根据切片文件的唯一标识在后端将多个相同文件的切片还原成一个文件
流程:获取分片文件 ➡️ 还原分片 ➡️ 返回拼接好的文件信息
需要优化的点
还原切片时需要注意的问题
解决办法
1)如何识别多个切片是来自于同一个文件的?
这个可以在发送请求时,为每个切片传递一个相同文件的identifier参数。
2)如何将多个切片还原成一个文件?
什么时候开始拼接:确认所有切片都已上传完后开始进行拼接,这个可以通过客户端在切片全部上传后调用后端定义的mkfile接口来通知服务端进行拼接,或者前端传递切片的总数totalChunks, 服务端判断接收的切片数量如果等于totalChunks的值就开始进行拼接,无须前端通知后端进行拼接。
怎么按顺序拼接:可以在每个切片上标记一个位置索引值,找到同一个context下的所有切片,根据chunkNumber确认每个切片的顺序,这个按顺序拼接切片,还原成文件
上面有几个重要的参数:identifier ,chunkNumber,totalChunks
identifier :我们需要获取为一个文件的唯一标识,可以通过下面两种方式获取
1. 根据文件名、文件长度等基本信息进行拼接,为了避免多个用户上传相同的文件,可以再额外拼接用户信息如uid等保证唯一性
2. 根据文件的二进制内容计算文件的hash,这样只要文件内容不一样,则标识也会不一样,缺点在于计算量比较大.
chunkNumber:当前切片的索引
totalChunks:总的切片数
理解,首先 MCube 会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将目标页面展示到屏幕。
前端分片代码
// 获取identifier,同一个文件会返回相同的值
function createIdentifiert(file) {
return file.name + file.size
}
let file = document.querySelector("[name=file]").files[0];
const LENGTH = 1024 * 1024 * 1;//1MB
let chunks = slice(file, LENGTH);
// 获取对于同一个文件,获取其identifier
let identifier = createIdentifier(file);
let tasks = [];
chunks.forEach((chunk, index) => {
let fd = new FormData();
//传递file对象
fd.append("file",chunk);
// 传递identifier
fd.append("identifier", identifier);
// 传递切片索引值
fd.append("chunkNumber", index + 1);
// 传递切片总数
fd.append(“totalChunks”, chunks.length);
tasks.push(post("/mkblk.php", fd));
});
// 所有切片上传完毕后,调用mkfile接口
Promise.all(tasks).then(res => {
let fd = new FormData();
fd.append("identifier", identifier);
fd.append("totalChunks",chunks.length);
post("/mkfile.php", fd).then(res => {
console.log(res);
})
});
后端还原分片代码
// mkblk.php接口
$identifier = $_POST['identifier'];
$path = './upload/' . $identifier;
if(!is_dir($path)){
mkdir($path);
}
// 把同一个文件的切片放在相同的目录下
$filename = $path . '/' . $_POST['chunkNumber’];
// 清除保存的切片
$res = move_uploaded_file($_FILES['file']['tmp_name'], $filename);
//接下来是mkfile.php接口的实现,这个接口会在所有切片上传后调用用来合并文件
// mkfile.php接口
$identifier = $_POST['identifier'];
$totalChunks= (int)$_POST['totalChunks'];
//合并后的文件名
$filename = './upload/' . $identifier . '/file.jpg’;
// 开始合并文件
for($i = 1; $i <= $totalChunks; ++$i){
$file = './upload/'.$ identifier. '/' .$i; // 读取单个切块
// 获取文件内容
$content = file_get_contents($file);
if(!file_exists($filename)){
//创建一个用于读写的空文件
$fd = fopen($filename, "w+");
}else{
//追加到一个文件,写操作向文件末尾追加数据。如果文件不存在,则创建文件。
$fd = fopen($filename, "a");
}
fwrite($fd, $content);// 将切块合并到一个文件上
}
以上代码还需要继续优化的点:断点续传、秒传、上传进度和暂停
1、断点续传
为什么需要断点续传?
怎么实现断点续传?
由于整个上传过程是按切片维度进行的,且mkfile接口是在所有切片上传完成后由客户端主动调用的,因此断点续传的实现也十分简单:
前端断点续传代码
// 获取已上传切片记录
function getUploadSliceRecord(context){
let record = localStorage.getItem(context)
if(!record){
return []
}else {
return JSON.parse(record)
}
}
// 保存已上传切片
function saveUploadSliceRecord(context, sliceIndex){
let list = getUploadSliceRecord(context)
list.push(sliceIndex)
localStorage.setItem(context, JSON.stringify(list))
}
let context = createContext(file);
// 获取上传记录
let record = getUploadSliceRecord(context);
let tasks = [];
chunks.forEach((chunk, index) => {
// 已上传的切片则不再重新上传
if(record.includes(index)){
return
}
let fd = new FormData();
fd.append("file", chunk);
fd.append("context", context);
fd.append("chunk", index + 1);
let task = post("/mkblk.php", fd).then(res=>{
// 上传成功后保存已上传切片记录
saveUploadSliceRecord(context, index)
record.push(index)
})
tasks.push(task);
});
...
后端断点续传代码
服务端实现断点续传的逻辑基本相似,只要在getUploadSliceRecord内部调用服务端的查询接口获取已上传切片的记录即可,因此这里不再展开。
后端代码优化:清除切片的时机
2、秒传
什么是秒传?
3、上传进度和暂停
理解,首先 MCube 会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将目标页面展示到屏幕。
目前社区已经存在一些成熟的大文件上传解决方案,如七牛SDK,腾讯云SDK等,也许并不需要我们手动去实现一个简陋的大文件上传库,但是了解其原理还是十分有必要的。
推荐的前端vue组件:vue-simple-uploader,支持vue2,vue3
vue-simple-uploader是基于simple-Uploader.js封装的大文件上传组件,具有以下优点:
1. 支持单文件、多文件、文件夹上传;支持拖拽文件、文件夹上传
2. 可暂停、继续上传
3. 错误处理
4. 支持“秒传”,通过文件判断服务端是否已存在从而实现“秒传”
5. 分块上传
6. 支持进度、预估剩余时间、出错自动重试、重传等操作
vue-simple-uploader 内部的实现也很简单,有兴趣的同学可以去看一下源码
理解,首先 MCube 会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将目
本文首先介绍了什么是大文件,以及大文件跟普通文件在上传时的区别,最后通过分析大文件上传的原理和思路给出简单的实现方案,并且推荐了一个成熟的vue大文件上传组件:vue-simple-uploader,希望对大家有所帮助。