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的前后端分离的思考与实践(五)多终端适配
Sep 26 NodeJs
nodejs中实现阻塞实例
Mar 24 NodeJs
Nodejs中的this详解
Mar 26 NodeJs
NodeJS配置HTTPS服务实例分享
Feb 19 NodeJs
详解nodejs中的process进程
Mar 19 NodeJs
nodejs个人博客开发第七步 后台登陆
Apr 12 NodeJs
nodejs实现OAuth2.0授权服务认证
Dec 27 NodeJs
详解Nodejs内存治理
May 13 NodeJs
Nodejs核心模块之net和http的使用详解
Apr 02 NodeJs
Nodejs 识别图片类型的方法
Aug 15 NodeJs
NodeJS多种创建WebSocket监听的方式(三种)
Jun 04 NodeJs
用Nodejs实现在终端中炒股的实现
Oct 18 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限制页面只能在微信自带浏览器访问的代码
2014/01/15 PHP
PHP 面向对象程序设计(oop)学习笔记(三) - 单例模式和工厂模式
2014/06/12 PHP
11个PHPer必须要了解的编程规范
2014/09/22 PHP
php实现模拟登陆方正教务系统抓取课表
2015/05/19 PHP
PHP在innodb引擎下快速代建全文搜索功能简明教程【基于xunsearch】
2016/10/14 PHP
JS 统计时间
2021/03/09 Javascript
js关闭当前页面(窗口)的几种方式总结
2013/03/05 Javascript
通过BootStrap-select插件 js jQuery控制select属性变化
2017/01/03 Javascript
nodejs爬虫遇到的乱码问题汇总
2017/04/07 NodeJs
Vue单页式应用(Hash模式下)实现微信分享的实例
2017/07/21 Javascript
jQuery选取所有复选框被选中的值并用Ajax异步提交数据的实例
2017/08/04 jQuery
Vue快速实现通用表单验证功能
2019/12/05 Javascript
原生js+canvas实现贪吃蛇效果
2020/08/02 Javascript
使用纯前端JavaScript实现Excel导入导出方法过程详解
2020/08/07 Javascript
Javascript新手入门之字符串拼接与变量的应用
2020/12/03 Javascript
python入门之语句(if语句、while语句、for语句)
2015/01/19 Python
在Python中使用swapCase()方法转换大小写的教程
2015/05/20 Python
使用Python导出Excel图表以及导出为图片的方法
2015/11/07 Python
Python微信企业号开发之回调模式接收微信端客户端发送消息及被动返回消息示例
2017/08/21 Python
Python八大常见排序算法定义、实现及时间消耗效率分析
2018/04/27 Python
Python3实现转换Image图片格式
2018/06/21 Python
Python基本类型的连接组合和互相转换方式(13种)
2019/12/16 Python
Pytorch Tensor的统计属性实例讲解
2019/12/30 Python
自学python用什么系统好
2020/06/23 Python
python如何编写类似nmap的扫描工具
2020/11/06 Python
python读写数据读写csv文件(pandas用法)
2020/12/14 Python
python 实现客户端与服务端的通信
2020/12/23 Python
详解基于Facecognition+Opencv快速搭建人脸识别及跟踪应用
2021/01/21 Python
Python  Asyncio模块实现的生产消费者模型的方法
2021/03/01 Python
CSS3 清除浮动的方法示例
2018/06/01 HTML / CSS
HTML5组件Canvas实现图像灰度化(步骤+实例效果)
2013/04/22 HTML / CSS
园艺专业毕业生求职信
2014/09/02 职场文书
辞职信模板(中英文版)
2015/02/27 职场文书
管理失职检讨书范文
2015/05/05 职场文书
2015年敬老院工作总结
2015/05/18 职场文书
大学生党员暑假实践(活动总结)
2019/08/21 职场文书