居于H5的多文件、大文件、多线程上传解决方案
一、 功能性需求与非功能性需求
- 要求操作便利,一次选择多个文件进行上传;
- 支持大文件上传(1G),同时需要保证上传期间用户电脑不出现卡死等体验;
- 交互友好,能够及时反馈上传的进度;
- 服务端的安全性,不因上传文件功能导致JVM内存溢出影响其他功能使用;
- 最大限度利用网络上行带宽,提高上传速度;
二、 设计分析
- 对于大文件的处理,无论是用户端还是服务端,如果一次性进行读取发送、接收都是不可取,很容易导致内存问题。所以对于大文件上传,采用切块分段上传
- 从上传的效率来看,利用多线程并发上传能够达到最大效率。
- 对于大文件切块、多线程上传,需要考虑服务端合并文件的时间点;
三、解决方案:
在HTML5之前的标准是无法支持上面的功能,因此我们需要把功能实现居于H5提供的新特性上面: 1. H5新标准对file标签进行了增强,支持同时选择多个文件
- <input type="file" multiple=true onchange="doSomething(this.files)"/>
- 1
复制代码
注意multiple属性,设置为true; onchange:一般是选择文件确定后的响应事件 this.files:文件对象集合 2. File对象 H5提供的类似java的RandomAccessFile的文件操作对象,其中silce方法允许程序指定文件的起止字节进行读取。利用这个对象,实现对大文件的切分; 3. XMLHttpRequest 这个对象大家应该很熟悉了,属于web2.0的标准,我们最常用的ajax请求底层就是居于此对象。本质上XMLHttpRequest是一个线程对象,因此我们通过创建一定数量的XMLHttpRequest对象,实现多线程并行操作; 4. FormData对象 H5新增对象,可以理解为一个key-value的map,通过把文件的二进制流和业务参数封装到此对象,再交由XMLHttpRequest对象发送到服务端,服务端可以通过普通的request.getParamter方法获取这些参数; 5. progress标签 H5新增的标签,在页面显示一个进度条: value:当前进度条的值 max:最大值 利用这个标签,结合XMLHttpRequest的回调来反馈目前上传的进度
四、客户端代码示例
- HTML代码:
<input type="file" multiple=true onchange="showFileList(this.files)"/>
<input id="uploadBtn" type="button" value="上传" onclick="doUpload()"/>
1
2
javascript脚本:
复制代码
var quence = new Array();//待上传的文件队列,包含切块的文件
/**
* 用户选择文件之后的响应函数,将文件信息展示在页面,同时对大文件的切块大小、块的起止进行计算、入列等
*/
function showFileList(files) {
if(!files) {
return;
}
var chunkSize = 5 * 1024 * 1024; //切块的阀值:5M
$(files).each(function(idx,e){
//展示文件列表,略......
if(e.size > chunkSize) {//文件大于阀值,进行切块
//切块发送
var chunks = Math.max(Math.floor(fileSize / chunkSize), 1)+1;//分割块数
for(var i=0 ; i<chunks; i++) {
var startIdx = i*chunkSize;//块的起始位置
var endIdx = startIdx+chunkSize;//块的结束位置
if(endIdx > fileSize) {
endIdx = fileSize;
}
var lastChunk = false;
if(i == (chunks-1)) {
lastChunk = true;
}
//封装成一个task,入列
var task = {
file:e,
uuid:uuid,//避免文件的重名导致服务端无法定位文件,需要给每个文件生产一个UUID
chunked:true,
startIdx:startIdx,
endIdx:endIdx,
currChunk:i,
totalChunk:chunks
}
quence.push(task);
}
} else {//文件小于阀值
var task = {
file:e,
uuid:uuid,
chunked:false
}
quence.push(task);
}
});
}
/**
* 上传器,绑定一个XMLHttpRequest对象,处理分配给其的上传任务
**/
function Uploader(name) {
this.url=""; //服务端处理url
this.req = new XMLHttpRequest();
this.tasks; //任务队列
this.taskIdx = 0; //当前处理的tasks的下标
this.name=name;
this.status=0; //状态,0:初始;1:所有任务成功;2:异常
//上传 动作
this.upload = function(uploader) {
this.req.responseType = "json";
//注册load事件(即一次异步请求收到服务端的响应)
this.req.addEventListener("load", function(){
//更新对应的进度条
progressUpdate(this.response.uuid, this.response.fileSize);
//从任务队列中取一个再次发送
var task = uploader.tasks[uploader.taskIdx];
if(task) {
console.log(uploader.name + ":当前执行的任务编号:" +uploader.taskIdx);
this.open("POST", uploader.url);
this.send(uploader.buildFormData(task));
uploader.taskIdx++;
} else {
console.log("处理完毕");
uploader.status=1;
}
});
//处理第一个
var task = this.tasks[this.taskIdx];
if(task) {
console.log(uploader.name + ":当前执行的任务编号:" +this.taskIdx);
this.req.open("POST", this.url);
this.req.send(this.buildFormData(task));
this.taskIdx++;
} else {
uploader.status=1;
}
}
//提交任务
this.submit = function(tasks) {
this.tasks = tasks;
}
//构造表单数据
this.buildFormData = function(task) {
var file = task.file;
var formData = new FormData();
formData.append("fileName", file.name);
formData.append("fileSize", file.size);
formData.append("uuid", task.uuid);
var chunked = task.chunked;
if(chunked) {//分块
formData.append("chunked", task.chunked);
formData.append("data", file.slice(task.startIdx, task.endIdx));//截取文件块
formData.append("currChunk", task.currChunk);
formData.append("totalChunk", task.totalChunk);
} else {
formData.append("data", file);
}
return formData;
}
}
/**
*用户点击“上传”按钮
*/
function doUpload() {
//创建4个Uploader上传器(4条线程)
var uploader0 = new Uploader("uploader0");
var task0 = new Array();
var uploader1 = new Uploader("uploader1");
var task1 = new Array();
var uploader2 = new Uploader("uploader2");
var task2 = new Array();
var uploader3 = new Uploader("uploader3");
var task3 = new Array();
//将文件列表取模hash,分配给4个上传器
for(var i=0 ; i<quence.length; i++) {
if(i%4==0) {
task0.push(quence[i]);
} else if(i%4==1) {
task1.push(quence[i]);
} else if(i%4==2) {
task2.push(quence[i]);
} else if(i%4==3) {
task3.push(quence[i]);
}
}
/提交任务,启动线程上传
uploader0.submit(task0);
uploader0.upload(uploader0);
uploader1.submit(task1);
uploader1.upload(uploader1);
uploader2.submit(task2);
uploader2.upload(uploader2);
uploader3.submit(task3);
uploader3.upload(uploader3);
//注册一个定时任务,每2秒监控文件是否都上传完毕
uploadCompleteMonitor = setInterval("uploadComplete()",2000);
}
五、服务端处理:
服务端处理逻辑相对比较传统,利用输入输出流、NIO等把文件写到磁盘即可。 这里需要特别考虑的是关于被切块文件的合并。前端在上传的时候,文件块是无序到达服务端,因此我们在每次接收到一个文件块的时候需要判断被切块的文件是否都传输完毕并进行合并,思路如下: 回到前端,我们在构造被切块的文件formData的数据结构:
formData.append("fileName", file.name);
formData.append("fileSize", file.size);
formData.append("uuid", task.uuid);
formData.append("chunked", task.chunked);
formData.append("data", file.slice(task.startIdx, task.endIdx));//截取文件块
formData.append("currChunk", task.currChunk);
formData.append("totalChunk", task.totalChunk);
fileName:文件的原始名字 fileSize:文件的大小,KB uuid:文件的uuid chunked:true,标识是分段上传的文件块 data:文件二进制流 currChunk:当前上传的块编号 totalChunk:总块数
服务端以文件的UUID为key,维护一个chunk计数器,每接收到一块就找到对应的uuid执行计数器+1,同时考虑到并发情况,需采用同步关键字,避免出现逻辑错误。当计数器等于totalChunk的时候,进行文件合并
六、运行效果
- 腾讯移动安全实验室发布《2013年手机安全报告》
- 自定义Unity 容器的扩展 --- Unity Application Block Event Broker
- LINQ to SQL集成到应用程序中需考虑的一些问题
- WCF的追踪分析工具——SvcPerf
- 解决七牛云存储缓存加速Gravatar 头像图片路径url 参数失效的问题
- 命令行解析的规则以及Command Line Parser Library
- 简单代码让WordPress 支持电子邮箱(Email)作为登录名
- .NET Migration工具
- 如何有效监控.NET 应用程序
- 写入Ring Buffer
- Enterprise Library 4 缓存快速入门
- Enterprise Library 4 缓存应用程序块的设计
- 让WordPress 在RSS 中Feed 输出支持“More”标签
- WordPress文章版权保护:复制文字自动添加版权信息
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- Oracle私网mtu滚动修改实施方案
- 如何使用GOLDENGATE构建数据库的审计表之一
- [java][Servlet]Servlet 简介-Servlet 到 Spring MVC 的简化之路-Servlet/Tomcat/ Spring 之间的关系
- oracle19c安装脚本.
- GOLDENGATE EXTRACT在DATABASE SWITCHOVER后表现以及处理方案
- JAVA中的23种设计模式(GOF)
- 机器学习第3天:预测汽车的燃油效率
- Oracle GoldenGate 19 Microservices安装、配置以及简单数据同步
- 最小覆盖子串
- 通配符匹配
- LaTeX多行注释
- [886]mysql查询以某个字符开头
- 不同路径问题
- 信号量及其应用
- 岛屿问题