本文介绍如何利用NodeJS的HTTP模块处理Range请求,实现服务端对文件片段的响应及客户端的分段下载逻辑。通过示例代码展示如何解析请求头、设置206状态码及Content-Range响应头,并结合命令行客户端模拟下载、暂停与恢复操作,为业务中的断点续传功能提供实现思路。
前言
本文介绍如何使用NodeJS的HTTP服务构建客户端,通过处理Range请求来实现文件下载功能,并基于此Demo扩展在业务场景中实现断点续传等功能的思路。
服务端实现
服务端实现
我们利用http模块创建服务器以处理Range请求。在服务器代码中,为了减少回调嵌套带来的复杂性,我们采用async函数,因此需要将异步操作方法转换为Promise。以往通常使用util模块的promisify函数逐个转换异步方法,较为繁琐;本次我们引入第三方模块mz,直接引入已转换好的替代模块以简化操作。
在使用mz模块前,需先执行安装命令:
npm install mz
服务端核心代码示例如下:
// 文件:server.js
const http = require("http");
const path = require("path");
const url = require("url");
// 引入 mz 模块转换成 Promise 的 fs 模块
const fs = require("mz/fs");
// 请求处理函数
async function listener(req, res) {
// 获取 range 请求头,格式为 Range:bytes=0-5
let range = req.headers["range"];
// 下载文件路径
let p = path.resovle(__dirname, url.parse(url, true).pathname);
// 存在 range 请求头将返回范围请求的数据
if (range) {
// 获取范围请求的开始和结束位置
let [, start, end] = range.match(/(\d*)-(\d*)/);
// 错误处理
try {
let statObj = await fs.stat(p);
} catch (e) {
res.end("Not Found");
}
// 文件总字节数
let total = statObj.size;
// 处理请求头中范围参数不传的问题
start = start ? ParseInt(start) : 0;
end = end ? ParseInt(end) : total - 1;
// 响应客户端
res.statusCode = 206;
res.setHeader("Accept-Ranges", "bytes");
res.setHeader("Content-Range", `bytes ${start}-${end}/${total}`);
fs.createReadStream(p, { start, end }).pipe(res);
} else {
// 没有 range 请求头时将整个文件内容返回给客户端
fs.createReadStream(p).pipe(res);
}
}
// 创建服务器
const server = http.createServer(listener);
// 监听端口
server.listen(3000, () => {
console.log("server start 3000");
});
上述服务端代码需同时兼容Range请求与普通请求。两者的区别在于,若客户端发送的是Range请求,其请求头中会包含类似Range:bytes=0-5的字段。我们可以通过req对象的headers属性获取该请求头。需要注意的是,NodeJS在处理请求头时会将原本大写字母开头的键统一转换为小写,因此在获取时应使用小写形式。
若检测到为Range请求,则通过可读流读取指定范围的内容并返回给客户端;若非Range请求,则通过可读流读取整个文件内容返回。在处理Range响应时,需将响应状态码设置为206,并在响应头中设置Accept-Ranges为bytes,同时设置Content-Range为类似byte 0-5/100的格式,其中0表示返回数据的起始索引,5表示结束索引(包含该字节),100表示文件的总字节数。
在使用url和path模块解析及拼接下载文件路径时,必须进行错误检测。若指定文件不存在,应直接向客户端返回Not Found错误信息。
我们可以使用curl命令来验证服务端代码的正确性。在命令行工具中执行相关命令,并在命令窗口中检查返回值是否符合预期。
curl -v --header "Range:bytes=0-5" http://localhost:3000
客户端实现
客户端实现
在使用curl命令访问服务器时,只能请求固定范围的数据,这并不等同于完整的下载功能,因为每次仅下载一个固定范围的数据。若需实现多次下载并自动维护Range请求的范围,则需要借助自行实现的客户端逻辑。
为简化演示,本下载客户端运行于命令行窗口,通过指令模拟实际项目中开始下载、暂停和恢复按钮的功能。当在窗口中输入s指令时开始下载,输入p指令时暂停下载,输入r指令时恢复下载。
// 文件:client.js
const http = require("http");
const fs = require("fs");
const path = require("path");
// 请求配置
let config = {
host: "localhost",
port: 3000,
path: "/download.txt"
};
let start = 0; // 请求初始值
let step = 5; // 每次请求字符个数
let pause = false; // 暂停状态
let total; // 文件总长度
// 创建可写流
let ws = fs.createWriteStream(path.resolve(__dirname, config.path.slice(1)));
// 下载函数
function download() {
// 配置,每次范围请求 step 个字节
config.headers = {
"Range": `bytes=${start}-${start + step - 1}`;
};
// 维护下次 start 的值
start += step;
// 发送请求
http.request(config, res => {
// 获取文件总长度
if (typeof total !== "number") {
total = res.headers["content-ranges"].match(/\/(\d*)/)[1];
}
// 读取返回数据
let buffers = [];
res.on("data", data => buffers.push(data));
res.on("end", () => {
// 合并数据并写入文件
let buf = Buffer.concat(buffers);
ws.write(buf);
// 递归进行下一次请求
if (!pause && start < total) {
download();
}
});
}).end();
}
// 监控输入
process.stdin.on("data", data => {
// 获取指令
let ins = data.toString().match(/(\w*)\/r/)[1];
switch (ins) {
case "s":
case "r":
pause = false;
download();
break;
case "p":
pause = true;
break;
}
});
在上述代码中,下载的文件路径通过config对象中的path属性进行配置。每次调用download函数下载时,都会重新计算当前范围请求的初始位置和结束位置,并设置相应的Range请求头。后续的请求通过递归调用download函数来实现。
执行时,需先启动服务器,随后通过命令行输入node client.js来启动客户端。在命令窗口中输入对应的指令,即可进行开始下载、暂停下载和恢复下载的操作。
总结
总结
至此,相信读者已了解什么是范围请求,以及范围请求中客户端和服务端所需执行的操作。其核心在于对应的请求头和响应头的正确使用,特别是范围请求的响应状态码为206。此类需求在一些上传、下载资源的网站中十分常见,其目的正是为了实现断点续传,避免资源文件在一次上传或下载未完成时,下次操作需重新从头开始,而是可以接着上次的位置继续。此外,范围请求在视频网站中也被广泛应用,支持边请求边观看,无需一次性加载整个视频资源,从而节省流量和时间。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。