基于Node.js Express的支持浏览器下载管理器断点续传的大文件下载服务
基于Node.js Express的大文件下载服务实现与完善
需求概述
- 基于Node.js Express开发下载服务,支持最大20GB的大文件传输
- 用户访问
https://my-url.com/api/download即可触发下载,需兼容浏览器内置下载管理器的暂停、恢复、取消操作:- 暂停:服务器停止发送文件数据
- 恢复:从上次中断的进度位置继续传输
- 取消:服务器立即终止下载流程
- 支持多用户同时下载,参考Ubuntu桌面版下载的实现逻辑
现有代码的问题分析
原代码的核心逻辑已具备Range请求处理能力,但存在以下影响稳定性与兼容性的问题:
- 同步IO阻塞:使用
fs.statSync()查询大文件状态会阻塞Event Loop,降低并发处理能力 - 错误处理缺失:未处理文件不存在、非法Range请求、文件读取失败等异常场景
- 进度计算错误:用当前请求的已发送字节除以整个文件大小计算进度,逻辑不符合实际
- 响应头不完整:缺少
Content-Disposition头,浏览器可能不会触发下载弹窗;未处理非Range请求的200状态码 - 流处理不严谨:手动调用
res.write(chunk)未处理背压,大文件传输时可能导致内存溢出 - Range参数未校验:未验证start/end值的合法性,可能引发无效的文件读取
完善后的代码
const express = require('express'); const router = express.Router(); const path = require('path'); const fs = require('fs').promises; const { createReadStream } = require('fs'); router.get('/api/download', async (req, res) => { const rootPath = path.join(__dirname, '../', 'download'); const filePath = path.join(rootPath, 'big-file.zip'); let fileStat; // 异步获取文件信息,避免阻塞Event Loop try { fileStat = await fs.stat(filePath); } catch (err) { if (err.code === 'ENOENT') { return res.status(404).send('文件不存在'); } return res.status(500).send('服务器内部错误'); } const fileSize = fileStat.size; let start = 0; let end = fileSize - 1; // 处理Range请求并校验参数合法性 const range = req.headers.range; if (range) { const parts = range.replace(/bytes=/, '').split('-'); start = parseInt(parts[0], 10); if (isNaN(start) || start >= fileSize) { return res.status(416).send('请求的范围超出文件大小'); } end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1; end = Math.min(end, fileSize - 1); } const chunkSize = end - start + 1; const headers = { 'Accept-Ranges': 'bytes', 'Content-Length': chunkSize, 'Content-Type': 'application/zip', // 触发浏览器下载弹窗,指定文件名 'Content-Disposition': 'attachment; filename="big-file.zip"' }; // 根据请求类型设置响应状态码 if (range) { headers['Content-Range'] = `bytes ${start}-${end}/${fileSize}`; res.writeHead(206, headers); } else { res.writeHead(200, headers); } // 创建文件可读流 const fileStream = createReadStream(filePath, { start, end }); // 处理文件读取错误 fileStream.on('error', (err) => { console.error('文件读取异常:', err); res.status(500).end(); }); // 使用pipe自动处理背压,替代手动write fileStream.pipe(res); // 监听请求关闭事件,终止文件流(对应暂停/取消操作) req.on('close', () => { console.log('下载已暂停或取消,终止文件流'); fileStream.destroy(); }); // 监听流结束事件 fileStream.on('end', () => { console.log('文件传输完成'); res.end(); }); }); module.exports = router;
核心改进说明
- 异步IO优化:使用
fs.promises.stat()替代同步方法,避免阻塞Event Loop,提升多用户并发处理能力 - 完整错误处理:覆盖文件不存在、非法Range请求、文件读取失败等场景,返回标准HTTP状态码
- 规范响应头:添加
Content-Disposition确保浏览器触发下载弹窗;区分Range请求(206)与普通请求(200)的状态码 - 流稳定性优化:用
pipe()处理文件流,自动管理背压,防止大文件传输时内存溢出 - 参数合法性校验:严格验证Range请求的start/end值,避免无效的文件读取操作
- 兼容浏览器操作:通过监听
req.close事件,在用户暂停/取消下载时立即终止文件流,符合浏览器下载管理器的交互逻辑
内容的提问来源于stack exchange,提问作者fw1983
相关产品推荐
相关产品推荐

