nodejs 实现 multipart/byteranges 分块下载
引言
最近接触到了文件下载,一般默认返回 Content-Type: application/octet-stream 文件流,支持单范围请求,而多范围请求需要自我手动实现。
一、HTTP 范围请求基础
1.1 Range 请求头格式
客户端通过Range
请求头指定需要获取的字节范围,格式如下:
http
sql
Range: bytes=start-end, start-end
-
单范围:
bytes=0-1023
(获取前 1024 字节) -
多范围:
bytes=0-500, 1000-2000
(获取两个不连续范围) -
后缀范围:
bytes=-500
(获取最后 500 字节)
1.2 服务器响应格式
-
单范围响应 :返回
206 Partial Content
状态码,包含Content-Range
头 -
多范围响应 :返回
multipart/byteranges
类型,使用 boundary 分隔各块
二、Node.js实现方案
2.1核心实现代码
javascript
解析rang参数,获取分块开始位置和结束位置
javascript
const { ctx } = this;
const fileSize, rangeHeader, filePath;
const ranges = [];
let match;
if (rangeHeader.startsWith('bytes=')) {
match = rangeHeader.replace('bytes=', '').split(',').map(vl => (vl && vl.trim()));
for (let i = 0; i < match.length; i++) {
let val = match[i];
if (val) {
const [startStr, endStr] = val.split('-');
let start = Number(startStr);
let end = endStr ? Number(endStr) : fileSize - 1;
start = isNaN(start) ? 0 : Math.max(0, start);
end = isNaN(end) ? fileSize - 1 : Math.min(end, fileSize - 1);
if (start <= end) {
ranges.push({ start, end });
}
}
}
}
// 解析不成功,返回失败
if (ranges.length === 0) {
ctx.status = 416;
ctx.set('Content-Range', `bytes */${fileSize}`);
ctx.body = 'Range Not Satisfiable';
return;
} else if (ranges.length === 1) {
// 只有一块,按单范围返回
const { start, end } = ranges[0];
ctx.set('Content-Length', end - start + 1);
ctx.body = createReadStream(filePath, { start, end });
return;
}
设置响应头,分块获取文件后拼接响应体
javascript
const stream = ctx.res
const boundary = Math.floor(Math.random() * 100000000)
stream.writeHead(206, {
'Content-Length': contentLength,
'Content-Type': `multipart/byteranges; boundary=${boundary}`,
'Accept-Ranges': 'bytes'
})
for (let i = 0; i < ranges.length; i++) {
const { start, end } = ranges[i];
stream.write(`--${boundary}\r\n`);
stream.write(`Content-Type: application/octet-stream\r\n`);
stream.write(`Content-Range: bytes ${start}-${end}/${fileSize}\r\n\r\n`);
await new Promise((resolve, reject) => {
const readStream = createReadStream(filePath, { start, end });
readStream.on('data', (chunk) => {
stream.write(chunk);
})
readStream.on('close', resolve);
readStream.on('error', reject);
});
stream.write('\r\n');
}
stream.write(`--${boundary}--\r\n`);
stream.end();