在Web应用开发中,文件上传是个老生常谈的话题,但当文件尺寸超过100MB时,传统的表单上传方式就会暴露出诸多问题。我曾经接手过一个在线教育平台的项目,用户需要上传高清教学视频,平均每个文件都在500MB以上。最初采用普通上传方式,服务器经常报413错误(请求实体过大),而且网络波动导致的上传失败让用户苦不堪言。
HTTP协议本身对请求体大小没有硬性限制,但实际环境中存在多重制约:
更致命的是,一旦上传过程中断,整个文件需要重新传输。我曾用Fiddler做过测试:上传一个300MB文件到90%时断网,用户需要从0%重新开始,这种体验简直灾难。
分块上传(Chunked Upload)将大文件切割成若干小块(通常1-5MB),通过多次请求分别上传。这种方案有三大核心优势:
在JSP环境中实现这套机制,需要前端分片、服务端校验、存储重组三个环节的紧密配合。下面通过一个电商网站商品视频上传的实际案例,详解各环节实现要点。
前端采用HTML5的File API进行分片处理,核心代码如下:
javascript复制// 获取文件对象
const file = document.getElementById('fileInput').files[0];
const chunkSize = 2 * 1024 * 1024; // 2MB分块
const totalChunks = Math.ceil(file.size / chunkSize);
// 生成文件指纹(用于断点续传识别)
const fileHash = await calculateMD5(file);
for (let i = 0; i < totalChunks; i++) {
const start = i * chunkSize;
const end = Math.min(file.size, start + chunkSize);
const chunk = file.slice(start, end);
uploadChunk(chunk, i, fileHash);
}
关键点:使用Web Worker计算文件MD5时,大文件可能造成UI线程阻塞。解决方案是采用抽样哈希——只计算文件头尾和中间若干段的哈希值组合。
实现健壮的上传控制需要考虑以下因素:
并发控制:Chrome对同一域名最多6个TCP连接
javascript复制// 使用令牌桶算法控制并发
const MAX_CONCURRENT = 3;
let currentUploads = 0;
const queue = [];
function processQueue() {
while (queue.length > 0 && currentUploads < MAX_CONCURRENT) {
const {chunk, index, hash} = queue.shift();
currentUploads++;
doUpload(chunk, index, hash).finally(() => {
currentUploads--;
processQueue();
});
}
}
断点续传实现:
进度计算:
javascript复制// 精确计算总进度
const progress = uploadedChunks.reduce((sum, chunk) => {
return sum + (chunk.loaded / chunk.total);
}, 0) / totalChunks;
在Tomcat环境中,需要调整web.xml配置:
xml复制<multipart-config>
<max-file-size>52428800</max-file-size> <!-- 50MB -->
<max-request-size>52428800</max-request-size>
</multipart-config>
分片接收示例:
jsp复制<%@ page import="java.io.*" %>
<%
String chunkIndex = request.getParameter("chunkNumber");
String fileHash = request.getParameter("fileHash");
// 创建临时目录
File tempDir = new File(getServletContext().getRealPath("/uploads/temp/" + fileHash));
if (!tempDir.exists()) tempDir.mkdirs();
// 保存分片
Part filePart = request.getPart("file");
File chunkFile = new File(tempDir, chunkIndex + ".part");
filePart.write(chunkFile.getAbsolutePath());
// 记录上传状态
try (FileWriter writer = new FileWriter(new File(tempDir, "progress.txt"), true)) {
writer.write(chunkIndex + "\n");
}
%>
当所有分片上传完成后,触发合并操作:
java复制File[] chunks = tempDir.listFiles((dir, name) -> name.endsWith(".part"));
Arrays.sort(chunks, Comparator.comparingInt(f ->
Integer.parseInt(f.getName().split("\\.")[0])));
try (OutputStream output = new FileOutputStream(finalFile)) {
for (File chunk : chunks) {
Files.copy(chunk.toPath(), output);
chunk.delete(); // 删除已合并分片
}
}
重要提示:合并大文件时(如超过1GB),建议使用NIO的FileChannel.transferTo方法,减少内存占用:
java复制try (FileChannel out = new FileOutputStream(finalFile).getChannel()) { for (File chunk : chunks) { try (FileChannel in = new FileInputStream(chunk).getChannel()) { in.transferTo(0, in.size(), out); } } }
CDN边缘上传:通过与云服务商API集成,将分片直接上传至最近的边缘节点
javascript复制// 阿里云OSS直传示例
const client = new OSS({
region: 'oss-cn-hangzhou',
accessKeyId: '您的AccessKey',
accessKeySecret: '您的AccessKeySecret',
bucket: '您的Bucket名称'
});
async function uploadToOSS(chunk, index) {
await client.put(`temp/${fileHash}/${index}`, chunk);
}
P2P传输:利用WebRTC实现客户端间分片共享(适合企业内部大文件分发)
分片校验:
java复制// 校验分片完整性
MessageDigest md = MessageDigest.getInstance("MD5");
try (InputStream is = Files.newInputStream(chunkFile.toPath())) {
byte[] buffer = new byte[8192];
int read;
while ((read = is.read(buffer)) != -1) {
md.update(buffer, 0, read);
}
}
String actualHash = DatatypeConverter.printHexBinary(md.digest());
if (!actualHash.equals(clientProvidedHash)) {
throw new SecurityException("分片校验失败");
}
权限控制:
搭建ELK日志系统记录关键指标:
java复制// 使用Log4j2结构化日志
logger.info("ChunkUploaded",
Map.of(
"fileHash", fileHash,
"chunkIndex", chunkIndex,
"clientIP", request.getRemoteAddr(),
"networkType", request.getHeader("X-Network-Type")
));
现象:部分分片反复上传失败
排查步骤:
client_max_body_size是否大于分片大小xml复制<Connector
connectionUploadTimeout="120000"
disableUploadTimeout="false"
maxPostSize="0" />
df -h和inode数量df -i现象:视频文件无法播放或ZIP解压报错
解决方案:
java复制// 在合并完成后写入校验信息
try (DataOutputStream out = new DataOutputStream(new FileOutputStream(finalFile, true))) {
out.writeUTF("EOF_MARKER:" + fileHash);
}
现象:上传大文件时JVM崩溃
优化方案:
bash复制-XX:+UseG1GC -Xms512m -Xmx2g -XX:MaxGCPauseMillis=200
java复制try (RandomAccessFile raf = new RandomAccessFile(finalFile, "rw")) {
raf.setLength(totalSize);
MappedByteBuffer out = raf.getChannel().map(
FileChannel.MapMode.READ_WRITE, 0, totalSize);
for (File chunk : chunks) {
try (FileInputStream fis = new FileInputStream(chunk)) {
FileChannel fc = fis.getChannel();
MappedByteBuffer in = fc.map(
FileChannel.MapMode.READ_ONLY, 0, fc.size());
out.put(in);
}
}
}
在阿里云ECS(2核4G)环境下实测不同方案的吞吐量:
| 方案 | 500MB文件上传耗时 | 内存峰值 | 网络利用率 |
|---|---|---|---|
| 传统表单上传 | 82s | 680MB | 63% |
| 分块上传(1MB) | 76s | 210MB | 71% |
| 分块上传+CDN | 58s | 180MB | 89% |
| 分块上传+P2P | 42s | 250MB | 94% |
测试结论: