文章目录
一、前端实现分片上传
在前端,我们通过 JavaScript 的 File.slice()
方法将大文件切割成多个小的分片,然后逐个分片上传到后端。这样可以避免在上传过程中遇到的大文件上传性能瓶颈,且支持断点续传。
- 选择文件并切割成分片
我们需要先选择文件,并通过 slice()
方法将大文件切割成多个小块(即分片)。每个分片会单独上传。每次上传文件分片时,我们会附带必要的元数据(如文件名、总分片数、当前分片编号等)来帮助后端完成文件合并。
HTML 和 JavaScript 代码
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>文件分片上传</title>
<style>
/* 样式 */
.success { color: green; }
.error { color: red; }
</style>
</head>
<body>
<div class="container">
<h1>文件分片上传</h1>
<form id="uploadForm">
<label for="file">选择文件:</label>
<input type="file" id="file" name="file" required>
<br><br>
<button type="submit">上传文件</button>
</form>
<div id="message" class="message"></div>
<div>
<label>上传进度:</label>
<progress id="uploadProgress" value="0" max="100" style="width: 100%;"></progress>
<span id="progressPercentage">0%</span>
</div>
</div>
<script>
const form = document.getElementById('uploadForm');
const messageDiv = document.getElementById('message');
const progressBar = document.getElementById('uploadProgress');
const progressPercentage = document.getElementById('progressPercentage');
const chunkSize = 1024 * 1024; // 每个分片的大小(1MB)
// 获取已上传的分片列表
function getUploadedChunks(identifier) {
return fetch(`/api/upload/check?identifier=${identifier}`)
.then(response => response.ok ? response.json() : [])
.then(result => result.uploadedChunks || []);
}
// 上传当前分片
function uploadChunk(file, chunkNumber, totalChunks, identifier) {
const chunk = file.slice(chunkNumber * chunkSize, (chunkNumber + 1) * chunkSize);
const formData = new FormData();
formData.append('file', chunk);
formData.append('filename', file.name);
formData.append('totalChunks', totalChunks);
formData.append('chunkNumber', chunkNumber + 1); // 当前分片的编号
formData.append('identifier', identifier);
return fetch('/api/upload/chunk', {
method: 'POST',
body: formData,
})
.then(response => {
if (!response.ok) throw new Error('分片上传失败');
return response.text();
});
}
form.onsubmit = function(e) {
e.preventDefault(); // 阻止表单的默认提交行为
const fileInput = document.getElementById('file');
const file = fileInput.files[0]; // 获取选择的文件
const totalChunks = Math.ceil(file.size / chunkSize); // 计算分片总数
const identifier = file.name + "_" + Date.now(); // 为文件生成唯一标识符
// 获取已上传的分片列表
getUploadedChunks(identifier)
.then(uploadedChunks => {
let chunkNumber = uploadedChunks.length; // 从已上传的分片之后开始上传
const totalSize = file.size; // 文件的总大小
// 更新进度条
function updateProgress(totalSize, uploadedSize) {
uploadedSize = Math.min(uploadedSize, totalSize);
const progress = (uploadedSize / totalSize) * 100; // 计算进度
progressBar.value = progress;
progressPercentage.textContent = `${Math.round(progress)}%`;
}
// 上传下一个分片
function uploadNextChunk() {
if (chunkNumber < totalChunks) {
return uploadChunk(file, chunkNumber, totalChunks, identifier)
.then(result => {
messageDiv.innerHTML = `<span class="success">${result}</span>`;
chunkNumber++; // 上传成功后,进入下一个分片
const uploadedSize = chunkNumber * chunkSize; // 已上传的大小
updateProgress(totalSize, uploadedSize); // 更新进度条
return uploadNextChunk(); // 上传下一个分片
})
.catch(error => {
messageDiv.innerHTML = `<span class="error">${error.message}</span>`;
// 如果上传失败,重试当前分片
return new Promise(resolve => setTimeout(resolve, 3000)) // 等待 3 秒重试
.then(() => uploadNextChunk());
});
} else {
// 确保进度条显示为100%并显示上传完成
updateProgress(totalSize, totalSize);
messageDiv.innerHTML += "<span class='success'>文件上传完成!</span>";
return Promise.resolve(); // 上传完成
}
}
uploadNextChunk(); // 开始上传分片
})
.catch(error => {
messageDiv.innerHTML = `<span class="error">${error.message}</span>`;
});
};
</script>
</body>
</html>
代码说明:
file.slice()
:通过该方法将大文件切割成多个小块(分片)。slice
方法接受两个参数:起始位置和结束位置。通过这个方法可以将大文件分割成大小适中的块,进行分片上传。FormData
:每次上传一个分片时,使用FormData
将分片文件和其他信息(如文件名、分片总数、当前分片号)传递给后端。fetch
:使用fetch
发送POST
请求,将每个分片上传到服务器。
二、后端处理分片上传
后端负责接收每个分片,并保存到临时位置。当所有分片上传完毕后,后端需要将这些分片合并成原始文件。
- 后端处理分片上传
文件分片上传接口
后端使用 Spring Boot 提供的 MultipartFile
接口来接收文件分片。每次上传一个分片时,后端保存它,并在上传完成后进行文件合并。
java
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.Channels;
@RestController
@RequestMapping("/api/upload")
public class FileUploadController {
private static final String UPLOAD_DIR = "E:/uploads/"; // 定义文件保存目录
@PostMapping("/chunk")
public ResponseEntity<String> uploadChunk(
@RequestParam("file") MultipartFile file,
@RequestParam("filename") String filename,
@RequestParam("totalChunks") int totalChunks,
@RequestParam("chunkNumber") int chunkNumber) {
try {
// 保存每个分片到临时文件
File destFile = new File(UPLOAD_DIR + filename + "_part_" + chunkNumber);
storeFileWithZeroCopy(file, destFile);
// 检查是否上传完成所有分片
if (chunkNumber == totalChunks) {
// 合并所有分片
File mergedFile = new File(UPLOAD_DIR + filename);
mergeChunks(filename, totalChunks, mergedFile);
return new ResponseEntity<>("文件上传完成", HttpStatus.OK);
} else {
return new ResponseEntity<>("分片上传成功", HttpStatus.OK);
}
} catch (Exception e) {
return new ResponseEntity<>("文件上传失败:" + e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
// 使用零拷贝技术将分片文件保存到磁盘
private void storeFileWithZeroCopy(MultipartFile file, File destFile) throws IOException {
try (ReadableByteChannel inputChannel = Channels.newChannel(file.getInputStream());
FileChannel outputChannel = new RandomAccessFile(destFile, "rw").getChannel()) {
outputChannel.transferFrom(inputChannel, 0, file.getSize());
}
}
// 合并所有分片成一个完整文件
private void mergeChunks(String filename, int totalChunks, File mergedFile) throws IOException {
try (RandomAccessFile outputFile = new RandomAccessFile(mergedFile, "rw");
FileChannel outputChannel = outputFile.getChannel()) {
for (int i = 1; i <= totalChunks; i++) {
File partFile = new File(UPLOAD_DIR + filename + "_part_" + i);
try (ReadableByteChannel inputChannel = Channels.newChannel(new FileInputStream(partFile))) {
outputChannel.transferFrom(inputChannel, outputFile.length(), partFile.length());
}
partFile.delete(); // 删除已合并的分片
}
}
}
}
代码说明:
MultipartFile
:Spring 提供的接口,用于接收上传的文件分片。storeFileWithZeroCopy
:使用零拷贝技术(transferFrom
)将文件分片直接保存到磁盘,避免了内存拷贝的性能损失。mergeChunks
:当所有分片上传完毕,调用该方法合并所有分片文件,最终生成一个完整的文件。
零拷贝(Zero Copy)
零拷贝是一种优化技术,它可以避免数据在内存和磁盘之间的多次复制,减少 CPU 负担,提高性能。在这里,我们使用 FileChannel.transferFrom()
方法将文件分片直接写入目标文件,而不经过内存的中转。
三、总结
通过前端使用 File.slice()
方法将大文件切割成多个小分片,并逐一上传,后端接收到每个分片后进行保存和合并。这样能够有效避免大文件上传过程中的网络波动、时间过长等问题,同时提供了断点续传的功能。
文件分片上传的步骤:
- 前端切割文件并上传:将文件分割成小块,逐一上传。
- 后端接收分片并保存:每次接收一个文件分片,并保存到临时文件。
- 上传完成后,后端合并分片:当所有分片上传完成,后端将所有分片合并成完整的文件。