经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » JSJS库框架 » JavaScript » 查看文章
NodeJS使用Range请求实现下载功能的方法示例
来源:jb51  时间:2018/10/15 8:59:59  对本文有异议

前言

本篇使用 NodeJS 的 HTTP 服务创建客户端,使用 Range 请求实现下载功能,并通过本篇的 Demo 扩展在业务中实现断点续传等功能的思路。

服务端的实现

我们通过 http 模块创建服务器处理 Range 请求,在服务器代码中我们为了减少回调嵌套使用 async 函数,所以需要将异步的操作方法转换成 Promise,以往我们使用 util 的 promisify 来一个一个转换异步方法,比较麻烦,我们这次使用第三方模块 mz 并直接引入转换好的替代模块。

使用 mz 之前需要先安装:

  1. npm install mz

服务端代码如下:

  1. // 文件:server.js
  2. const http = require("http");
  3. const path = require("path");
  4. const url = require("url");
  5.  
  6. // 引入 mz 模块转换成 Promise 的 fs 模块
  7. const fs = require("mz/fs");
  8.  
  9. // 请求处理函数
  10. async function listener(req, res) {
  11. // 获取 range 请求头,格式为 Range:bytes=0-5
  12. let range = req.headers["range"];
  13.  
  14. // 下载文件路径
  15. let p = path.resovle(__dirname, url.parse(url, true).pathname);
  16.  
  17. // 存在 range 请求头将返回范围请求的数据
  18. if (range) {
  19. // 获取范围请求的开始和结束位置
  20. let [, start, end] = range.match(/(\d*)-(\d*)/);
  21.  
  22. // 错误处理
  23. try {
  24. let statObj = await fs.stat(p);
  25. } catch (e) {
  26. res.end("Not Found");
  27. }
  28.  
  29. // 文件总字节数
  30. let total = statObj.size;
  31.  
  32. // 处理请求头中范围参数不传的问题
  33. start = start ? ParseInt(start) : 0;
  34. end = end ? ParseInt(end) : total - 1;
  35.  
  36. // 响应客户端
  37. res.statusCode = 206;
  38. res.setHeader("Accept-Ranges", "bytes");
  39. res.setHeader("Content-Range", `bytes ${start}-${end}/${total}`);
  40. fs.createReadStream(p, { start, end }).pipe(res);
  41. } else {
  42. // 没有 range 请求头时将整个文件内容返回给客户端
  43. fs.createReadStream(p).pipe(res);
  44. }
  45. }
  46.  
  47. // 创建服务器
  48. const server = http.createServer(listener);
  49.  
  50. // 监听端口
  51. server.listen(3000, () => {
  52. console.log("server start 3000");
  53. });
  54.  

在上面服务端的代码中,需要兼容 Range 请求和普通请求,两种请求的区别是,如果客户端发送的是 Range 请求,会携带 Range:bytes=0-5 格式的请求头,我们可以通过 req 的 headers 属性获取,在获取请求头时,原本大写字母开头 NodeJS 统一处理成小写,所以获取时应小写。

如果是 Range 请求则通过可读流读取对应的内容返回客户端,如果不是,则通过可读流读取整个文件返回客户端,在响应 Range 请求的过程中需要设置响应状态为 206,需要设置响应头 Accept-Ranges 值为 bytes,需要设置响应头 Content-Range 值为 byte 0-5/100 的格式,0 为返回数据开始的索引,5 为结束的索引(包含),100 为文件的总字节数。

在通过 url 和 path 模块解析和拼接下载文件路径时,应该进行错误检测,如果文件不存在则直接返回客户端 Not Found。

我们可以使用 curl 命令来检测我们的服务端代码,在命令行工具中输入下面命令,在命令窗口查看返回值是否正确。

  1. curl -v --header "Range:bytes=0-5" http://localhost:3000

客户端的实现

在上面使用 curl 命令来访问我们的服务器时,只能请求固定范围的数据,而不是类似于下载功能,每次都下载一个范围的数据,但是想要多次下载并自动维护 Range 的范围需要借助我们自己实现的客户端逻辑。

为了简便,我们的下载客户端是在命令行窗口运行的,通过指令来模拟实际项目中的开始下载、暂停和恢复按钮,当在窗口中输入 s 指令时开始下载,输入 p 指令时暂停下载,输入 r 指令时恢复下载。

  1. // 文件:client.js
  2. const http = require("http");
  3. const fs = require("fs");
  4. const path = require("path");
  5.  
  6. // 请求配置
  7. let config = {
  8. host: "localhost",
  9. port: 3000,
  10. path: "/download.txt"
  11. };
  12.  
  13. let start = 0; // 请求初始值
  14. let step = 5; // 每次请求字符个数
  15. let pause = false; // 暂停状态
  16. let total; // 文件总长度
  17.  
  18. // 创建可写流
  19. let ws = fs.createWriteStream(path.resolve(__dirname, config.path.slice(1)));
  20.  
  21. // 下载函数
  22. function download() {
  23. // 配置,每次范围请求 step 个字节
  24. config.headers = {
  25. "Range": `bytes=${start}-${start + step - 1}`;
  26. };
  27.  
  28. // 维护下次 start 的值
  29. start += step;
  30.  
  31. // 发送请求
  32. http.request(config, res => {
  33. // 获取文件总长度
  34. if (typeof total !== "number") {
  35. total = res.headers["content-ranges"].match(/\/(\d*)/)[1];
  36.  
  37. }
  38.  
  39. // 读取返回数据
  40. let buffers = [];
  41. res.on("data", data => buffers.push(data));
  42. res.on("end", () => {
  43. // 合并数据并写入文件
  44. let buf = Buffer.concat(buffers);
  45. ws.write(buf);
  46.  
  47. // 递归进行下一次请求
  48. if (!pause && start < total) {
  49. download();
  50. }
  51. });
  52. }).end();
  53. }
  54.  
  55. // 监控输入
  56. process.stdin.on("data", data => {
  57. // 获取指令
  58. let ins = data.toString().match(/(\w*)\/r/)[1];
  59. switch (ins) {
  60. case "s":
  61. case "r":
  62. pause = false;
  63. download();
  64. break;
  65. case "p":
  66. pause = true;
  67. break;
  68. }
  69. });

在上面代码中下载的文件通过 config 中的 path 属性配置,每次调用 download 函数下载时都会重新计算当前范围请求的初始位置和结束位置,并设置 Range 请求头,下一次请求靠递归 download 来实现。

在执行时需先启动我们的服务器,在通过命令行输入 node client.js 来启动客户端,在命令窗口输入对应的指令进行开始下载、暂停下载和恢复下载操作。

总结

相信现在已经了解什么是范围请求,范围请求客户端和服务端需要做些什么,其实说白了就是对应的请求头和响应头的使用,需要注意的是范围请求的响应状态码为 206,这样的需求在一些上传、下载资源的网站也很常见,其目的就是为了让我们实现断点续传,不至于一次没有上传或下载完成的资源文件,在下一次的做同样操作时需要重新来过,可以接着上次的位置继续,范围请求在视频网站上也广泛应用,边请求边观看,不至于一次加载整个视频资源,节省流量,节省时间。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持w3xue。

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号