c#⼤⽂件上传详解及实例代码
最近遇见⼀个需要上传百兆⼤⽂件的需求,调研了七⽜和腾讯云的切⽚分段上传功能,因此在此整理前端⼤⽂件上传相关功能的实现。
在某些业务中,⼤⽂件上传是⼀个⽐较重要的交互场景,如上传⼊库⽐较⼤的Excel表格数据、上传影⾳⽂件等。如果⽂件体积⽐较⼤,或者⽹络条件不好时,上传的时间会⽐较长(要传输更多的报⽂,丢包重传的概率也更⼤),⽤户不能刷新页⾯,只能耐⼼等待请求完成。
下⾯从⽂件上传⽅式⼊⼿,整理⼤⽂件上传的思路,并给出了相关实例代码,由于PHP内置了⽐较⽅便的⽂件拆分和拼接⽅法,因此服务端代码使⽤PHP进⾏⽰例编写。
本⽂相关⽰例代码位于github上,主要参考
聊聊⼤⽂件上传
⼤⽂件切割上传
⽂件上传的⼏种⽅式
⾸先我们来看看⽂件上传的⼏种⽅式。
普通表单上传
使⽤PHP来展⽰常规的表单上传是⼀个不错的选择。⾸先构建⽂件上传的表单,并指定表单的提交内容类型为enctype="multipart/form-data",表明表单需要上传⼆进制数据。
然后编写index.php上传⽂件接收代码,使⽤move_uploaded_file⽅法即可(php⼤法好…)
form表单上传⼤⽂件时,很容易遇见服务器超时的问题。通过xhr,前端也可以进⾏异步上传⽂件的操作,⼀般由两个思路。
⽂件编码上传
第⼀个思路是将⽂件进⾏编码,然后在服务端进⾏解码,之前写过⼀篇在前端实现图⽚压缩上传的博客,其主要实现原理就是将图⽚转换成base64进⾏传递
varimgURL = ateObjectURL(file);
ctx.drawImage(imgURL, 0, 0);
// 获取图⽚的编码,然后将图⽚当做是⼀个很长的字符串进⾏传递
vardata= DataURL( "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将三个字节转化成四个字节,因此编码后的⽂本,会⽐原⽂本⼤出三分之⼀左右),对于体积很⼤的⽂件来说,上传和解析的时间会明显增加。
更多关于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( sult) // 读取result或直接上传
}
// 把从input⾥读取的⽂件内容,放到fileReader的result字段⾥
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来实现。把form的target属性设置为⼀个看不见的iframe,那么返回的数据就会被这个iframe接受,因此只有该iframe会被刷新,⾄于返回结果,也可以通过解析这个iframe内的⽂本来获取。
functionupload{
varnow = + newDate
varid = 'frame'+ now
$( "body").append( `<iframe 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中封装⼀组请求参数,⽤来模拟表单请求,⽆法避免⼤⽂件上传超时的问题
编码上传,我们可以⽐较灵活地控制上传的内容
⼤⽂件上传最主要的问题就在于:在同⼀个请求中,要上传⼤量的数据,导致整个过程会⽐较漫长,且失败后需要重头开始上传。试想,如果我们将这个请求拆分成多个请求,每个请求的时间就会缩短,且如果某个请求失败,只需要重新发送这⼀次请求即可,⽆需从头开始,这样是否可以解决⼤⽂件上传的问题呢?
综合上⾯的问题,看来⼤⽂件上传需要实现下⾯⼏个需求
⽀持拆分上传请求(即切⽚)
⽀持断点续传
⽀持显⽰上传进度和暂停上传
接下来让我们依次实现这些功能,看起来最主要的功能应该就是切⽚了。
⽂件切⽚
参考:⼤⽂件切割上传
编码⽅式上传中,在前端我们只要先获取⽂件的⼆进制内容,然后对其内容进⾏拆分,最后将每个切⽚上传到服务端即可。
在Java中,⽂件FIle对象是Blob对象的⼦类,Blob对象包含⼀个重要的⽅法slice,通过这个⽅法,我们就可以对⼆进制⽂件进⾏拆分。
下⾯是⼀个拆分⽂件的⽰例,对于up6来说开发者不需要关⼼拆分的细节,由控件帮助实现,开发者只需要关⼼业务逻辑即可。
控件上传的时候会为每⼀个⽂件块数据添加相关的信息,开发者在服务端接收到数据后可以⾃已进⾏处理。
服务器接收到这些切⽚后,再将他们拼接起来就可以了,下⾯是PHP拼接切⽚的⽰例代码
对于up6来说,开发⼈员不需要进⾏拼接,up6已经提供了⽰例代码,已经实现了这个逻辑。
保证唯⼀性,控件会为每⼀个⽂件块添加信息,如块索引,块MD5,⽂件MD5
断点续传
up6⾃带续传功能,up6在服务端已经保存了⽂件的信息,在客户端也保存了⽂件的进度信息。在上传时控件会⾃动加载⽂件进度信息,开发者不需要关⼼这些细节。在⽂件块的处理逻辑中只需要根据⽂件块索引来识别即可。
此时上传时刷新页⾯或者关闭浏览器,再次上传相同⽂件时,之前已经上传成功的切⽚就不会再重新上传了。
服务端实现断点续传的逻辑基本相似,只要在getUploadSliceRecord内部调⽤服务端的查询接⼝获取已上传切⽚的记录即可,因此这⾥不再展开。
此外断点续传还需要考虑切⽚过期的情况:如果调⽤了mkfile接⼝,则磁盘上的切⽚内容就可以清除掉了,如果客户端⼀直不调⽤mkfile的接⼝,放任这些切⽚⼀直保存在磁盘显然是不可靠的,⼀般情况下,
切⽚上传都有⼀段时间的有效期,超过该有效期,就会被清除掉。基于上述原因,断点续传也必须同步切⽚过期的实现逻辑。
续传效果