NodeJS使用Range请求实现下载功能的方法示例


Posted in NodeJs onOctober 12, 2018

前言

本篇使用 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 请求的过程中需要设置响应状态为 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,这样的需求在一些上传、下载资源的网站也很常见,其目的就是为了让我们实现断点续传,不至于一次没有上传或下载完成的资源文件,在下一次的做同样操作时需要重新来过,可以接着上次的位置继续,范围请求在视频网站上也广泛应用,边请求边观看,不至于一次加载整个视频资源,节省流量,节省时间。

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

NodeJs 相关文章推荐
nodeJs爬虫获取数据简单实现代码
Mar 29 NodeJs
Nodejs获取网络数据并生成Excel表格
Mar 31 NodeJs
nodejs加密Crypto的实例代码
Jul 07 NodeJs
NodeJs下的测试框架Mocha的简单介绍
Feb 22 NodeJs
NodeJs form-data格式传输文件的方法
Dec 13 NodeJs
nodejs多版本管理总结
Apr 03 NodeJs
NodeJS服务器实现gzip压缩的示例代码
Oct 12 NodeJs
nodejs实现UDP组播示例方法
Nov 04 NodeJs
在NodeJs中使用node-schedule增加定时器任务的方法
Jun 08 NodeJs
ubuntu系统下使用pm2设置nodejs开机自启动的方法
May 12 NodeJs
浅谈JS和Nodejs中的事件驱动
May 05 NodeJs
浅谈Node的内存泄露问题
May 06 NodeJs
nodejs实现范围请求的实现代码
Oct 12 #NodeJs
NodeJS搭建HTTP服务器的实现步骤
Oct 12 #NodeJs
NodeJS服务器实现gzip压缩的示例代码
Oct 12 #NodeJs
nodejs aes 加解密实例
Oct 10 #NodeJs
nodejs读取本地中文json文件出现乱码解决方法
Oct 10 #NodeJs
nodejs require js文件入口,在package.json中指定默认入口main方法
Oct 10 #NodeJs
nodejs更新package.json中的dependencies依赖到最新版本的方法
Oct 10 #NodeJs
You might like
使用php验证复选框有效性的示例
2013/11/13 PHP
php实现微信公众平台账号自定义菜单类
2014/12/02 PHP
PHP实现的数组和XML文件相互转换功能示例
2018/03/15 PHP
PHP设计模式之委托模式定义与用法简单示例
2018/08/13 PHP
javascript 解析后的xml对象的读取方法细解
2009/07/25 Javascript
JS 打印功能代码可实现打印预览、打印设置等
2014/10/31 Javascript
Javascript Object 对象学习笔记
2014/12/17 Javascript
js中substr,substring,indexOf,lastIndexOf,split,replace的用法详解
2015/11/09 Javascript
js仿小米手机上下滑动效果
2017/02/05 Javascript
Vue 2.x教程之基础API
2017/03/06 Javascript
Nodejs+express+ejs简单使用实例代码
2017/09/18 NodeJs
vue2.0$nextTick监听数据渲染完成之后的回调函数方法
2018/09/11 Javascript
vue canvas绘制矩形并解决由clearRec带来的闪屏问题
2019/09/02 Javascript
[05:08]2014DOTA2国际邀请赛 Hao专访复仇的胜利很爽
2014/07/15 DOTA
python定时采集摄像头图像上传ftp服务器功能实现
2013/12/23 Python
Python 获取新浪微博的最新公共微博实例分享
2014/07/03 Python
Python读取指定目录下指定后缀文件并保存为docx
2017/04/23 Python
python利用MethodType绑定方法到类示例代码
2017/08/27 Python
实例介绍Python中整型
2019/02/11 Python
通过python连接Linux命令行代码实例
2020/02/18 Python
使用keras2.0 将Merge层改为函数式
2020/05/23 Python
python可以用哪些数据库
2020/06/22 Python
ASP.NET Core中的配置详解
2021/02/05 Python
Rossignol金鸡美国官网:始于1907年法国百年雪具品牌
2019/03/06 全球购物
英国电子专家:maplin
2019/09/04 全球购物
护士实习鉴定范文
2013/12/22 职场文书
求职简历中自我评价
2014/01/28 职场文书
公司薪酬管理制度
2014/01/31 职场文书
详细的本科生职业生涯规划范文
2014/09/16 职场文书
防汛工作情况汇报
2014/10/28 职场文书
2014年学生会干事工作总结
2014/11/07 职场文书
2016感恩父亲节主题广播稿
2015/12/18 职场文书
python3 实现mysql数据库连接池的示例代码
2021/04/17 Python
MySQL下使用Inplace和Online方式创建索引的教程
2021/05/26 MySQL
教你用python实现12306余票查询
2021/06/30 Python
Nginx隐藏式跳转(浏览器URL跳转后保持不变)
2022/04/07 Servers