1. 项目背景与需求分析
作为一名在中关村摸爬滚打多年的老码农,最近接手了一个颇具挑战性的政府项目——需要开发一个能够稳定上传下载10GB大小文件的Web系统,并且必须适配国产信创环境。这个需求听起来简单,但实际开发过程中遇到的坑,比二环早高峰的交通还要复杂。
核心需求可以归纳为以下几点:
- 超大文件处理能力:支持10GB以上文件的上传下载
- 信创环境适配:兼容统信UOS、银河麒麟等国产操作系统
- 断点续传功能:网络中断后能够恢复上传/下载
- 分片上传机制:将大文件切分为小块传输
- 完整的文档体系:包括设计文档、开发手册和部署指南
2. 技术选型与方案设计
2.1 前端技术栈选择
经过对各种开源组件的评估,我们最终选择了Vue.js作为前端框架,主要基于以下考虑:
- 组件化开发:便于封装上传下载功能
- 响应式特性:实时展示上传下载进度
- 丰富的生态系统:axios、element-ui等配套组件成熟
- 良好的国产浏览器兼容性
2.2 后端技术方案
后端采用传统的JSP/Servlet架构,主要考虑因素包括:
- 项目历史原因:客户已有JSP技术栈
- 稳定性:Servlet在大文件处理方面表现可靠
- 可控性:便于针对信创环境做特殊适配
2.3 分片上传设计
大文件上传的核心在于分片策略:
- 分片大小:16MB(经过多次测试得出的最优值)
- 并发控制:普通环境5并发,信创环境2并发
- 哈希校验:采用SM3国密算法计算文件指纹
- 断点续传:基于分片索引和文件哈希实现
3. 核心实现细节
3.1 前端Vue组件实现
文件分片上传逻辑
javascript复制// FileUploader.vue核心方法
methods: {
async uploadChunk(file, chunkIndex) {
const start = chunkIndex * this.chunkSize;
const end = Math.min(file.size, start + this.chunkSize);
const chunk = file.slice(start, end);
const formData = new FormData();
formData.append('file', new Blob([chunk]));
formData.append('chunkIndex', chunkIndex);
formData.append('totalChunks', Math.ceil(file.size / this.chunkSize));
formData.append('fileHash', this.fileMd5);
formData.append('fileName', file.name);
try {
const response = await axios.post(this.uploadUrl, formData, {
headers: { 'X-Gov-Env': this.govMode ? 'true' : 'false' },
timeout: this.govMode ? 300000 : 60000,
onUploadProgress: (progressEvent) => {
this.updateProgress(chunkIndex, progressEvent);
}
});
return response.data;
} catch (error) {
if (this.govMode && error.code === 'ECONNABORTED') {
await this.delayRetry();
return this.uploadChunk(file, chunkIndex);
}
throw error;
}
}
}
关键实现要点
- 分片计算:使用File API的slice方法切割文件
- 进度监控:通过axios的onUploadProgress回调
- 信创适配:增加超时时间和重试机制
- 并发控制:使用Promise.race实现简单的并发队列
3.2 后端JSP实现
文件接收Servlet
java复制@WebServlet("/FileUploadServlet")
@MultipartConfig(
fileSizeThreshold = 1024 * 1024 * 10,
maxFileSize = 1024 * 1024 * 100,
maxRequestSize = 1024 * 1024 * 500
)
public class FileUploadServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 获取分片参数
String fileHash = request.getParameter("fileHash");
int chunkIndex = Integer.parseInt(request.getParameter("chunkIndex"));
// 保存分片到临时目录
Part filePart = request.getPart("file");
File chunkFile = new File(tempDir, fileHash + "_" + chunkIndex);
try (InputStream input = filePart.getInputStream();
OutputStream output = new FileOutputStream(chunkFile)) {
byte[] buffer = new byte[1024 * 1024];
int bytesRead;
while ((bytesRead = input.read(buffer)) != -1) {
output.write(buffer, 0, bytesRead);
}
}
// 返回响应
response.getWriter().write("{\"success\":true}");
}
}
文件合并Servlet
java复制@WebServlet("/FileMergeServlet")
public class FileMergeServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 获取合并参数
String fileHash = request.getParameter("fileHash");
String fileName = request.getParameter("fileName");
int totalChunks = Integer.parseInt(request.getParameter("totalChunks"));
// 合并文件
File mergedFile = new File(finalDir, fileName);
try (FileChannel outChannel = new FileOutputStream(mergedFile).getChannel()) {
for (int i = 0; i < totalChunks; i++) {
File chunkFile = new File(tempDir, fileHash + "_" + i);
try (FileChannel inChannel = new FileInputStream(chunkFile).getChannel()) {
inChannel.transferTo(0, inChannel.size(), outChannel);
}
chunkFile.delete();
}
}
// 返回结果
response.getWriter().write("{\"success\":true}");
}
}
4. 信创环境适配要点
4.1 浏览器兼容性处理
javascript复制// 信创浏览器检测
Vue.prototype.$isGovBrowser = () => {
const ua = navigator.userAgent.toLowerCase();
return ua.includes('uos') || ua.includes('kylin') || ua.includes('loongson');
};
4.2 文件系统适配
java复制public class GovPathUtil {
public static String getTempDir() {
String os = System.getProperty("os.name").toLowerCase();
if (os.contains("linux") && System.getenv("GOV_ENV") != null) {
return "/opt/gov_upload"; // 信创专用路径
}
return System.getProperty("java.io.tmpdir");
}
}
4.3 性能优化策略
- 并发控制:信创环境下将并发数从5降到2
- 超时设置:信创环境超时时间延长至5分钟
- 重试机制:网络异常时自动延迟重试
- 内存管理:合理设置缓冲区大小,避免OOM
5. 实际应用中的经验总结
5.1 踩过的坑与解决方案
-
国产浏览器兼容性问题
- 现象:某些国产浏览器对File API支持不完整
- 解决:增加polyfill并降级处理
-
大文件内存溢出
- 现象:上传大文件时服务器内存飙升
- 解决:优化流式处理,使用NIO的FileChannel
-
信创环境网络不稳定
- 现象:上传过程中频繁断开
- 解决:实现自动重试机制,增加超时时间
5.2 性能优化建议
- 分片大小选择:经过测试,16MB在大多数场景下表现最佳
- 并发数设置:需要根据服务器性能动态调整
- 进度保存:将上传状态持久化到localStorage
- 断点续传:基于文件哈希和分片索引实现
5.3 安全注意事项
- 文件校验:合并前校验每个分片的完整性
- 权限控制:限制临时文件的访问权限
- 日志记录:详细记录上传下载操作
- 清理机制:定期清理过期临时文件
6. 扩展功能实现
6.1 文件夹上传
javascript复制async uploadFolder(folder) {
const entries = await readDirectory(folder);
for (const entry of entries) {
if (entry.isDirectory) {
await this.uploadFolder(entry);
} else {
await this.startUpload(entry.file);
}
}
}
6.2 断点续传实现
-
前端实现:
- 保存已上传分片索引到localStorage
- 重新上传时跳过已上传分片
-
后端实现:
- 检查分片是否已存在
- 支持分片覆盖上传
6.3 批量下载功能
javascript复制async batchDownload(files) {
const downloadTasks = files.map(file =>
this.downloadFile(file)
);
await Promise.all(downloadTasks);
}
7. 项目文档体系
完整的文档应包括:
-
系统设计文档
- 架构图
- 时序图
- 信创适配说明
-
前端开发手册
- 组件API
- 事件说明
- 调试技巧
-
后端接口文档
- Servlet配置
- 参数说明
- 错误码定义
-
- 环境要求
- 配置说明
- 常见问题
8. 项目成果与反思
经过三个月的开发和测试,系统已经:
- 在飞腾CPU+银河麒麟环境中稳定运行
- 成功处理超过100次的10GB文件上传
- 获得客户"比政务大厅WiFi还稳定"的评价
- 代码开源并形成内部技术资产
主要经验教训:
- 早期性能测试很重要:我们低估了信创环境的性能差异
- 文档要同步编写:后期补文档比开发还费时
- 监控不能少:需要实时掌握上传下载状态
- 用户反馈很宝贵:实际使用场景总能发现新问题
这个项目让我深刻体会到,在技术方案选型时,不能只考虑技术先进性,更要考虑实际运行环境和团队技术栈。有时候,"土办法"反而最管用。