点击上面的“Java基地”,选择“设为明星”
做一个积极向上的人,而不是一个积极向上的废物人!
源码精品专栏
最近遇到一个上传非常大文件的需求,考察了七牛和腾讯云的分段上传功能,所以这里就实现了后端大文件上传相关功能。
在个体业务中,上传大文件是一个比较重要的交互场景,比如上传比较大的Excel表格数据到库、上传音视频文件等等。 如果文件体积比较大,或者网络状况不好,上传时间会较长(需要传输的数据包较多,丢包重传的概率也较大),用户无法刷新页面,只能耐心等待请求完成。
我们先从文件上传方法开始,梳理一下上传大文件的思路,并给出相关示例代码。 由于PHP对外有方便的文件分割和拼接方式,因此服务器代码采用PHP进行示例编译。
本文相关示例代码位于github上,主要参考
谈谈大文件上传
剪切并上传大文件
文件上传的几种形式
首先我们来看看文件上传的几种形式。
使用PHP来显示常规表单上传是一个不错的选择。 首先创建一个用于文件上传的表单,并指定表单的提交内容类型为enctype="multipart/form-data",表示该表单需要上传二进制数据。
图片
然后编译index.php上传文件接收代码,使用move_uploaded_file方法(php大法不错...)
当以表单上传大文件时,很容易遇到服务器超时的问题。 通过xhr,前端还可以异步上传文件。 一般来说,有两种想法。
第一个想法是对文件进行编码,然后在服务器端对其进行解码。 之前写过一篇关于后端图片压缩和上传的博客。 主要实现原理是将图像转为base64进行传输。
var imgURL = URL.createObjectURL(file);
ctx.drawImage(imgURL, 0, 0);
// 获取图片的编码,然后将图片当做是一个很长的字符串进行传递
vardata= canvas.toDataURL( "image/jpeg", 0.5);
在服务端需要做的事情也比较简单,首先解码base64,然后保存图片即可
$imgData = $_REQUEST[ 'imgData'];
$base64 = explode( ',', $imgData)[ 1];
$img = base64_decode($base64);
$url = './test.jpg';
if(file_put_contents($url, $img)) {
exit(json_encode( array(
url => $url
)));
}
Base64编码的缺点是其体积比原图片要大(因为Base64将三个字节转换为四个字节,所以编码后的文本会比原文本大三分之一左右)php 上传大文件,对于体积大的文件来说,上传并且解析时间将显着减少。
关于base64的更多信息,请参考Base64注释。
除了base64编码之外,还可以直接在后端读取文件内容并以二进制格式上传
// 读取二进制文件
functionreadBinary(text){
vardata = newArrayBuffer(text.length);
varui8a = newUint8Array(data, 0);
for( vari = 0; i < text.length; i++){
ui8a[i] = (text.charCodeAt(i) & 0xff);
}
console.log(ui8a)
}
varreader = newFileReader;
reader. = function{
readBinary( this.result) // 读取result或直接上传
}
// 把从input里读取的文件内容,放到fileReader的result字段里
reader.readAsBinaryString(file);
formData异步上传
FormData对象主要用于组装一组用于发送请求的键/值对,可以更灵活地发送Ajax请求。 您可以使用FormData来模拟表单提交。
letfiles = e.target.files // 获取input的file对象
letformData = newFormData;
formData.append( 'file', file);
axios.post(url, formData);
服务端处理方式与直接form表单请求基本相同。
iframe无刷新页面
在低版本的浏览器(如IE)上,xhr是不支持直接上传formdata的,因此只能用form来上传文件,而form提交本身会进行页面跳转,这是因为form表单的target属性导致的,其取值有
_self,默认值,在相同的窗口中打开响应页面
_blank,在新窗口打开
_parent,在父窗口打开
_top,在最顶层的窗口打开
framename,在指定名字的iframe中打开
如果需要让用户体验异步上传文件的感觉,可以通过framename指定一个iframe。 将表单的target属性设置为不可见的iframe,那么返回的数据就会被iframe接受,所以只会刷新iframe,通过解析iframe中的文本也可以得到返回的结果。
functionupload{
varnow = + newDate
varid = 'frame'+ now
$( "body").append( `<iframe style="display:none;" name="${id}" id="${id}" />`);
var$form = $( "#myForm")
$form.attr({
"action": '/index.php',
"method": "post",
"enctype": "multipart/form-data",
"encoding": "multipart/form-data",
"target": id
}).submit
$( "#"+id).on( "load", function{
varcontent = $( this).contents.find( "body").text
try{
vardata = JSON.parse(content)
} catch(e){
console.log(e)
}
})
}
现在我们就来看看前面提到的上传表单中实现大文件上传时遇到的超时问题。
表单上传和iframe页面无刷新上传实际上是通过form标签上传文件。 在该方法中,整个请求完全交给浏览器处理。 当上传大文件时,请求可能会超时。
通过fromData,实际上是在xhr中封装了一组请求参数,用于模拟表单请求,并不能防止大文件上传超时的问题
编码上传php 上传大文件,我们可以灵活控制上传的内容
上传大文件的主要问题是,在同一个请求中,需要上传大量数据,导致流程较长,并且上传失败后需要重新启动。 试想一下,如果我们把这个请求拆分成多个请求,每个请求的时间就会缩短,而且如果一个请求失败,我们只需要重新发送这个请求,而不用从头开始。 文件上传问题怎么办?
根据之前的问题,看来大文件的上传需要满足以下要求
支持拆分上传请求(即分块)
支持断点续传
支持显示上传进度和暂停上传
接下来我们依次实现一下这个功能。 看来最重要的功能应该是切割了。
参考:剪切并上传大文件
在编码方式上传中,我们只需要在后端获取文件的二进制内容,然后将内容进行分割,最后将每一块上传到服务器。
在Java中,文件FIle对象是Blob对象的泛型类型,而Blob对象包含一个重要的方法slice,通过它我们可以对二进制文件进行分割。
以下是分割文件的示例。 对于up6来说,开发者不需要关心分裂的细节。 通过控件来实现,开发者只需要关心业务逻辑。
图片
控件上传时,会在每个文件块数据中添加相关信息,开发者在服务器端收到数据后可以自行处理数据。
图片
服务器收到这种切割后,就可以将它们拼接在一起。 以下是PHP拼接和裁剪的示例代码
对于up6来说,开发者不需要拼接,up6已经提供了示例代码,已经实现了这个逻辑。
图片
为了保证唯一性,控件会为每个文件块添加信息,如块索引、块MD5、文件MD5
up6有自己的续传功能,up6已经在服务器端保存了文件信息,在客户端也保存了文件进度信息。 上传时,控件会手动加载文件进度信息,开发者无需关心这些细节。 在文件块的处理逻辑中,只需要根据文件块索引来识别即可。
此时,上传时请刷新页面或关闭浏览器。 再次上传同一文件时,之前成功上传的切片将不会重新上传。
服务端续传上传的逻辑基本类似,只要在getUploadSliceRecord内部调用服务端的查询socket来获取上传切片的记录即可,这里不再展开。
另外,还需要考虑过期的chunk:如果调用mkfile接口,可以将c盘上的chunk的内容删除。 如果客户端从来不调用mkfile接口,那么chunk仍然保存在c盘,其实是不可靠的。 一般来说,切片上传是有有效期的。 过了有效期,它们将被删除。 基于以上原因,断点续传还必须同步区块过期的实现逻辑。
图片
通过xhr.upload中的progress方法,可以监控每个区块的上传进度。
上传暂停的实现也比较简单。 通过xhr.abort,可以取消当前未完成的上传块的上传,实现上传暂停的疗效。 恢复上传与恢复上传类似。 首先获取已上传的块列表,然后重新启动发送未上传的块。
由于篇幅限制,这里不会实现上传进度和暂停的功能。
达到疗效:
图片
目前社区中已经存在一些成熟的大文件上传解决方案,比如七牛SDK、腾讯云SDK等。也许我们不需要自动实现一个小而小的大文件上传库,但是还是有必要的了解其原理。
本文首先梳理了后端文件上传的几种形式,然后讨论了大文件上传的几种场景,以及大文件上传需要实现的几个功能
通过Blob对象的slice方法将文件切分成块
整理了服务器端恢复文件所需的条件和参数,并演示了PHP如何将块恢复到文件
通过保存上传块的记录从断点恢复上传
还剩下一些问题,比如:合并文件时防止内存溢出、切片失效策略、上传进度暂停等功能,还没有深入或者一一实现,继续学习
欢迎加入我的知识星球,一起讲解结构、交流源码。 如何加入,长按下方二维码:
知识星球已更新源码,分析如下:
最近更新的系列《太郎SpringBoot 2.X入门》有20多篇文章,内容涉及MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo 、RabbitMQ、RocketMQ、Kafka、性能测试等
提供了一个近3W行代码的SpringBoot示例,以及一个超过4W行代码的电商微服务项目。