Range
Range
范围请求,允许服务器只发送 HTTP 消息的一部分到客户端。Range
可以一次性请求多个部分,请求范围用一个逗号分隔开,服务器会以multipart/byteranges
文件的形式将其返回。如果服务器返回的是范围响应,需要使用206 Partial Content
状态码。在请求的范围越界的情况下,服务器会返回416 Range Not Satisfiable (请求的范围无法满足)
状态码,表示客户端错误。服务器允许忽略Range
,从而返回整个文件,状态码用 200 。
相关状态码:
200
,206
,416
.
相关首部:Accept-Ranges
,Range
,Content-Range
,If-Range
,Transfer-Encoding
.
- index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>http range</title>
<style>
body {
margin: 0;
padding: 20px;
box-sizing: border-box;
}
</style>
</head>
<body>
<h1>HTTP Range</h1>
<hr />
<h3 class="result"></h3>
<script>
fetchData()
async function fetchData() {
const url = 'http://localhost:3000/range';
const res = await fetch(url, {
headers: {
Range: `bytes=0-2048`,
},
}).then((resp) => resp.blob());
console.log("res => ", res)
// open(URL.createObjectURL(res), '_blank');
const img = new Image()
img.onload = () => {
document.querySelector('.result').appendChild(img)
}
img.src = URL.createObjectURL(res)
// document.querySelector('.result').textContent = res;
}
</script>
</body>
</html>
- server.mjs
import { createServer } from 'node:http';
import { readFile, stat, createReadStream } from 'node:fs';
import { parse } from 'node:url';
import { promisify } from 'node:util';
const filePath = './favicon.ico';
const contentType = 'image/vnd.microsoft.icon';
createServer(async (req, res) => {
const { pathname } = parse(req.url);
res.setHeader('Access-Control-Allow-Origin', '*');
console.log('pathname => ', req.method, pathname);
if (pathname === '/') {
const html = await promisify(readFile)('./range.html');
res.setHeader('Content-Type', 'text/html;charset=utf-8');
res.end(html);
} else if (pathname === '/range') {
if (req.method.toUpperCase() === 'OPTIONS') {
res.setHeader(
'Access-Control-Allow-Headers',
'Accept-Ranges, Range, Content-Type, Content-Length'
);
res.statusCode = 200;
return res.end();
}
const { size: fileSize } = await promisify(stat)(filePath);
const headerRange = req.headers['range'];
console.log('headerRange => ', headerRange);
res.setHeader('Accept-Ranges', 'bytes');
res.setHeader('Content-Type', contentType);
// 分片传输
if (headerRange) {
// 只获取一个range
const reg = /^bytes=\d+$/;
if (reg.test(headerRange)) {
const index = +headerRange.split('=')[1];
if (index >= fileSize) return handleBoudary(res);
return createReadStream(filePath, { start: index, end: index }).pipe(res);
}
const ranges = headerRange
.replace('bytes=', '')
.split(',')
.map((item) => item.trim().split('-'));
console.log('ranges => ', ranges);
// 处理越界情况
const boundaryStatus = ranges.some(([start, end]) => {
return start >= fileSize || end >= fileSize;
});
if (boundaryStatus) return handleBoudary(res);
// 处理多个range
if (ranges.length > 1) {
const range_divider = String(Math.random()).slice(2);
res.statusCode = 206;
res.setHeader('Content-Type', `multipart/byteranges; boundary=${range_divider}`);
const chunksPromise = ranges.map(([start, end]) => {
// 从结尾开始获取
console.log("start => ", start, end)
if (start === '' && end !== '') {
start = fileSize - parseInt(end);
end = fileSize - 1;
} else {
start = parseInt(start);
end = end ? parseInt(end) : fileSize - 1;
}
return getChunk(start, end, filePath);
});
const chunks = await Promise.all(chunksPromise);
let list = [];
for (const { start, end, chunk } of chunks) {
const str = [
`\r\n--${range_divider}`,
`Content-Type: ${contentType}`,
`Content-Range: bytes ${start}-${end}/${fileSize}`,
'',
chunk,
].join('\r\n');
list.push(str);
}
const result = list.join('') + `\r\n--${range_divider}--`;
return res.end(result);
}
// 处理单个range (部分内容)
let [start, end] = ranges[0];
// 从结尾开始获取
if (start === '' && end !== '') {
start = fileSize - parseInt(end);
end = fileSize - 1;
} else {
start = parseInt(start);
end = end ? parseInt(end) : fileSize - 1;
}
res.writeHead(206, {
'Content-Length': end - start + 1,
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
});
const readStream = createReadStream(filePath, { start, end });
return readStream.pipe(res);
}
// 完整的内容
res.statusCode = 200;
res.setHeader('Content-Length', fileSize);
createReadStream(filePath).pipe(res);
} else {
res.statusCode = 404;
res.end('Not found');
}
}).listen(3000, () => {
console.log('server runnning in 3000');
});
function handleBoudary(res) {
res.statusCode = 416
res.end('请求范围不符合要求')
}
function getChunk(start, end, filePath) {
return new Promise((resolve) => {
const readStream = createReadStream(filePath, { start, end });
let chunk = '';
readStream.on('data', (data) => {
chunk += data;
});
readStream.on('end', () => {
resolve({ start, end, chunk });
// console.log('end');
});
});
}
标签:const,res,http,start,Range,fileSize,end
From: https://www.cnblogs.com/chlai/p/18133602