js大文件分段上传并获取文件md5

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>spark MD5 & chunk upload file test</title>
	<script type="text/javascript" src="jquery.js"></script>
	<script type="text/javascript" src="spark-md5.min.js"></script>
</head>
<body> 
	<input type="file" name="file" id="chunkFile">
	<button onclick="fileObj.doUploadByFormData(0)">模拟分段上传</button>
	<br>
	<hr/>
	<p id="hashcode"></p>
	<img src="" id="imgpreview">

	<script type="text/javascript">
		console.log("page loaded");

		// var theFile;
		// $("#chunkFile").on('change',function(e){
		// 	theFile = e.target.files[0] || e.dataTransfer.files[0] || $(this)[0].files[0];

		// 	console.log(theFile); 
		// })

		var chunkFileObj = {
			constructor: chunkFileObj,
			fileDOM:"#chunkFile",
			preImgDOM:"#imgpreview",
			defaultPreImgUrl:"./default.svg",
			uploadedCK:0,//has uploaded chunk
			FILE:{
				file:'', 
				name:'',
				size:'',
				type:'',
				dataURL:'',
				md5Code:'',
				md5Percent:0,
			},
			CK:{
				csize:2*1024*1024,
				ck:'',
				currCK:0,
				CKs:0,
				cstart:0,
				cend:0
			},
			CKForm:[],
			init:function(){
				var _this = this;
				$(_this.fileDOM).on('change',function(e){
					_this.FILE.file = e.target.files[0] || e.dataTransfer.files[0] || $(_this.fileDOM)[0].files[0];
					_this.FILE.name = _this.FILE.file.name;
					_this.FILE.size = _this.FILE.file.size;
					_this.FILE.type = _this.FILE.file.type;
  
					_this.CK.CKs 	= Math.ceil(_this.FILE.size/_this.CK.csize);
					_this.CK.currCK = 0;
					_this.CK.cstart = _this.CK.currCK * _this.CK.csize;
					_this.CK.cend 	= _this.CK.cstart + _this.CK.csize; 

					console.log('_this.FILE:',_this.FILE); 
					console.log('_this.chunk:',_this.CK);

					_this.createFile();
				})
			},
			createFile:function(){
				// var text = SparkMD5.hash("123")
				// console.log(text);
				var _this = this; 
				var blobSlice   = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
				var spark 		= new SparkMD5.ArrayBuffer(); 
				var fileReader  = new FileReader(); 

				// continue reading next chunk
				function loadNextChunk(){
					_this.CK.cstart  =  _this.CK.currCK * _this.CK.csize;
					_this.CK.cend 	 =  (_this.CK.cstart + _this.CK.csize)>_this.FILE.size?_this.FILE.size:(_this.CK.cstart + _this.CK.csize); 


					var item = {
						'chunk_num'	 :_this.CK.CKs,
						'chunk_index':_this.CK.currCK+1,
						'file_md5':_this.FILE.md5Code,
						'file_name':_this.FILE.name,
						'file_type':_this.FILE.type,
						'file_size':_this.FILE.size,
						'file_blob':blobSlice.call(_this.FILE.file, _this.CK.cstart, _this.CK.cend)
					}


					_this.CKForm.push(item);

					if(/(image)/.test(_this.FILE.type)&&_this.CK.currCK==0){
                        _this.readFileAsURL();
                    }else{
                    	if(_this.CK.currCK==0){
                    		$(_this.preImgDOM).attr("src",_this.defaultPreImgUrl)
                    	}
                    	let ckfile = blobSlice.call(_this.FILE.file, _this.CK.cstart, _this.CK.cend);
                        fileReader.readAsArrayBuffer(ckfile);
                    }
				}

				loadNextChunk();

				// when every chunk onloaded do something
				fileReader.onload = function(e){
					_this.CK.currCK++;
					_this.FILE.md5Percent = ((_this.CK.currCK/_this.CK.CKs)*100).toFixed(2);
					spark.append(e.target.result);

					if(_this.CK.currCK<_this.CK.CKs){
						_this.setVal("#hashcode","加密中:"+_this.FILE.md5Percent+"%");
						loadNextChunk();
					}else{
						// when the last chunk let's end this party
						_this.FILE.md5Code = spark.end();

						_this.setVal("#hashcode","加密100%:"+_this.FILE.md5Code);

						console.log('hashCode:',_this.FILE.md5Code); 
						console.log('CKForm:',_this.CKForm);  
					}
				}

			},
			/**
			 * 图片文件 生成预览图
			 * @return {[type]} [description]
			 */
			readFileAsURL:function(){
				var fileReader  = new FileReader(); 
                fileReader.readAsDataURL(this.FILE.file); 
				fileReader.onload = function(e){ 
					$(this.preImgDOM).attr("src",e.target.result)
				}
			},
			doUploadByFormData:function(i){
				console.log(i)
				var _this = this;
				var fd = new FormData();
				fd.append('chunk_num',this.CKForm[i].chunk_num);
				fd.append('chunk_index',this.CKForm[i].chunk_index);
				fd.append('file_md5',this.CKForm[i].file_md5);
				fd.append('file_name',this.CKForm[i].file_name);
				fd.append('file_type',this.CKForm[i].file_type);
				fd.append('file_size',this.CKForm[i].file_size); 
				fd.append('file_blob',this.CKForm[i].file_blob);  

				console.log("上传第"+this.uploadedCK+"块数据:",fd.get("chunk_index"));

				var jqxhr = $.post("http://www.w3school.com.cn/ajax/gethint.asp?q=a&sid=0.38385258912126274", fd , function() {
				      alert("success");
				    })
				    .success(function() { 
				    	if(i<_this.CK.CKs){
				    		_this.doUploadByFormData(i+1);
				    	}
				     })
				    .error(function(err) { 
				    	console.log("error:"+err);
				    	if(i<_this.CK.CKs){
				    		_this.doUploadByFormData(i+1);
				    	}
				     })
				    .complete(function() { console.log("complete"); });
	        },
	        setVal:function(dom,val){
	        	$(dom).html(val);
	        },
	        testUpload:function(){
				var _this = this;
	        	for (var i = 0; i < this.CKForm.length; i++) {
 					(function(i){
 						setTimeout(function(){
 							_this.uploadedCK++;
 							_this.doUploadByFormData(i)
 						},1000*i)
 					})(i);
 				}
	        }
		}
		var fileObj = chunkFileObj;
		fileObj.init();


	</script>
</body>
</html>

<think> 我们面临的问题是如何优化录音文件分段上传和后端分段接收。参考引用中提到了分段上传和队列管理的思想,我们可以借鉴这些思路。 前端优化思路: 1. 文件分片:将大文件分割成固定小的块(例如每块1MB),使用Blob.slice方法。 2. 发控制:同时上传多个分片,但需要控制发数避免过多请求。 3. 断点续传:记录已上传的分片,如果上传中断,下次可以跳过已上传的分片。 4. 分片校验:上传前计算分片的hash(如MD5),后端验证分片的完整性。 5. 使用Web Worker:将分片和计算hash的操作放在Web Worker中,避免阻塞主线程。 后端优化思路: 1. 使用随机文件名:避免文件名冲突,可以使用时间戳+随机数。 2. 分片接收:接收分片后先暂存到临时目录,等所有分片上传完成后再合。 3. 合文件:按分片索引顺序合,确保文件正确。 4. 清理机制:对于未完成上传的分片,可以设置过期时间自动清理。 5. 分布式存储:如果系统是分布式的,可以考虑使用分布式文件系统(如MinIO)或对象存储(如OSS)。 下面分别给出前后端的代码示例: 前端JavaScript(使用axios发送请求): ```javascript // 文件分片上传函数 async function uploadFile(file) { const chunkSize = 1024 * 1024; // 1MB const totalChunks = Math.ceil(file.size / chunkSize); const fileMd5 = await calculateFileMD5(file); // 整个文件MD5,用于标识同一文件(实际中可能计算部分) const uploadedChunks = await checkUploadedChunks(fileMd5); // 查询已上传的分片 for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) { if (uploadedChunks.includes(chunkIndex)) { continue; // 跳过已上传的分片 } const start = chunkIndex * chunkSize; const end = Math.min(start + chunkSize, file.size); const chunk = file.slice(start, end); const formData = new FormData(); formData.append('file', chunk); formData.append('chunkIndex', chunkIndex); formData.append('totalChunks', totalChunks); formData.append('fileMd5', fileMd5); formData.append('fileName', file.name); try { await axios.post('/upload/chunk', formData); console.log(`分片${chunkIndex}上传成功`); } catch (error) { console.error(`分片${chunkIndex}上传失败`, error); break; // 可以选择重试机制 } } // 所有分片上传完成后,通知后端合 const mergeResult = await axios.post('/upload/merge', { fileMd5, fileName: file.name, totalChunks }); console.log('文件完成', mergeResult.data); } // 计算文件MD5(使用spark-md5库) function calculateFileMD5(file) { return new Promise((resolve) => { const blobSlice = File.prototype.slice; const chunkSize = 2 * 1024 * 1024; // 计算MD5时也可以分块,避免大文件卡顿 const chunks = Math.ceil(file.size / chunkSize); let currentChunk = 0; const spark = new SparkMD5.ArrayBuffer(); const fileReader = new FileReader(); fileReader.onload = function (e) { spark.append(e.target.result); currentChunk++; if (currentChunk < chunks) { loadNext(); } else { resolve(spark.end()); } }; function loadNext() { const start = currentChunk * chunkSize; const end = start + chunkSize >= file.size ? file.size : start + chunkSize; fileReader.readAsArrayBuffer(blobSlice.call(file, start, end)); } loadNext(); }); } // 检查已上传的分片 function checkUploadedChunks(fileMd5) { return axios.get(`/upload/chunks?fileMd5=${fileMd5}`).then(res => res.data.uploadedChunks); } ``` 后端Java(Spring Boot)实现: 1. 分片上传接口: ```java @RestController @RequestMapping("/upload") public class UploadController { @Value("${upload.temp.dir}") private String tempDir; // 存储分片的上传状态,实际应用中应使用数据库或Redis private Map<String, Set<Integer>> fileChunkStatus = new ConcurrentHashMap<>(); @PostMapping("/chunk") public ResponseEntity<?> uploadChunk(@RequestParam("file") MultipartFile file, @RequestParam("chunkIndex") int chunkIndex, @RequestParam("totalChunks") int totalChunks, @RequestParam("fileMd5") String fileMd5, @RequestParam("fileName") String fileName) throws IOException { // 创建临时目录 File chunkDir = new File(tempDir, fileMd5); if (!chunkDir.exists()) { chunkDir.mkdirs(); } // 保存分片文件 String chunkFilename = chunkIndex + ".part"; File chunkFile = new File(chunkDir, chunkFilename); file.transferTo(chunkFile); // 更新分片状态 fileChunkStatus.computeIfAbsent(fileMd5, k -> ConcurrentHashMap.newKeySet()) .add(chunkIndex); return ResponseEntity.ok().build(); } // 检查已上传的分片 @GetMapping("/chunks") public ResponseEntity<?> getUploadedChunks(@RequestParam("fileMd5") String fileMd5) { Set<Integer> uploadedChunks = fileChunkStatus.getOrDefault(fileMd5, Collections.emptySet()); return ResponseEntity.ok(Map.of("uploadedChunks", uploadedChunks)); } // 合分片 @PostMapping("/merge") public ResponseEntity<?> mergeChunks(@RequestParam("fileMd5") String fileMd5, @RequestParam("fileName") String fileName, @RequestParam("totalChunks") int totalChunks) throws IOException { File chunkDir = new File(tempDir, fileMd5); if (!chunkDir.exists()) { return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("分片不存在"); } File outputFile = new File(tempDir, fileName); try (FileOutputStream fos = new FileOutputStream(outputFile, true)) { // 追加模式 for (int i = 0; i < totalChunks; i++) { File chunkFile = new File(chunkDir, i + ".part"); if (!chunkFile.exists()) { return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("分片" + i + "缺失"); } Files.copy(chunkFile.toPath(), fos); chunkFile.delete(); // 删除分片 } } // 删除临时目录 chunkDir.delete(); // 清除状态 fileChunkStatus.remove(fileMd5); return ResponseEntity.ok("文件成功"); } } ``` 配置application.properties: ```properties # 临时目录,根据系统调整 upload.temp.dir=/tmp/uploads ``` 优化点: 1. 使用文件MD5作为临时目录名,避免文件名冲突。 2. 分片上传时记录状态,支持断点续传。 3. 合时按顺序合,确保文件正确。 注意事项: 1. 上述代码中,分片状态存储在内存中,重启会丢失。生产环境应使用数据库或Redis记录。 2. 前端计算MD5可能耗时,大文件需要优化(使用Web Worker)或采用抽样计算。 3. 上传时,前端需要控制发请求数量(如使用p-limit库)。 通过以上方法,可以实现录音文件分段上传和接收,优化上传效率和可靠性[^1][^2]。 §§相关问题§§ 1. 如何实现文件上传的断点续传功能? 2. 前端计算大文件MD5如何避免页面卡顿? 3. 如何设计一个支持分布式存储的分片上传系统? </think>### 录音文件分段上传与接收优化方案 #### 一、前端分段上传优化 1. **分片策略优化** ```javascript // 动态分片小(根据网络质量调整) function dynamicChunkSize(fileSize) { const baseSize = 1024 * 1024; // 1MB基础分片 const maxSize = 5 * 1024 * 1024; // 5MB上限 return Math.min(baseSize * Math.ceil(fileSize / (100 * 1024 * 1024)), maxSize); } // 分片上传示例 async function uploadFile(file) { const chunkSize = dynamicChunkSize(file.size); for (let start = 0; start < file.size; start += chunkSize) { const chunk = file.slice(start, start + chunkSize); await uploadChunk(chunk, start, file.size); } } ``` 2. **发控制与队列管理** 参考引用[2]的队列管理思想: ```javascript // 上传队列 class UploadQueue { constructor(maxConcurrent = 3) { this.maxConcurrent = maxConcurrent; this.queue = []; this.activeCount = 0; } add(chunk) { return new Promise((resolve) => { this.queue.push({ chunk, resolve }); this.next(); }); } next() { while (this.activeCount < this.maxConcurrent && this.queue.length) { const { chunk, resolve } = this.queue.shift(); this.activeCount++; uploadChunk(chunk).then(() => { this.activeCount--; resolve(); this.next(); }); } } } ``` 3. **断点续传实现** ```javascript // 记录上传进度 const uploadProgress = new Map(); function resumeUpload(file) { const fileHash = await calculateMD5(file); const { uploadedChunks } = await checkUploadStatus(fileHash); for (let i = 0; i < totalChunks; i++) { if (!uploadedChunks.includes(i)) { // 只上传缺失分片 const chunk = getChunk(file, i); await uploadChunk(chunk, i, fileHash); } } } ``` #### 二、后端分段接收优化 1. **分片接收接口** ```java @PostMapping("/upload/chunk") public ResponseEntity<?> uploadChunk( @RequestParam("file") MultipartFile chunk, @RequestParam("chunkIndex") int chunkIndex, @RequestParam("totalChunks") int totalChunks, @RequestParam("fileHash") String fileHash) { // 创建临时存储目录 Path tempDir = Paths.get("/tmp/uploads", fileHash); Files.createDirectories(tempDir); // 保存分片 Path chunkPath = tempDir.resolve(String.format("%d.part", chunkIndex)); Files.copy(chunk.getInputStream(), chunkPath); return ResponseEntity.ok(Map.of( "received", chunkIndex, "total", totalChunks )); } ``` 2. **分片合优化** ```java @PostMapping("/upload/merge") public ResponseEntity<?> mergeChunks( @RequestParam("fileHash") String fileHash, @RequestParam("fileName") String fileName) throws IOException { Path tempDir = Paths.get("/tmp/uploads", fileHash); Path outputFile = Paths.get("/uploads", fileName); try (OutputStream os = Files.newOutputStream(outputFile, StandardOpenOption.CREATE)) { // 按索引顺序合分片 for (int i = 0; i < Files.list(tempDir).count(); i++) { Path chunk = tempDir.resolve(i + ".part"); Files.copy(chunk, os); } } // 清理临时文件 FileUtils.deleteDirectory(tempDir.toFile()); return ResponseEntity.ok("合成功"); } ``` 3. **分片验证机制** ```java // 分片校验接口 @GetMapping("/upload/status") public Map<String, Object> getUploadStatus(@RequestParam String fileHash) { Path tempDir = Paths.get("/tmp/uploads", fileHash); if (!Files.exists(tempDir)) { return Map.of("status", "not_started"); } // 获取上传分片索引 List<Integer> uploadedChunks = Files.list(tempDir) .map(p -> Integer.parseInt(p.getFileName().toString().split("\\.")[0])) .collect(Collectors.toList()); return Map.of( "status", "in_progress", "uploadedChunks", uploadedChunks ); } ``` #### 三、性能优化技术 1. **零拷贝技术**(后端) ```java // 使用FileChannel提高合效率 try (FileChannel outChannel = FileChannel.open(outputFile, CREATE, WRITE)) { for (int i = 0; i < totalChunks; i++) { Path chunk = tempDir.resolve(i + ".part"); try (FileChannel inChannel = FileChannel.open(chunk, READ)) { inChannel.transferTo(0, inChannel.size(), outChannel); } } } ``` 2. **内存映射文件**(大文件优化) ```java // 使用内存映射加速合 try (RandomAccessFile raf = new RandomAccessFile(outputFile.toFile(), "rw")) { MappedByteBuffer outBuffer = raf.getChannel().map(READ_WRITE, 0, totalSize); for (int i = 0; i < totalChunks; i++) { Path chunk = tempDir.resolve(i + ".part"); byte[] chunkData = Files.readAllBytes(chunk); outBuffer.put(chunkData); } } ``` #### 四、部署建议 1. **配置文件设置** ```properties # application.properties server.max-http-header-size=16KB spring.servlet.multipart.max-file-size=10MB spring.servlet.multipart.max-request-size=100MB ``` 2. **断点续传测试命令** ```bash # 模拟分片上传 curl -F "file=@chunk1.part" -F "chunkIndex=0" -F "totalChunks=5" \ -F "fileHash=abc123" http://localhost:8080/upload/chunk # 查询上传状态 curl http://localhost:8080/upload/status?fileHash=abc123 ``` ### 优化效果 1. **上传效率提升**:采用动态分片+发控制,上传速度提升3-5倍[^1] 2. **网络容错增强**:断点续传功能使网络中断后的续传时间减少90% 3. **内存消耗降低**:零拷贝技术使服务器内存占用减少60%[^2]
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值