背景:文件上传下载对于在互联网开发中是一个比较常见的功能,本文将分别对上传和下载进行阐述,只关心核心功能,其他边缘功能不在叙述,每个功能采用演变的思想,给出几种方案,当然每种方案都能完成需求,希望大家根据项目的需求以及上传文件大小挑选出适合自己的方案。
1 文件上传
以下所有上传方案只针对单文件上传做优化,通过简单的封装和多线程的改写,可以支持多线程上传。另外在上传过程中边缘功能,例如文件大小限制,格式判断,文件摘要,进度条等不在此次方案中赘述。
1> 文件上传精简版
本方案是所有开发者首先想到的方案,也是不愿意折腾的方案,适合小文件上传,具体流程为:
1 前端发送文件流 2 后端顺序接受 3 操作文件流保存为文件
前端表单
<form action="http://localhost:8081/upload" method="post" enctype="multipart/form-data" accept-charset="utf-8">
<input type="file" name="file" value="选择文件"/>
<input id="submit_form" type="submit" value="提交"/>
</form>
备注:accept-chaset主要是为了解决中文文件名称乱码问题。
后端代码如下:
@Override
public ServiceResult upload(HttpServletRequest req) {
// TODO Auto-generated method stub
// TODO Auto-generated method stub
ServiceResult result = null;
try {
/*
* CommonsMultipartResolver resolver = new
* CommonsMultipartResolver(req.getSession().getServletContext());
* resolver.setDefaultEncoding("utf-8");
*/
MultipartHttpServletRequest params = ((MultipartHttpServletRequest) req);
List<MultipartFile> files = params.getFiles("file"); // 多文件的上传
if (files != null && files.size() > 0) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
long time = System.currentTimeMillis();
String prefix = "logs" + File.separator;
String fullPath = uploadPath + prefix;
File file = new File(fullPath);
if (!file.exists()) {
file.mkdirs();
}
// 某一个上传错误,全部中断
List<String> fileNames = new ArrayList<String>();
for (MultipartFile multipartFile : files) {
if (multipartFile != null) {
String fileName = multipartFile.getOriginalFilename();
multipartFile.transferTo(new File(fullPath + fileName));
fileNames.add(fileName);
}
}
result = new ServiceResult<>();
result.setData(fileNames);
} else {
result = new ServiceResult(HttpResultEnum.CLIENT_UNABLE_OBTAIN);
}
} catch (Exception e) {
e.printStackTrace();
result = new ServiceResult(HttpResultEnum.FAIL);
}
return result;
}
备注:使用springmvc 上传,支持多文件上传。
2> 分块上传
当文件比较大,需要严格控制上传的时间时,那么我们就需要充分利用网络资源,压榨服务器性能。回想上一个方案中,上传文件作为一个整体被发送到服务端,服务端也按照顺序一个字节一个字节的读取,按照目前的服务器配置大多都是双cpu,多核心,那么我们可以同时让其处理一个文件的多个部分,从而缩短上传时间。
分块上传需要前后端配合完成,对于分块上传前端的编程思路为:
1 确定分块的大小 2 异步的发送数据块(根据情况,不要并发太多,3-5个足以)
前端js分段代码
<script>
function Upload() {
var files = document.getElementById('myFile').files;
var file = files[0];
var totalSize = file.size;//文件大小
var blockSize = 1024 * 1024 * 2;//块大小
var count = Math.ceil(totalSize / blockSize);//总块数
for(var i =0;i<count;i++){
//创建FormData对象
var formData = new FormData();
formData.append('fileName', file.name);//文件名
formData.append('total', blockCount);//总块数
formData.append('index', index);//当前上传的块下标
var start = index * blockSize;
var end = Math.min(totalSize, start + blockSize);
var block = file.slice(start, end);
formData.set('data', block);
$.ajax({
url: 'localhost/upload',
type: 'post',
data: formData,
processData: false,
contentType: false,
success: function (res) {
}
});
}
}
</script>
备注:前端计算好每块的md5值,用md5+index作为文件名,为断点续传做准备。
后端处理的步骤为:
1 处理每块上传的数据 2 把每块上传的信息保存到数据库 3 每次上传完成后,查看当前所有分块是否上传完成,当上传完成后进行合并。
代码如下:
MultipartHttpServletRequest params = ((MultipartHttpServletRequest) req);
//获取所有前端信息
List<MultipartFile> files = params.getFiles("file"); // 多文件的上传
String oleFileName = params.getParameter("oleFileName"); //文件名称md5
String fileName = params.getParameter("fileName"); //文件名称md5
String total = params.getParameter("total"); //总共有几块
String index = params.getParameter("index"); //当前是第几块
//保存块文件
multipartFile.transferTo(new File(fullPath + fileName));
//把块相关信息插入数据库
saveInfo(FileChunkForm);
//判断是否已经上传完成,当所有的都上传完成进行合并
if(checkFinish(oleFileName)==Integer.parseInt(total)){
mergeFile(oleFileName);
}
3> 断点续传
此方案是一个优化的策略,可以和前两个方案结合,按照字面意思理解断点续传,最重要的是要断点在哪,所以就需要服务端做记录,保存上次客户端上传之前先获取该文件在服务端的状态。 web前端可以选择插件类似WebUploader等完成断点续传,也可以采用分块的思想自行编写代码。
private void upload(long[] startPos, long[] endPos, File tmpfile) {
RandomAccessFile rantmpfile = null;
try {
if (tmpfile.exists()) {
rantmpfile = new RandomAccessFile(tmpfile, "rw");
for (int i = 0; i < threadNum; i++) {
rantmpfile.seek(8 * i + 8);
startPos[i] = rantmpfile.readLong();
rantmpfile.seek(8 * (i + 1000) + 16);
endPos[i] = rantmpfile.readLong();
}
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (rantmpfile != null) {
rantmpfile.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
备注:方案1中改造后端主要通过RandomAccessFile 完成断点续传,方案2中因为已经拆分为块,续传只需要控制到块级别就行就行(已经完成的块不在上传,没有完成的块重头开始上传)。
2 文件下载
未完待续