文件上传在web应用中是比较常见的功能,前段时间做了一个多文件、大文件、多线程文件上传的功能,使用效果还不错,总结分享下。

一、 功能性需求与非功能性需求

  • 要求操作便利,一次选择多个文件进行上传;

  • 支持大文件上传(1G),同时需要保证上传期间用户电脑不出现卡死等体验;

  • 交互友好,能够及时反馈上传的进度;

  • 服务端的安全性,不因上传文件功能导致JVM内存溢出影响其他功能使用;

  • 最大限度利用网络上行带宽,提高上传速度;

二、 设计分析

  • 对于大文件的处理,无论是用户端还是服务端,如果一次性进行读取发送、接收都是不可取,很容易导致内存问题。所以对于大文件上传,采用切块分段上传

  • 从上传的效率来看,利用多线程并发上传能够达到最大效率。

  • 对于大文件切块、多线程上传,需要考虑服务端合并文件的时间点;

三、解决方案:

在HTML5之前的标准是无法支持上面的功能,因此我们需要把功能实现居于H5提供的新特性上面:

1. H5新标准对file标签进行了增强,支持同时选择多个文件

  1. <input type="file" multiple=true onchange="doSomething(this.files)"/>

  2. 1

注意multiple属性,设置为true;

onchange:一般是选择文件确定后的响应事件

this.files:文件对象集合

2. File对象

H5提供的类似java的RandomAccessFile的文件操作对象,其中silce方法允许程序指定文件的起止字节进行读取。利用这个对象,实现对大文件的切分;

3.

这个对象大家应该很熟悉了,属于web2.0的标准,我们最常用的ajax请求底层就是居于此对象。本质上是一个线程对象,因此我们通过创建一定数量的对象,实现多线程并行操作;

4. FormData对象

H5新增对象,可以理解为一个key-value的map,通过把文件的二进制流和业务参数封装到此对象,再交由对象发送到服务端,服务端可以通过普通的request.getParamter方法获取这些参数;

5. progress标签

H5新增的标签,在页面显示一个进度条:

value:当前进度条的值

max:最大值

利用这个标签,结合的回调来反馈目前上传的进度

四、客户端代码示例

  • HTML代码:

    1. <input type="file" multiple=true onchange="showFileList(this.files)"/>

    2. <input id="uploadBtn" type="button" value="上传" onclick="doUpload()"/>

    3. 1

    4. 2

  • java脚本:

    1. var quence = new Array();//待上传的文件队列,包含切块的文件

    2. /**

    3. * 用户选择文件之后的响应函数,将文件信息展示在页面,同时对大文件的切块大小、块的起止进行计算、入列等

    4. */

    5. function showFileList(files) {

    6. if(!files) {

    7. return;

    8. }

    9. var chunkSize = 5 * 1024 * 1024; //切块的阀值:5M

    10. $(files).each(function(idx,e){

    11. //展示文件列表,略......

    12. if(e.size > chunkSize) {//文件大于阀值,进行切块

    13. //切块发送

    14. var chunks = Math.max(Math.floor(fileSize / chunkSize), 1)+1;//分割块数

    15. for(var i=0 ; i<chunks; i++) {

    16. var startIdx = i*chunkSize;//块的起始位置

    17. var endIdx = startIdx+chunkSize;//块的结束位置

    18. if(endIdx > fileSize) {

    19. endIdx = fileSize;

    20. }

    21. var lastChunk = false;

    22. if(i == (chunks-1)) {

    23. lastChunk = true;

    24. }

    25. //封装成一个task,入列

    26. var task = {

    27. file:e,

    28. uuid:uuid,//避免文件的重名导致服务端无法定位文件,需要给每个文件生产一个UUID

    29. chunked:true,

    30. startIdx:startIdx,

    31. endIdx:endIdx,

    32. currChunk:i,

    33. totalChunk:chunks

    34. }

    35. quence.push(task);

    36. }

    37. } else {//文件小于阀值

    38. var task = {

    39. file:e,

    40. uuid:uuid,

    41. chunked:false

    42. }

    43. quence.push(task);

    44. }

    45. });

    46. }

    47. /**

    48. * 上传器,绑定一个对象,处理分配给其的上传任务

    49. **/

    50. function Uploader(name) {

    51. this.url=""; //服务端处理url

    52. this.req = new ();

    53. this.tasks; //任务队列

    54. this.taskIdx = 0; //当前处理的tasks的下标

    55. this.name=name;

    56. this.status=0; //状态,0:初始;1:所有任务成功;2:异常

    57. //上传 动作

    58. this.upload = function(uploader) {

    59. this.req.responseType = "json";

    60. //注册load事件(即一次异步请求收到服务端的响应)

    61. this.req.addEventListener("load", function(){

    62. //更新对应的进度条

    63. progressUpdate(this.response.uuid, this.response.fileSize);

    64. //从任务队列中取一个再次发送

    65. var task = uploader.tasks[uploader.taskIdx];

    66. if(task) {

    67. console.log(uploader.name + ":当前执行的任务编号:" +uploader.taskIdx);

    68. this.open("POST", uploader.url);

    69. this.send(uploader.buildFormData(task));

    70. uploader.taskIdx++;

    71. } else {

    72. console.log("处理完毕");

    73. uploader.status=1;

    74. }

    75. });

    76. //处理第一个

    77. var task = this.tasks[this.taskIdx];

    78. if(task) {

    79. console.log(uploader.name + ":当前执行的任务编号:" +this.taskIdx);

    80. this.req.open("POST", this.url);

    81. this.req.send(this.buildFormData(task));

    82. this.taskIdx++;

    83. } else {

    84. uploader.status=1;

    85. }

    86. }

    87. //提交任务

    88. this.submit = function(tasks) {

    89. this.tasks = tasks;

    90. }

    91. //构造表单数据

    92. this.buildFormData = function(task) {

    93. var file = task.file;

    94. var formData = new FormData();

    95. formData.append("fileName", file.name);

    96. formData.append("fileSize", file.size);

    97. formData.append("uuid", task.uuid);

    98. var chunked = task.chunked;

    99. if(chunked) {//分块

    100. formData.append("chunked", task.chunked);

    101. formData.append("data", file.slice(task.startIdx, task.endIdx));//截取文件块

    102. formData.append("currChunk", task.currChunk);

    103. formData.append("totalChunk", task.totalChunk);

    104. } else {

    105. formData.append("data", file);

    106. }

    107. return formData;

    108. }

    109. }

    110. /**

    111. *用户点击“上传”按钮

    112. */

    113. function doUpload() {

    114. //创建4个Uploader上传器(4条线程)

    115. var uploader0 = new Uploader("uploader0");

    116. var task0 = new Array();

    117. var uploader1 = new Uploader("uploader1");

    118. var task1 = new Array();

    119. var uploader2 = new Uploader("uploader2");

    120. var task2 = new Array();

    121. var uploader3 = new Uploader("uploader3");

    122. var task3 = new Array();

    123. //将文件列表取模hash,分配给4个上传器

    124. for(var i=0 ; i<quence.length; i++) {

    125. if(i%4==0) {

    126. task0.push(quence[i]);

    127. } else if(i%4==1) {

    128. task1.push(quence[i]);

    129. } else if(i%4==2) {

    130. task2.push(quence[i]);

    131. } else if(i%4==3) {

    132. task3.push(quence[i]);

    133. }

    134. }

    135. /提交任务,启动线程上传

    136. uploader0.submit(task0);

    137. uploader0.upload(uploader0);

    138. uploader1.submit(task1);

    139. uploader1.upload(uploader1);

    140. uploader2.submit(task2);

    141. uploader2.upload(uploader2);

    142. uploader3.submit(task3);

    143. uploader3.upload(uploader3);

    144. //注册一个定时任务,每2秒监控文件是否都上传完毕

    145. uploadCompleteMonitor = setInterval("uploadComplete()",2000);

    146. }

五、服务端处理:

服务端处理逻辑相对比较传统,利用输入输出流、NIO等把文件写到磁盘即可。

这里需要特别考虑的是关于被切块文件的合并。前端在上传的时候,文件块是无序到达服务端,因此我们在每次接收到一个文件块的时候需要判断被切块的文件是否都传输完毕并进行合并,思路如下:

回到前端,我们在构造被切块的文件formData的数据结构:

  1. formData.append("fileName", file.name);

  2. formData.append("fileSize", file.size);

  3. formData.append("uuid", task.uuid);

  4. formData.append("chunked", task.chunked);

  5. formData.append("data", file.slice(task.startIdx, task.endIdx));//截取文件块

  6. formData.append("currChunk", task.currChunk);

  7. formData.append("totalChunk", task.totalChunk);

fileName:文件的原始名字

fileSize:文件的大小,KB

uuid:文件的uuid

chunked:true,标识是分段上传的文件块

data:文件二进制流

currChunk:当前上传的块编号

totalChunk:总块数

服务端以文件的UUID为key,维护一个chunk计数器,每接收到一块就找到对应的uuid执行计数器+1,同时考虑到并发情况,需采用同步关键字,避免出现逻辑错误。当计数器等于totalChunk的时候,进行文件合并

前端效果:

文件上传存储目录:D:\wamp64\www\up6\db\upload\2019\04\19\920144c756af424ca59136be71cf9209

文件上传完成后,被完整的存放在了目录中。

DEMO下载地址:https://dwz.cn/fgXtRtnu

居于H5的多文件、大文件、多线程上传解决方案的更多相关文章

  1. 文件/大文件上传功能实现(JS+PHP)全过程

    文件/大文件上传功能实现(JS+PHP) 参考博文:掘金-橙红年代 前端大文件上传 路漫漫 其修远 PHP + JS 实现大文件分割上传 本文是学习文件上传后的学习总结文章,从无到有实现文件上传功能, ...

  2. java大文件上传解决方案

    最近遇见一个需要上传百兆大文件的需求,调研了七牛和腾讯云的切片分段上传功能,因此在此整理前端大文件上传相关功能的实现. 在某些业务中,大文件上传是一个比较重要的交互场景,如上传入库比较大的Excel表 ...

  3. 绝对好用Flash多文件大文件上传控件

    本实例采用的是Uploadify上传插件,.NET程序,源程序是从网上找的,但是有Bug,已经修改好,并标有部分注释.绝对好用,支持单文件.多文件上传,支持大文件上传,已经过多方面测试,保证好用. 以 ...

  4. 文件/大文件上传功能实现(JS+PHP)全过程

    PHP用超级全局变量数组$_FILES来记录文件上传相关信息的. 1.file_uploads=on/off 是否允许通过http方式上传文件 2.max_execution_time=30 允许脚本 ...

  5. 求大师点化,寻求大文件(最大20G左右)上传方案

    之前仿造uploadify写了一个HTML5版的文件上传插件,没看过的朋友可以点此先看一下~得到了不少朋友的好评,我自己也用在了项目中,不论是用户头像上传,还是各种媒体文件的上传,以及各种个性的业务需 ...

  6. php大文件上传解决方案

    PHP用超级全局变量数组$_FILES来记录文件上传相关信息的. 1.file_uploads=on/off 是否允许通过http方式上传文件 2.max_execution_time=30 允许脚本 ...

  7. PHP之文件的锁定、上传与下载

    小结文件的锁定机制.上传和下载 1.文件锁定 现在都在讲究什么分布式.并发等,实际上文件的操作也是并发的,在网络环境下,多个用户在同一时刻访问页面,对同一服务器上的同一文件进行着读取,如果,这个用户刚 ...

  8. 使用swfupload上传超过30M文件,使用FLASH上传组件

    原文:使用swfupload上传超过30M文件,使用FLASH上传组件 前一段时间会员的上传组件改用FLASH的swfupload来上传,既能很友好的显示上传进度,又能完全满足大文件的上传. 后来服务 ...

  9. jmert中如何测试上传文件接口(测试上传excel文件)

    第一次用jmeter这个工具测试上传接口,以前没做过这一块,导致走了很多弯路.特地把经验谢谢,怕自己以后忘记... 一,jmeter如何上传文件 jmeter 的 http requests post ...

随机推荐

  1. H5的本地存储技术及其与Cookie的比较

    第一部分: H5的本地存储技术 HTML5 提供了两种在客户端存储数据的新方法.先看下面的例子: 例1:var mySelection = {name:"car", amount: ...

  2. for...in的改进版for...of

    for...in 用起来似乎还不错,为什么又弄个 for...of 呢? 来看个例子: 'user strict' var arr = [12,13,14,15,16]; for(var i in a ...

  3. 从零开始实现RPC框架 - RPC原理及实现

    最近被人问到RPC相关的东西~突然发现还是有很多原理没有清楚,所以要好好系统的学习一下RPC以及它的原理 先大致了解一下RPC的大概,原文:https://blog.csdn.net/top_code ...

  4. 代码: !AJAX

    http://www.cnblogs.com/cwp-bg/p/7668840.html ajax和jsonp使用总结 2017-10-17 var requestUrl="http://l ...

  5. yum梳理

  6. SQL 中的语法顺序与执行顺序

    FROM : HOME SQL 是一种声明式语言 SQL 语言是为计算机声明了一个你想从原始数据中获得什么样的结果的一个范例,而不是告诉计算机如何能够得到结果. SQL 语言声明的是结果集的属性,计算 ...

  7. C#对接JAVA系统遇到的AES加密坑

    起因对接合作伙伴的系统,需要对数据进行AES加密 默认的使用了已经写好的帮助类中加密算法,发现结果不对,各种尝试改变加密模式改变向量等等折腾快一下午.最后网上查了下AES在JAVA里面的实现完整代码如 ...

  8. HttpWebResponse远程服务器返回错误: (500) 内部服务器错误 的解决办法

    在工作中用C#开发了一个小程序,不断访问去请求一个网站的页面,在循环过程中有时会报“远程服务器返回错误: (500) 内部服务器错误”,有时不会,出现的时机也不太一样.开始以为是网站的问题,后来网站是 ...

  9. 并发中的volatile

    目录 1. 概述 2. volatile的特性 3. volatile写-读的内存语义 4. volatile内存语义的实现 5. JSR-133为什么要增强volatile的内存语义 6. 总结 1 ...

  10. int和string之间的转换

    #include<cstring> #include<algorithm> #include<stdio.h> #include<iostream> # ...