在HTTP协议下,上传大文件可能会遇到很多问题;
最近,在开发中就遇到了很多挫折;
最开始的需求是对 500M及以上的文件 进行上传;
1 直接上传大文件
直接上传大文件,网络请求时间太长,超过了项目中规定的超时时间 —— 30s;
并且文件向服务器上传的过程中,文件流是存在于内存中,很有可能造成 内存溢出;
因此该方案 失败。
2 分片上传大文件
由于文件size过大,最好的方法就是 分片上传 ,将大文件分割成多个固定大小的块,然后逐个上传;
<input type="file" />
<script>
cont inp = documnet.querySelector('input');
inp.onChange = async (e) => {
cont file = inp.files[0];
if (!file) {
return;
}
file.slice(0, 100); // 0-99个字节
console.log(file);
// 得到一个blob对象 Blob {size: 100, type: ''}
// blob对象可以直接利用ajax发送请求;
const chunks = createChunks(file, 10 * 1024 * 1024);
const result = await hash(chunks); // 拿到hash计算结果
console.log(result); // 得到hash值
}
function createChunks(file, chunkSize) {
const result = [];
for (let i = 0; i < file.size; i += chunkSize) {
result.push(file.slice(i, i + chunkSize));
}
return result; // Blob对象的集合,Blob对象是可以直接ajax请求发到服务器的
}
</script>
这里可以使用FormData函数来增加文件信息;
const formData = new FormData();
formData.append('file', file, file.name); // 'file' 是后端接收文件数据的字段名
formData.append('fieldName', 'value'); // 添加其他数据
fetch('upload_endpoint', { // 替换为你的上传端点
method: 'POST',
body: formData
})
.then(response => response.json()) // 假设后端返回JSON格式的响应
.then(data => {
console.log(data);
})
.catch(error => {
console.error('Error:', error);
});
断点上传需要去问服务端,这个文件,我还需要传递哪些分片;
// 如何告诉服务端是哪个文件?
// 文件名不合适,会重复;文件路径不合适;
// 合理的做法是采用hash值
// 增量算法,计算hash值;
// 先用一块数据计算结果,计算出来
function hash(chunks) {
return new Promise((resolve) => {
const spark = new SparkMD5();
function _read(i) {
if (i >= chunks.length) {
console.log(spark.end()); // 文件的hash值计算完毕
resolve(spark.end());
return; // 读取完成
}
const blob = chunks[i];
const reader = new FileReader(); // 读字节
reader.onload = e => {
const bytes = e.target.result; // 读取到的字节数组
spark.append(bytes);
_read(i + 1);
}
reader.readAsArrayBuffer(blob);
// 读ArrayBuffer的字节,读取过程是异步的。所以有onload事件
}
_read(0);
});
}
上传过程中,每个块都会被独立地上传到服务器,并且在服务器端进行合并;如此减少了单个请求的数据量,降低了上传过程中的网络传输负担,同时通过并发请求合理利用了网络带宽资源。
(插入一段冷知识 -^-)
谷歌浏览器从 59版本 开始限制并发请求数为6,超过这个数量,浏览器会等待之前的请求完成后再发起新的请求;
但是在分片上传的时候遇到了 “token过期的问题”;
- 项目规定token在10min之后过期;将10min分为两段,前5分钟调用接口,返回一个token;后5min调用接口,返回一个新的token;
- 拦截器拦截到新的token,会在下次调用接口的时候,携带该token;
- 服务器端会验证token是否合法,来判断请求是否合法;(很明显,无论何时,都会有两个token是合法的)
举例:将大文件分为“50”片;由于分片是在文件添加好之后一次性下发,因此调用上传接口时的request header中的 Token都是相同的;可能会出现 Token 过期的情况;
并且同时下发50个上传请求,会占用全部的网络资源,如果上传不能及时完成,会阻塞其他功能的使用。
3 控制并发请求数
参考之前写的一片文章 前端如何控制并发请求数量
将大文件分成 50片,并且同时只能下发 3个请求,当其中一个请求结束,下一个请求接上;
如此,文件下发时,可以携带最新的Token,解决了 Token过期的问题;并且并发请求3个,解决了阻塞其他请求的问题。
4 断点续传
以上3个步骤已经初步实现大文件上传了;但一旦出现断网,网络不稳定的情况,就是灾难了,需要重新添加文件,重新上传;
为了解决这个问题,采用“断点续传”功能;将50个分片请求分别编号;失败的请求放置在一个“失败数组”中;在50个请求发送完成后,接着重新发送失败的请求;不断重试,直到全部发送完成。
总结一下
实现分片上传,通常要考虑的因素有以下几个方面:
- 块大小选择:选择适当的块大小对于分片上传的效率和性能至关重要;如果块太小,会增加上传请求的数量和服务器端的处理开销;如果块太大,可能会导致上传过程中的超时或内存限制问题。一般来说,块大小应该在几百KB到几MB之间;
- 块编号和顺序:为了在服务器端正确地合并块,每个块都应该有一个唯一的编号,并按照正确的顺序上传。可以在上传前将块编号和总块数等信息发送到服务器端,以便服务器端正确地组装文件;
- 断点续传:分片上传也可以支持断点续传功能,即在上传过程中出现中断或失败时,可以从上次上传的位置继续上传。为了实现断点续传,服务器端需要保存已上传的块信息,并在继续上传时进行验证和处理。
对于分片上传的处理:
- 前端负责将文件切分成多个小片段:使用 JavaScript 的 File API,可以将大文件分割成多个小的 Blob 对象或者ArrayBuffer 对象;
- 前端上传分片:通过 AJAX 或者 Fetch API 发送每个分片到服务器。可以使用 XMLHttpRequest 或者 fetch 函数来发送异步请求;
- 服务器端接收分片:服务器端需要接收每个分片,并将其存储在临时位置或者内存中;
- 服务器端验证和合并分片:当所有分片都上传完成后,服务器端需要验证每个分片的完整性和正确顺序。如果所有分片都齐全,可以将它们合并成完整的文件;
- 完成上传:一旦文件合并完成,服务器可以将文件保存到永久存储位置,并返回上传成功的响应给前端;
- 错误处理和重试:在上传过程中,需要处理可能出现的错误,例如网络中断、分片丢失等情况。可以通过重试机制来处理上传失败的分片。

4167

被折叠的 条评论
为什么被折叠?



