在Web开发中,文件上传是个老生常谈的话题。但当文件尺寸超过100MB时,传统的表单上传方式就会暴露出诸多问题。我曾在多个企业级项目中处理过视频素材、设计图纸等大文件上传需求,最头疼的就是上传中途网络波动导致前功尽弃。
HTTP协议本身是无状态的,这意味着一旦上传中断,服务器无法自动恢复传输。更糟的是,大文件上传往往需要几分钟甚至几小时,用户不可能每次都从头开始。这就是为什么我们需要分块上传(Chunked Upload)和断点续传(Resume Upload)技术。
分块上传的核心思想是将大文件切割成若干小块(通常每块1-5MB),然后按顺序上传这些小块。这样做有三大优势:
在JSP中实现分块上传需要前后端配合:
javascript复制// 前端分片示例(使用File API)
const chunkSize = 5 * 1024 * 1024; // 5MB
let start = 0;
let end = Math.min(file.size, start + chunkSize);
while (start < file.size) {
const chunk = file.slice(start, end);
// 上传chunk到服务端...
start = end;
end = Math.min(file.size, start + chunkSize);
}
断点续传的关键在于记录上传进度。通常我们会:
java复制// 服务端状态记录示例
public class UploadStatus {
private String fileMd5;
private int totalChunks;
private Set<Integer> uploadedChunks;
private String tempFilePath;
}
重要提示:文件指纹计算应考虑"文件名+大小+修改时间"的组合,避免不同用户上传同名文件导致冲突。
在JSP中处理分块上传需要注意几个关键点:
jsp复制<%@ page import="java.io.*" %>
<%
// 获取分块参数
int chunkNumber = Integer.parseInt(request.getParameter("chunk"));
int totalChunks = Integer.parseInt(request.getParameter("chunks"));
String identifier = request.getParameter("identifier");
// 创建临时目录
File tempDir = new File(application.getRealPath("/temp"));
if (!tempDir.exists()) tempDir.mkdirs();
// 写入分块文件
File chunkFile = new File(tempDir, identifier + "." + chunkNumber);
try (InputStream in = request.getInputStream();
OutputStream out = new FileOutputStream(chunkFile)) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
}
%>
当所有分块上传完成后,需要将它们合并成完整文件:
java复制// 合并分块示例
public File mergeChunks(String identifier, int totalChunks, String destPath) throws IOException {
File destFile = new File(destPath);
try (FileOutputStream fos = new FileOutputStream(destFile, true)) {
for (int i = 0; i < totalChunks; i++) {
File chunk = new File("temp", identifier + "." + i);
try (FileInputStream fis = new FileInputStream(chunk)) {
byte[] buffer = new byte[8192];
int len;
while ((len = fis.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
}
chunk.delete(); // 删除已合并的分块
}
}
return destFile;
}
用户体验的关键是实时反馈上传进度:
javascript复制// 使用XMLHttpRequest监控进度
xhr.upload.onprogress = function(e) {
if (e.lengthComputable) {
const percent = Math.round((e.loaded / e.total) * 100);
progressBar.style.width = percent + '%';
}
};
网络不稳定时自动重试非常重要:
javascript复制function uploadChunk(chunk, retry = 3) {
return new Promise((resolve, reject) => {
const attempt = () => {
xhr.send(formData);
xhr.onerror = () => {
if (retry > 0) {
setTimeout(attempt, 1000);
retry--;
} else {
reject();
}
};
};
attempt();
});
}
处理大文件时要特别注意内存使用:
java复制// 正确的流处理方式
try (InputStream in = request.getInputStream();
OutputStream out = new FileOutputStream(file)) {
byte[] buffer = new byte[8192]; // 8KB缓冲区
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
}
文件上传是常见的安全漏洞来源,必须:
java复制// 安全的文件类型检查
public boolean isSafeFile(File file) {
try (InputStream is = new FileInputStream(file)) {
byte[] header = new byte[8];
is.read(header);
// 检查文件魔数(Magic Number)
return Arrays.equals(header, new byte[]{0x25, 0x50, 0x44, 0x46}) // PDF
|| Arrays.equals(header, 0, 4, new byte[]{0x89, 0x50, 0x4E, 0x47}, 0, 4); // PNG
} catch (IOException e) {
return false;
}
}
在电商平台项目中,我们最初使用base64编码传输文件分块,结果发现:
后来改用二进制直接传输,性能提升超过300%。另一个教训是关于临时文件清理 - 我们曾因未及时清理导致服务器磁盘爆满,现在使用定时任务每小时检查:
java复制// 定时清理临时文件
@Scheduled(fixedRate = 3600000) // 每小时执行
public void cleanTempFiles() {
File tempDir = new File("temp");
File[] files = tempDir.listFiles();
if (files != null) {
long cutoff = System.currentTimeMillis() - 86400000; // 24小时前
for (File file : files) {
if (file.lastModified() < cutoff) {
file.delete();
}
}
}
}
对于特别大的文件(如4K视频),我们还实现了以下优化: