http Range
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]) => { // 从结尾开始获取 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'); }); }); }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!