本篇使用 NodeJS 的 HTTP 服务建立客户端,使用 Range 请求实现下载功能,并经过本篇的 Demo 扩展在业务中实现断点续传等功能的思路。node
咱们经过 http
模块建立服务器处理 Range 请求,在服务器代码中咱们为了减小回调嵌套使用 async
函数,因此须要将异步的操做方法转换成 Promise,以往咱们使用 util
的 promisify
来一个一个转换异步方法,比较麻烦,咱们此次使用第三方模块 mz
并直接引入转换好的替代模块。npm
使用 mz
以前须要先安装:bash
npm install mz服务器
服务端代码以下:curl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
复制代码 |
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 请求的过程当中须要设置响应状态为 206
,须要设置响应头 Accept-Ranges
值为 bytes
,须要设置响应头 Content-Range
值为 byte 0-5/100
的格式,0
为返回数据开始的索引,5
为结束的索引(包含),100
为文件的总字节数。async
在经过 url
和 path
模块解析和拼接下载文件路径时,应该进行错误检测,若是文件不存在则直接返回客户端 Not Found
。函数
咱们可使用 curl
命令来检测咱们的服务端代码,在命令行工具中输入下面命令,在命令窗口查看返回值是否正确。工具
curl -v --header “Range:bytes=0-5” http://localhost:3000网站
在上面使用 curl
命令来访问咱们的服务器时,只能请求固定范围的数据,而不是相似于下载功能,每次都下载一个范围的数据,可是想要屡次下载并自动维护 Range 的范围须要借助咱们本身实现的客户端逻辑。
为了简便,咱们的下载客户端是在命令行窗口运行的,经过指令来模拟实际项目中的开始下载、暂停和恢复按钮,当在窗口中输入 s
指令时开始下载,输入 p
指令时暂停下载,输入 r
指令时恢复下载。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
复制代码 |
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
,这样的需求在一些上传、下载资源的网站也很常见,其目的就是为了让咱们实现断点续传,不至于一次没有上传或下载完成的资源文件,在下一次的作一样操做时须要从新来过,能够接着上次的位置继续,范围请求在视频网站上也普遍应用,边请求边观看,不至于一次加载整个视频资源,节省流量,节省时间。