求助:向Node.js发送二进制文件分片后重构文件异常问题
解决文件分片上传后重构格式错误的问题
嘿,我之前也踩过这个坑!文件分片重构后格式不对,十有八九是后端写入时的编码处理出了问题——毕竟Node.js的fs.createWriteStream默认如果没明确配置,很容易把二进制数据当成字符串来处理,直接破坏了原始文件的字节结构。下面是具体的排查和解决方法:
核心问题分析
前端用file.slice切出的是Blob(二进制数据),浏览器发送的也是原始字节流,但后端如果在写入时错误地指定了编码(比如默认的utf8),会把Buffer转换成字符串再写入,这会导致二进制文件(比如图片、视频、压缩包)的字节被篡改,最终文件损坏。
解决方案步骤
1. 前端确保二进制发送正确
发送分片时直接传递file.slice得到的Blob,不要做额外的编码转换,同时设置正确的请求头:
const file = document.querySelector('input[type="file"]').files[0]; const chunkSize = 1024 * 1024; // 1MB分片大小 const totalChunks = Math.ceil(file.size / chunkSize); async function uploadChunks() { for (let i = 0; i < totalChunks; i++) { const start = i * chunkSize; const end = Math.min(start + chunkSize, file.size); const chunk = file.slice(start, end); const xhr = new XMLHttpRequest(); xhr.open('POST', '/upload-chunk', true); // 明确告诉后端这是二进制流 xhr.setRequestHeader('Content-Type', 'application/octet-stream'); // 传递分片索引、总数、文件名等元信息 xhr.setRequestHeader('Chunk-Index', i); xhr.setRequestHeader('Total-Chunks', totalChunks); xhr.setRequestHeader('File-Name', file.name); await new Promise((resolve, reject) => { xhr.onload = () => resolve(); xhr.onerror = () => reject(new Error('分片上传失败')); xhr.send(chunk); // 直接发送Blob,无需转换 }); } } // 触发上传 document.querySelector('#upload-btn').addEventListener('click', uploadChunks);
2. 后端正确接收并写入二进制数据
关键是不要设置编码,让fs.createWriteStream直接处理Buffer。以Express为例:
const express = require('express'); const fs = require('fs'); const path = require('path'); const app = express(); // 用raw中间件接收二进制数据,不要用json/urlencoded中间件 app.use(express.raw({ type: 'application/octet-stream', limit: '10mb' })); // 临时存储分片的目录 const tempChunkDir = path.join(__dirname, 'temp-chunks'); const uploadDir = path.join(__dirname, 'uploads'); // 创建必要的目录 [tempChunkDir, uploadDir].forEach(dir => { if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); }); app.post('/upload-chunk', (req, res) => { const chunkIndex = parseInt(req.headers['chunk-index']); const totalChunks = parseInt(req.headers['total-chunks']); const fileName = req.headers['file-name']; // 写入分片到临时文件,关键:encoding设为null,直接处理Buffer const chunkPath = path.join(tempChunkDir, `${fileName}.chunk${chunkIndex}`); const writeStream = fs.createWriteStream(chunkPath, { encoding: null }); writeStream.write(req.body); // req.body是原始Buffer writeStream.end(); writeStream.on('finish', async () => { // 检查所有分片是否已上传 const allChunksUploaded = Array.from({ length: totalChunks }, (_, i) => { return fs.existsSync(path.join(tempChunkDir, `${fileName}.chunk${i}`)); }).every(Boolean); if (allChunksUploaded) { // 合并所有分片到最终文件 const finalFilePath = path.join(uploadDir, fileName); const finalWriteStream = fs.createWriteStream(finalFilePath, { encoding: null }); for (let i = 0; i < totalChunks; i++) { const chunkBuffer = fs.readFileSync(path.join(tempChunkDir, `${fileName}.chunk${i}`)); finalWriteStream.write(chunkBuffer); // 删除临时分片 fs.unlinkSync(path.join(tempChunkDir, `${fileName}.chunk${i}`)); } finalWriteStream.end(); finalWriteStream.on('finish', () => { res.status(200).send('文件上传并合并完成!'); }); } else { res.status(200).send('分片上传成功,等待其余分片'); } }); writeStream.on('error', err => { console.error('分片写入失败:', err); res.status(500).send('分片上传失败'); }); }); app.listen(3000, () => { console.log('服务器运行在 http://localhost:3000'); });
额外注意事项
- 禁止转换编码:绝对不要把
req.body转成字符串(比如req.body.toString()),直接写入Buffer。 - 分片顺序要严格:合并时必须按分片索引从小到大写入,否则文件会乱序损坏。
- 边界处理:前端
file.slice的end值要取Math.min(start + chunkSize, file.size),避免最后一个分片出现空字节。 - 大文件优化:如果处理超大文件,建议用
fs.createReadStream读取分片再写入,避免一次性读取Buffer占用过多内存。
内容的提问来源于stack exchange,提问作者k1r0s
相关产品推荐
相关产品推荐

