文件过大上传处理:文件过大时,推荐端侧上传即前端(调用三方分片上传或三方的其他优化方法)直接上传到云存储。本文主要针对服务端上传,即前端->服务端->云存储的过程优化。
一、图片压缩再上传:图片过大时上传会占用服务器内存,可以在前端进行压缩后上传,如图是我上传后下载的图片,原图为14M,压缩后只有270KB
<!-- @format -->
<template>
<div >
<div ref="articalImg" class="articalImg">
<span>
<label class="button text-overflow" for="messageFile">
<span>上传图片<i class="el-icon-plus"></i></span>
</label>
<form id="messageFileForm" class="upload-form">
<input
ref="messageFile"
@change="fileChange($event)"
type="file"
id="messageFile"
style="position:absolute;clip:rect(0 0 0 0);"
accept="image/*"
class="upload-label"
/>
</form>
</span>
</div>
</div>
</template>
<script>
const MAX_IMAGE_WIDTH = 4096; // 图片最大宽度,超过了会裁剪
const MAX_IMAGE_PICK_SIZE = 20 * 1024 * 1024; // 选择图片最大大小,超过了不给旋转
const MAX_IMAGE_SIZE = 1 * 1024 * 1024; //超过1M压缩
export default {
methods: {
fileChange(ev) {
let that = this;
var fileList = ev.target.files;
let file = fileList[0];
let fileSize = file.size;
if (fileSize > MAX_IMAGE_PICK_SIZE) {
that.$Message.info('选择的图片最大不能超过20M');
ev.target.value = ''; // 清除,否则上传同一文件不会触发
return;
}
let quality = 1;
if (fileSize > MAX_IMAGE_SIZE) {
quality = MAX_IMAGE_SIZE / fileSize;
}
if (quality > 0.7) {
quality = 0.7; //默认压缩比为7
} else if (quality < 0.2) {
quality = 0.2; //最小压缩比2
}
// 旋转方向为默认
that.readImageFile(1, quality, file);
},
readImageFile(origin, quality, file) {
let that = this;
let fileName = file.name;
let reader = new FileReader();
reader.onload = function(evt) {
let base64File = evt.target.result;
that.imageCompress(
base64File,
origin,
{
quality
},
function(result) {
let blobFile = that.dataURLtoBlob(result);
let compressFile = new window.File([blobFile], fileName, { type: file.type });
that.uploadFile(compressFile);
}
);
};
reader.readAsDataURL(file);
},
// 压缩图片
imageCompress(path, Orientation, obj, callback) {
let img = new Image();
img.src = path;
img.onload = function() {
let that = this;
// 默认按比例压缩
let imgWidth = that.width;
let imgHeight = that.height;
let scale = imgWidth / imgHeight;
if (imgWidth > MAX_IMAGE_WIDTH) {
imgWidth = MAX_IMAGE_WIDTH;
imgHeight = imgWidth / scale;
}
let quality = obj.quality || 0.7; // 默认图片质量为0.7
// 生成canvas
let canvas = document.createElement('canvas');
let ctx = canvas.getContext('2d');
let anw = document.createAttribute('width');
let anh = document.createAttribute('height');
anw.nodeValue = imgWidth;
anh.nodeValue = imgHeight;
canvas.setAttributeNode(anw);
canvas.setAttributeNode(anh);
ctx.drawImage(that, 0, 0, imgWidth, imgHeight);
// quality值越小,所绘制出的图像越模糊
let base64 = canvas.toDataURL('image/jpeg', quality);
// 回调函数返回base64的值
callback(base64);
};
},
// 将base64转换为blob
dataURLtoBlob(dataurl) {
let arr = dataurl.split(',');
let mime = arr[0].match(/:(.*?);/)[1];
let bstr = atob(arr[1]);
let n = bstr.length;
let u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new Blob([u8arr], {
type: mime
});
},
//上传api
uploadFile(file) {
let formData = new FormData();
formData.append('file', file);
//清空本次选择的文件
this.$refs.messageFile.value = '';
this.$api['file/upload'](formData, {
headers: {
'Content-Type': 'multipart/form-data'
},
timeout: 300000
}).then(data => {
if (data) {
}
});
}
}
};
</script>
二、大文件上传限制优化加快上传速度:
1、后端限制:大文件上传报错
org.apache.tomcat.util.http.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (10486472) exceeds the configured maximum (10485760)
在springboot配置文件application.properties中加上配置:
(1)Spring Boot 1.4以下:
multipart.maxFileSize = 10Mb //单个文件的大小
multipart.maxRequestSize=100Mb //单次请求的文件的总大小
(2)Spring Boot1.4版本后:
spring.http.multipart.maxFileSize = 10Mb
spring.http.multipart.maxRequestSize=100Mb
(3)Spring Boot2.0之后:
spring.servlet.multipart.max-file-size=10000000
spring.servlet.multipart.max-request-size=100000000
2、nginx限制:打开文件后前端一直转圈页面卡死,过一会甚至提示上传超时或文件过大,而在windows本地没有这种问题,这往往是nginx限制的原因,可以更改配置client_max_body_size nginx.conf 修改默认限制上传文件大小,修改后打开文件前端几乎无等待,快速进入后端接口。原因是nginx对上传文件大小有限制,而且默认是1M。如我限制最大可以上传1G,http块下加上:
client_max_body_size 1024M;
另外,若上传文件很大,还要适当调整上传超时时间:
#文件大小限制,默认1m。
#限制请求体的大小,若超过所设定的大小,返回413错误。
client_max_body_size 50m;
#读取请求头的超时时间,若超过所设定的大小,返回408错误。
client_header_timeout 1m;
#读取请求实体的超时时间,若超过所设定的大小,返回413错误。
client_body_timeout 1m;
#http请求无法立即被容器(tomcat, netty等)处理,被放在nginx的待处理池中等待被处理。此参数为等待的最长时间,默认为60秒,官方推荐最长不要超过75秒。
proxy_connect_timeout 60s;
#http请求被容器(tomcat, netty等)处理后,nginx会等待处理结果,也就是容器返回的response。此参数即为服务器响应时间,默认60秒。
proxy_read_timeout 1m;
#http请求被服务器处理完后,把数据传返回给Nginx的用时,默认60秒。
proxy_send_timeout 1m;
3、后端卡顿:大文件上传占用带宽,上传一直不返回结果,页面则一直等待。这时候可以在后端异步上传,返回“上传中”结果给前端,在后端开启子线程进行上传;加上redis锁同一时间只能上传一个文件等方法来提升效率:
private Long id;
@Override
public Result<String> uploadFile(MultipartFile file) {
if (redisClient.isExists(UPLOAD_FLAG)){
Result<String> result = new Result<>();
result.setCode("-1");
result.setMessage("当前有文件在上传中,请稍后再试!");
return result;
}
// 打上文件正在上传标识,防止并发上传文件
redisClient.set(UPLOAD_FLAG,"true", 80000);
try{
UploadInfoDO uploadInfoDO = new UploadInfoDO();
uploadInfoDO.setFileStatus("上传中");
uploadInfoDO.setFileName(file.getOriginalFilename());
fileMapper.inser(uploadInfoDO);
id = uploadInfoDO.getId();
} catch (Exception e){
redisClient.expire(UPLOAD_FLAG, 0);
Result<String> result = new Result<>();
result.setCode("-1");
result.setMessage("文件入库异常"+ e.getMessage());
return result;
}
threadPoolTaskExecutor.execute(() -> upload(file));
return new Result<>();
}
//上传文件
private void upload(MultipartFile file){
String originalFilename = file.getOriginalFilename();
try {
//调用上传接口上传到服务器
......
//更新上传状态
UploadInfoDO uploadInfoDO = new UploadInfoDO();
uploadInfoDO.setId(id);
uploadInfoDO.setUploadStatus("上传成功");
fileMapper.updateVideoUploadInfoDO(UploadInfoDO);
} catch (Exception e) {
UploadInfoDO uploadInfoDO = new UploadInfoDO();
uploadInfoDO.setId(id);
uploadInfoDO.setUploadStatus("上传失败");
fileMapper.update(uploadInfoDO);
} finally {
redisClient.expire(UPLOAD_FLAG, 0);
}
}
但是这种情况下,大文件单文件上传仍然很慢,这就需要分片上传。
三、前端分片上传到服务端:上传时在前端进行分片,后端保存分片,前端判断是最后一个分片时候调用合并接口,合并接口按照之前分片的顺序进行合并
1、前端:
<template>
<div>
<div class="upload-div">
<span>
<label class="button text-overflow" for="messageFile">
<span class="upload-label">上传文件<i class="el-icon-plus"></i></span>
</label>
<form id="messageFileForm" class="upload-form">
<input
ref="messageFile"
@change="cutFile($event)"
type="file"
id="messageFile"
style="position: absolute; clip: rect(0 0 0 0)"
/>
</form>
</span>
</div>
</div>
</template>
<script>
import { cutFile, mergeFile } from "@/api/upload";
export default {
methods: {
cutFile(ev) {
var fileList = ev.target.files;
let file = fileList[0];
ev.target.value = '';
this.PostFile(file, 0);
},
//执行分片上传
PostFile(file, i, uuid) {
var name = file.name, //文件名
size = file.size, //总大小shardSize = 2 * 1024 * 1024,
shardSize = 1 * 1024 * 1024, //以1MB为一个分片,每个分片的大小
shardCount = Math.ceil(size / shardSize); //总片数
if (i >= shardCount) {
return;
}
//判断uuid是否存在
if (uuid == undefined || uuid == null) {
uuid = this.guid();
}
//console.log(size,i+1,shardSize); //文件总大小,第一次,分片大小//
var start = i * shardSize;
var end = start + shardSize;
var packet = file.slice(start, end); //将文件进行切片
/* 构建form表单进行提交 */
var form = new FormData();
form.append("uuid", uuid);// 前端生成uuid作为标识符传个后台每个文件都是一个uuid防止文件串了
form.append("data", packet); //slice方法用于切出文件的一部分
form.append("name", name);
form.append("totalSize", size);
form.append("total", shardCount); //总片数
form.append("index", i + 1); //当前是第几片
cutFile(form)
.then(res => {
console.log(res);
let msg = res.data
/* 表示上一块文件上传成功,继续下一次 */
if (msg.status == 201) {
form = '';
i++;
this.PostFile(file, i, uuid);
} else if (msg.status == 502) {
form = '';
/* 失败后,每2秒继续传一次分片文件 */
setInterval(function () { PostFile(file, i, uuid) }, 2000);
} else if (msg.status == 200) {
this.merge(uuid, name)
console.log("上传成功");
} else if (msg.status == 500) {
console.log('第' + msg.i + '次,上传文件有误!');
} else {
console.log('未知错误');
}
})
.catch(() => { });
},
merge(uuid, fileName) {
let file = { 'uuid': uuid, 'newFileName': fileName }
mergeFile(file)
.then(res => {
alert("上传成功");
})
.catch(() => { });
},
guid() {
return 'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = Math.random() * 16 | 0,
v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
}
};
</script>
<style scoped >
.upload-div {
margin: 20px 0;
}
.upload-label {
background-color: #169bd5;
color: #ffffff;
padding: 8px 10px;
font-weight: bold;
}
.upload-form {
display: inline;
}
.tip {
padding-left: 20px;
color: #f59a23;
}
</style>
/api/upload.js:
import request from '@/utils/request'
export function upload(formDatas) {
return request({
url: '/file/upload',
method: 'post',
upload: true,
data: formDatas
})
}
export function upload1(formDatas) {
return request({
url: '/file/upload1',
method: 'post',
upload: true,
data: formDatas
})
}
export function download() {
return request({
url: '/file/download',
method: 'post'
})
}
export function cutFile(formDatas) {
return request({
url: '/postFile/cutFile',
method: 'post',
upload: true,
data: formDatas
})
}
export function mergeFile(file) {
const uuid = file.uuid
const newFileName = file.newFileName
return request({
url: '/postFile/mergeFile',
method: 'get',
params: { uuid, newFileName }
})
}
2、后端:
配置文件支持最大1g文件上传:
server.port=9999
server.context-path=/demo
spring.redis.host=localhost
spring.redis.port=6379
#spring.redis.password=
spring.redis.database=1
spring.redis.pool.max-active=8
spring.redis.pool.max-wait=-1
spring.redis.pool.max-idle=500
spring.redis.pool.min-idle=0
spring.redis.timeout=0
#大文件上传
spring.http.multipart.enabled=true
spring.http.multipart.maxFileSize=1024MB
spring.http.multipart.maxRequestSize=1024MB
分片上传和合并接口:
package com.demo.rest;
import com.demo.dto.FileDTO;
import com.demo.dto.ResponseMessage;
import org.apache.tomcat.util.http.fileupload.FileUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.*;
@RestController
@RequestMapping("/postFile")
public class PostFileController {
//分片目录
private static String fileUploadTempDir = "D:/portalupload/fileuploaddir";
//合并后的目录
private static String fileUploadDir = "D:/portalupload/file";
//保存分片的文件
@RequestMapping("/cutFile")
public ResponseMessage fragmentation(@ModelAttribute FileDTO fileDTO) {
//resp.addHeader("Access-Control-Allow-Origin", "*");
Map<String, Object> map = new HashMap<>();
//MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) req;
// 获得文件分片数据
MultipartFile file = fileDTO.getData();
//分片第几片
int index = fileDTO.getIndex();
//总片数
int total = fileDTO.getTotal();
//获取文件名
String fileName = fileDTO.getName();
String name = fileName.substring(0, fileName.lastIndexOf("."));
String fileEnd = fileName.substring(fileName.lastIndexOf("."));
//前端uuid,作为标识
String uuid = fileDTO.getUuid();
File uploadFile = new File(fileUploadTempDir + "/" + uuid, index + ".tem");
if (!uploadFile.getParentFile().exists()) {
uploadFile.getParentFile().mkdirs();
}
if (index < total) {
try {
file.transferTo(uploadFile);
// 上传的文件分片名称
map.put("status", 201);
return ResponseMessage.success(map);
} catch (IOException e) {
e.printStackTrace();
map.put("status", 502);
return ResponseMessage.success(map);
}
} else {
try {
file.transferTo(uploadFile);
// 上传的文件分片名称
map.put("status", 200);
return ResponseMessage.success(map);
} catch (IOException e) {
e.printStackTrace();
map.put("status", 502);
return ResponseMessage.success(map);
}
}
}
//合并文件
@RequestMapping(value = "/mergeFile", method = RequestMethod.GET)
public ResponseMessage merge(String uuid, String newFileName) {
Map retMap = new HashMap();
try {
File dirFile = new File(fileUploadTempDir + "/" + uuid);
if (!dirFile.exists()) {
throw new RuntimeException("文件不存在!");
}
//分片上传的文件已经位于同一个文件夹下,方便寻找和遍历(当文件数大于十的时候记得排序用冒泡排序确保顺序是正确的)
String[] fileNames = dirFile.list();
//排序,排序不正确,读取的顺序不对,最后合并的文件会打不开
List<Integer> sortFileIndexs = new ArrayList<>();
for(String fileName :fileNames){
sortFileIndexs.add(Integer.valueOf(fileName.replace(".tem","")));
}
Collections.sort(sortFileIndexs);
//创建空的合并文件
File targetFile = new File(fileUploadDir, newFileName);
if(targetFile.exists()){
targetFile.delete();
}
if (!targetFile.getParentFile().exists()) {
targetFile.getParentFile().mkdirs();
}
targetFile.createNewFile();
RandomAccessFile writeFile = new RandomAccessFile(targetFile, "rw");
int position = 0;
for (Integer fileIndex : sortFileIndexs) {
String fileName = fileIndex+".tem";
System.out.println(fileName);
File sourceFile = new File(fileUploadTempDir + "/" + uuid, fileName);
RandomAccessFile readFile = new RandomAccessFile(sourceFile, "rw");
int chunksize = 1024 * 3;
byte[] buf = new byte[chunksize];
writeFile.seek(position);
int byteCount = 0;
while ((byteCount = readFile.read(buf)) != -1) {
if (byteCount != chunksize) {
byte[] tempBytes = new byte[byteCount];
System.arraycopy(buf, 0, tempBytes, 0, byteCount);
buf = tempBytes;
}
writeFile.write(buf);
position = position + byteCount;
}
readFile.close();
//FileUtils.forceDelete(sourceFile);//删除缓存的临时文件
}
writeFile.close();
retMap.put("code", "200");
}catch (IOException e){
e.printStackTrace();
retMap.put("code", "500");
}
return ResponseMessage.success(retMap);
}
}
3、测试:准备三个文件,一个小图片,一个较大的图片和一个视频文件
(1)点击页面上传6KB的小图片:
因为我限制分片大小为1M,所以小图片只有一个分片:
上传成功,图片可以正常打开
(2)上传大图片:调用了多次分片接口:
图片可以正常打开:
(3)上传大的视频文件:280MB视频上传大概耗时半分钟
视频可以正常打开:
四、服务端分片上传到云存储:同样是先分片再合并
//step2 循环上传每一片,可中断,下次从中断的片上传即可
//分片的方法不止一种,可以自行选择
try (FileInputStream in = (FileInputStream) file.getInputStream()) {
try (FileChannel fcin = in.getChannel()) {
ByteBuffer buffer = ByteBuffer.allocate(1024*1204);
int chunk = 0;
while (true) {
buffer.clear();
int flag = fcin.read(buffer);
if (flag == -1) {
break;
}
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes, 0, bytes.length);
//todo分片上传
chunk += 1;
}
System.out.println(chunk);
}
} catch (FileNotFoundException e) {
// e.printStackTrace();
} catch (IOException e) {
// e.printStackTrace();
}
//todo合并