近期业务需求,需要考虑到超大文件的上传功能,大文件和小文件上传到web服务器的实现有本质的区别:
-
小文件上传:前端直接使用表单(加上enctype="multipart/form-data"),直接使用 MultipartFile类接收上传的文件即可简单完成小文件的上传功能;
-
大文件上传:这里面可能出现的问题就太多了,专门用一个小结描述一下此问题,也知道为什么要写这边博客:
一、WEB服务器上传大文件会遇到的问题
1、大文件上传服务器的内存占用:
一般WEB开发框架如SpringMVC,在基于Web容器如Tomcat处理HTTP请求时,都倾向于采用职责链流水线式的处理机制。HTTP请求被封装为一个可解析对象放在内存里依次往下传。如果请求不光是正常文本,还带着上传附件,则必须考虑WEB容器限制单个请求的大小。如很多WEB容器默认一个请求最多分配20M的服务器内存,如果要在依次请求中上传文件则要根据限制上传文件大小调整这个配置。比如100M,这个使用如果有10个人同时上传100M文件,则就会占用1G内存。如果要上传更大文件,则必须要考虑分割文件多次请求上传,服务器接收到文件片段后再将文件重新组合。
如果上传文件占用内存太大,可能会导致服务器被拖死,所以部分大型网站用哪个专用服务器来处理大文件上传。
2、文件大小限制:
HTTP协议1.1版本中消息体长度字段Content-Length的类型规定为16个字节的Decimal类型(它能表示的最大值达到没朋友),且没有对该长度做逻辑上限制。单如果程序里用Int类型表示文件大小字节数,由于4个字节的int类型最大表示的有符号整数为2GB-1,无符号整数的最大值为4GB,会导致程序能上传的最大文件为2GB或4GB。
3、其他常见的应该考虑的问题
-
大文件传输,应该支持断点续传;
-
要有文件校验,校验不对的话能自动重传;
-
要考虑多线程分片上传,并发控制,带宽压力,急速上传(秒传);
-
多文件排队上传;
-
中转临时文件的删除机制;
二、本节大文件上传功能的实现
用到的技术点:SpringBoot+WebUploader+Redis+Maven,其实本来是想用一次Gradle的,但是无奈,公司项目用的是Maven;
1、新建一个新项目ZdUploader:
2、项目用到的依赖暂时如下:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.5</version> </dependency> <dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.5</version> </dependency> <!-- 下面是个人比较喜欢的小工具 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies>
3、简单的配置一下yml配置文件:
server: port: 8888 servlet: context-path: / session: 30 spring: redis: host: 192.168.163.128 port: 6379 pool: max-active: 30 max-idle: 10 max-wait: 10000 timeout: 0 http: multipart: max-file-size: 5MB max-request-size: 100MB logging: config: classpath:logback.xml level: root: info
4、先测试项目是否可以正常启动:
三、前端代码结构(在resources目录下)
1、前端代码结构比较简单,就是一个index.html页面,和依赖的前端插件webuploader插件;
2、在index.html中写点简单代码:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>断点上传</title> <link rel="stylesheet" type="text/css" href="css/webuploader.css"> <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> </head> <body> <div id="uploader" class="wu-example"> <div id="thelist" class="uploader-list"></div> <div class="btns"> <div id="picker">选择大文件</div> <button id="ctlBtn" class="btn btn-default">开始上传</button> </div> </div> <!--引入JS--> <script src="http://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script> <script type="text/javascript" src="js/webuploader.min.js"></script> <script type="text/javascript"> //自定义的js代码 </script> </body>
3、运行项目,访问“localhost:9999”————界面很简单
四、开始实现功能
1、大文件分块上传:
分块上传可以说是整个功能的基础,断点续传也是基于分块上传功能的。分块工作之际实现会比较复杂,我们可以直接使用百度的WebUploader来简单实现;
前后端必须高度配合,约定好每个分块的大小,这里我们设置为5M,实例化webUploader代码如下:
var uploader = WebUploader.create({ pick: { id: '#picker', label: '点击选择文件' }, formData: { uid: 0, md5: '', chunkSize: 5 * 1024 * 1024 }, swf: 'js/Uploader.swf', chunked: true, chunkSize: 5 * 1024 * 1024, // 字节 5M分块 threads: 3, server: 'http://10.125.23.111:10001/file/upload', auto: false, // 禁掉全局的拖拽功能。这样不会出现图片拖进页面的时候,把图片打开。 disableGlobalDnd: true, fileNumLimit: 1024, fileSizeLimit: 1024 * 1024 * 1024, // 200 M fileSingleSizeLimit: 1024 * 1024 * 1024 // 50 M });
我们将前端需要向后端传递的文件分块数据封装在一个实体类中:
public class MultipartFileParam { // 用户id private String uid; //任务ID private String id; //总分片数量 private int chunks; //当前为第几块分片 private int chunk; //当前分片大小 private long size = 0L; //文件名 private String name; //分片对象 private MultipartFile file; // MD5 private String md5; }
具体实现一次大文件(文件名为A.mp4)上传的过程描述如下:
1、前端利用上面实例化好的uploader对象计算出文件的MD5值;
2、前端调用后端的 checkFileMd5 方法检查此文件在本系统中的状态,状态有三:
-
IS_HAVE(100, "文件已存在!"),
-
NO_HAVE(101, "该文件没有上传过。"),
-
ING_HAVE(102, "该文件上传了一部分。");
3、我们当此文件时第一次上传,那么会返回NO_HAVE,
return new ResultVo(ResultStatus.NO_HAVE);
此时前端就会利用uploader实例对象将文件按约定大小进行分块,并发起多个文件上传请求,效果如下:
单个请求数据如下:
4、当后端接收到每个分块文件后,会根据总分片数量chunks创建一个A.mp4.conf文件,和主题文件A.mp4.tmp临时文件,并将此分块对应分块在A.mp4.conf文件中的位置标记为 Byte.MAX_VALUE;然后将文件内容写入到A.mp4.tmp文件的对应位置(这里可以选择使用RandomAccessFile或者MappedByteBuffer来合成文件,我们选择效率更高的MappedByteBuffer实现);
此时有个很重要的操作:
-
利用 A.mp4.conf 文件检查此文件的其他分块是否全部上传完成;
-
如果上传没有全部完成,那么就像Redis缓存中存入当前文件上传状态为false;以及此文件对应的具体文件名指向A.mp4.conf文件;
.opsForHash().put(Constants., param.getMd5(), ); .opsForValue().set(Constants.+ param.getMd5(), uploadDirPath + + fileName + );
-
如果文件已经上传完成,则将文件在Redis中的状态修改为true,并将此文件指向实际文件名A.mp4,同时将A.mp4.tmp临时主题文件更名为原文件名;
.opsForHash().put(Constants., param.getMd5(), ); .opsForValue().set(Constants.+ param.getMd5(), uploadDirPath + + fileName);
5、如果文件完全上传成功后,还需要将此文件的信息持久化到数据库中,表结构设计如下:
CREATE TABLE `file` ( `id` varchar(31) COLLATE utf8mb4_bin NOT NULL, `name` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL, `size` bigint(20) DEFAULT NULL, `sign_method` varchar(64) COLLATE utf8mb4_bin DEFAULT NULL COMMENT 'md5/sha256', `sign_value` varchar(64) COLLATE utf8mb4_bin DEFAULT NULL, `url` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '保存路径', `create_ts` bigint(20) DEFAULT NULL COMMENT '创建时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
至此一次文件上传完成,这个流程只是完成了最简单的大文件分块上传功能;
断点续传:
1、当文件上传过程中由于更重原因中断后,下次再进行传输前调用 checkFileMd5 方法时,根据它的MD5值在Redis中的查找到此文件的上传状态为部分上传;
2、我们再根据MD5获取到此文件的.conf文件,根据.conf文件中各个位置的状态就可以知道哪些分块是已经传输完成的,将未完成的分块写到一个List中,伴随着状态码一起返回给前端;
return new ResultVo<>(ResultStatus.ING_HAVE, missChunkList);
3、前端利用webuploader就可以简单实现从断点处继续上传未完成的分块上传工作(不必担心,这些都是webuploader封装好的);
同文件“秒传”:
1、在一个文件上传前调用 checkFileMd5 方法时,服务器后端会在数据库中查找,如果发现有此MD5对应的数据,那么代表此文件已经被上传过,那么就简单了,
return new ResultVo(ResultStatus.IS_HAVE, value);
2、前端接收到此状态码后,直接页面显示文件上传已完成,给用户的体验就是,文件“秒传”;
Demo演示效果如下:
正常上传成功:
秒传成功:
大功告成
2 Comments
(⊙o⊙)哇!博主的服务器响应真快
@宁王大人 难道你就没发现这篇博文没有写完么