三语言实现大文件分片上传实战
发布时间:2026/10/18 8:14:10 锦皓数字建站

文件上传功能实战Java、Python、PHP 实现大文件分片上传在现代 Web 应用开发中大文件上传是一个常见且复杂的场景。传统一次性上传方式在面对 100MB 以上文件时容易因网络波动、超时或内存溢出导致失败。分片上传Chunked Upload通过将大文件拆分为多个小块并逐块上传显著提升了上传成功率与用户体验。本文将以 Java、Python、PHP 三种主流语言为例详细实现一套完整的分片上传系统涵盖前端控制、后端处理与断点续传逻辑。一、分片上传核心原理分片上传的核心思想是将大文件按固定大小如 5MB切分为若干数据块每个块独立上传并在服务端记录上传状态。上传完成后服务端合并所有分片生成完整文件。关键点包括分片大小设定建议 2~10MB唍验唯一标识如文件指纹md5上传进度追踪断点续传支持二、前端实现基于 JavaScript 的分片上传!DOCTYPE html html langzh head meta charsetUTF-8 / title大文件分片上传/title /head body input typefile idfileInput / button onclickuploadFile()开始上传/button div idprogress/div script const CHUNK_SIZE 5 * 1024 * 1024; // 5MB let fileBlob, chunks, totalChunks; document.getElementById(fileInput).addEventListener(change, e { fileBlob e.target.files[0]; totalChunks Math.ceil(fileBlob.size / CHUNK_SIZE); chunks []; for (let i 0; i totalChunks; i) { chunks.push(fileBlob.slice(i * CHUNK_SIZE, (i 1) * CHUNK_SIZE)); } }); async function uploadFile() { const formData new FormData(); const fileHash await getFileHash(fileBlob); // 生成文件指纹 formData.append(fileHash, fileHash); formData.append(totalChunks, totalChunks); const uploadedChunks await getUploadedChunks(fileHash); const pendingChunks chunks.filter((_, idx) !uploadedChunks.includes(idx)); const progressDiv document.getElementById(progress); let successCount 0; for (const [idx, chunk] of pendingChunks.entries()) { formData.set(chunkIndex, idx); formData.set(chunk, chunk); try { const res await fetch(/api/upload/chunk, { method: POST, body: formData, }); const result await res.json(); if (result.success) { successCount; progressDiv.innerHTML 已上传 ${successCount}/${pendingChunks.length} 个分片; } } catch (err) { console.error(上传失败:, err); } } // 所有分片上传完成触发合并 if (successCount pendingChunks.length) { await mergeChunks(fileHash); alert(上传完成); } } // 简化版文件哈希计算实际可用 crypto-js async function getFileHash(file) { return new Promise((resolve) { const reader new FileReader(); reader.onload () { const array new Uint8Array(reader.result); let hash 0; for (let i 0; i array.length; i) { hash ((hash 5) - hash) array[i]; hash | 0; } resolve(hash.toString(16)); }; reader.readAsArrayBuffer(file.slice(0, 1024)); // 取前1KB计算 }); } async function getUploadedChunks(fileHash) { const res await fetch(/api/upload/status?fileHash${fileHash}); const data await res.json(); return data.uploaded || []; } async function mergeChunks(fileHash) { await fetch(/api/upload/merge?fileHash${fileHash}, { method: POST }); } /script /body /html三、Java 后端实现Spring Boot使用 Spring Boot 搭建分片上传接口核心逻辑如下RestController RequestMapping(/api/upload) public class ChunkUploadController { private final String UPLOAD_DIR /tmp/uploads/; PostMapping(/chunk) public ResponseEntityMapString, Object uploadChunk( RequestParam(fileHash) String fileHash, RequestParam(chunkIndex) int chunkIndex, RequestParam(chunk) MultipartFile chunk) { Path dirPath Paths.get(UPLOAD_DIR fileHash); if (!Files.exists(dirPath)) { try { Files.createDirectories(dirPath); } catch (IOException e) { return ResponseEntity.status(500).body(Map.of(error, 创建目录失败)); } } Path filePath dirPath.resolve(String.format(%d.dat, chunkIndex)); try { Files.write(filePath, chunk.getBytes()); } catch (IOException e) { return ResponseEntity.status(500).body(Map.of(error, 保存分片失败)); } // 记录已上传分片 ListInteger uploaded getUploadedChunks(fileHash); uploaded.add(chunkIndex); saveUploadedChunks(fileHash, uploaded); return ResponseEntity.ok(Map.of(success, true, chunkIndex, chunkIndex)); } GetMapping(/status) public ResponseEntityMapString, Object getStatus(RequestParam(fileHash) String fileHash) { ListInteger uploaded getUploadedChunks(fileHash); return ResponseEntity.ok(Map.of(uploaded, uploaded)); } PostMapping(/merge) public ResponseEntityMapString, Object mergeChunks(RequestParam(fileHash) String fileHash) { Path dirPath Paths.get(UPLOAD_DIR fileHash); if (!Files.exists(dirPath)) { return ResponseEntity.badRequest().body(Map.of(error, 分片不存在)); } ListPath sortedFiles Files.list(dirPath) .sorted(Comparator.comparing(p - Integer.parseInt(p.getFileName().toString().split(\\.)[0]))) .toList(); Path targetPath Paths.get(UPLOAD_DIR fileHash .final); try (OutputStream out Files.newOutputStream(targetPath)) { for (Path file : sortedFiles) { Files.copy(file, out, StandardCopyOption.REPLACE_EXISTING); } } catch (IOException e) { return ResponseEntity.status(500).body(Map.of(error, 合并失败)); } // 清理临时分片 try { Files.walk(dirPath).forEach(path - { try { if (!path.equals(dirPath)) Files.delete(path); } catch (IOException ignored) {} }); } catch (IOException ignored) {} return ResponseEntity.ok(Map.of(success, true, filename, fileHash .final)); } private ListInteger getUploadedChunks(String fileHash) { // 从缓存或数据库读取 return new ArrayList(Arrays.asList(0, 1, 3)); // 示例 } private void saveUploadedChunks(String fileHash, ListInteger chunks) { // 实际应写入 Redis / DB } }四、Python 后端实现Flaskfrom flask import Flask, request, jsonify import hashlib import os import shutil app Flask(__name__) UPLOAD_DIR /tmp/uploads/ app.route(/api/upload/chunk, methods[POST]) def upload_chunk(): file_hash request.form[fileHash] chunk_index int(request.form[chunkIndex]) chunk request.files[chunk] dir_path os.path.join(UPLOAD_DIR, file_hash) if not os.path.exists(dir_path): os.makedirs(dir_path) chunk_path os.path.join(dir_path, f{chunk_index}.dat) chunk.save(chunk_path) # 记录已上传分片可替换为 Redis uploaded_file os.path.join(dir_path, .uploaded) with open(uploaded_file, a) as f: f.write(f{chunk_index}\n) return jsonify({success: True, chunkIndex: chunk_index}) app.route(/api/upload/status) def get_status(): file_hash request.args.get(fileHash) dir_path os.path.join(UPLOAD_DIR, file_hash) uploaded [] uploaded_file os.path.join(dir_path, .uploaded) if os.path.exists(uploaded_file): with open(uploaded_file, r) as f: uploaded [int(line.strip()) for line in f.readlines()] return jsonify({uploaded: uploaded}) app.route(/api/upload/merge, methods[POST]) def merge_chunks(): file_hash request.args.get(fileHash) dir_path os.path.join(UPLOAD_DIR, file_hash) target_path os.path.join(UPLOAD_DIR, f{file_hash}.final) files [f for f in os.listdir(dir_path) if f.endswith(.dat)] sorted_files sorted(files, keylambda x: int(x.split(.)[0])) with open(target_path, wb) as target: for fname in sorted_files: with open(os.path.join(dir_path, fname), rb) as src: target.write(src.read()) # 清理临时分片 shutil.rmtree(dir_path) return jsonify({success: True, filename: f{file_hash}.final}) if __name__ __main__: app.run(port5000)五、PHP 后端实现原生?php $uploadDir /tmp/uploads/; $fileHash $_POST[fileHash] ?? ; $chunkIndex (int)$_POST[chunkIndex]; $chunk $_FILES[chunk]; if (!$fileHash || !$chunk[size]) { http_response_code(400); echo json_encode([error 参数缺失]); exit; } $dirPath $uploadDir . $fileHash; if (!is_dir($dirPath)) { mkdir($dirPath, 0755, true); } $filePath $dirPath . / . $chunkIndex . .dat; move_uploaded_file($chunk[tmp_name], $filePath); // 记录已上传分片 $uploadedFile $dirPath . /.uploaded; file_put_contents($uploadedFile, $chunkIndex . \n, FILE_APPEND | LOCK_EX); echo json_encode([success true, chunkIndex $chunkIndex]); ?六、总结与优化建议安全性对上传路径进行白名单校验防止路径穿越。性能使用 Redis 缓存上传状态避免频繁磁盘操作。扩展性引入任务队列如 RabbitMQ处理大文件合并。日志监控记录上传时间、大小、失败原因便于排查。分片上传不仅是技术实现更是对高并发、高可靠系统的考验。掌握这套方案可轻松应对企业级文件传输需求。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。