Node.js“请求”库是否支持异步可迭代的响应流?

时间:2019-04-09 02:29:01

标签: node.js async-await stream async-iterator

我对Node.js库有些陌生,我正在尝试弄清楚如何在HTTP响应流上使用异步迭代。我的总体目标是读取大量的响应流,并在块到达时对其进行处理,当前是通过生成器函数进行的。我无法将整个响应存储在内存中进行处理。

我正在使用request库来执行HTTP请求,如下所示。

const request = require("request");

// contrived chunk-by-chunk stream processing 
async function* getChunks(stream) {
  for await (const chunk of stream) {
    yield chunk[0];
  }
}

async function doWork() {
  var response = request.get("https://pastebin.com/raw/x4Nn0Tby");
  for await (c of getChunks(response)) {
    console.log(c);
  }
}

运行doWork()时,出现错误,指出stream的{​​{1}}变量不是异步可迭代的。

TypeError:流不可异步迭代

这是令人惊讶的,因为我认为所有可读流通常都是异步可迭代的,并且当没有提供回调时,请求库将返回流。当我将getChunks()替换为request.get(...)到某个本地文件时,所有文件都按预期工作。

也许fs.createReadStream(...)库不支持此功能。如果是这样,我该怎么办才能通过async-iteration处理HTTP响应流?

使用Node.js 11.13和request 2.88.0。

4 个答案:

答案 0 :(得分:1)

我对requestrequest-promise-native库进行了更多实验,并且认为在当前实现中这是不可能的。结果流似乎根本不是异步可迭代的。此外,在处理流之前,需要await的适当实现才能使响应返回(如@JBone's answer所建议)。但是,如果调用await request.get(...),则会检索到响应的全部内容,这对于大型响应是不希望的。

const r = require("request");
const rpn = require("request-promise-native");

// contrived chunk-by-chunk stream processing 
async function* getChunks(stream) {
  for await (const chunk of stream) {
    yield chunk[0];
  }
}

async function doWork() {
  const url = "https://pastebin.com/raw/x4Nn0Tby";
  const response = r.get(url);         // returns a non-async-iterable object.
  const response2 = await rp.get(url); // returns the contents of url

  for await (c of getChunks(response)) {  // yields response not async-iterable error.
    console.log(c);
  }
}

我对此问题的解决方案是将requestrequest-promise-native的用法替换为axios库。这些库在功能上相似,但是axios允许您指定请求应解析为流;如预期的那样,该流是异步可迭代的。

const axios = require("axios");

async function doWork() {
  var response = await axios.request({
    method: "GET",
    url: "https://pastebin.com/raw/x4Nn0Tby",
    responseType: "stream",
  });

  for await (c of getChunks(response)) {  // async-iteration over response works as expected.
    console.log(c);
  }
}

答案 1 :(得分:1)

简单的回答:不,不是。您可能想在request周围使用基于承诺的包装器,例如request-promise,然后再与async / await一起使用。

详细信息:请注意,request已经是deprecated by its creator,因此将不再使用。这意味着,迟早,您很可能需要切换到其他解决方案,例如axiossuperagentneedle,等等。

当然,您需要评估这些模块并确定最适合您的需求,但是我个人的建议是从axios开始,因为我过去对此有很好的经验。 ,YMMV。

答案 2 :(得分:0)

似乎您将不得不使用其他替代方法,就像在request模块文档中提到的那样,您可以在此处找到 https://www.npmjs.com/package/request

request supports both streaming and callback interfaces natively. If you'd like 
request to return a Promise instead, you can use an alternative interface wrapper for 
request. These wrappers can be useful if you prefer to work with Promises, or if 
you'd like to use async/await in ES2017.

Several alternative interfaces are provided by the request team, including:

request-promise (uses Bluebird Promises)
request-promise-native (uses native Promises)
request-promise-any (uses any-promise Promises)`

我对以下问题的回答:

我认为您可以创建async await个自定义方法来实现它。

async function doMyWork() {
try {
 const response = await myOwnRequest(url); 
 } catch (e) {
   console.log ('the error', e);
 }  
}

function myOwnRequest(url) {
  return new Promise(function (resolve, reject) {
   const resp = request.get(url);
   if(resp) {
    resolve();
   } else {
     reject();
   }
});
}

答案 3 :(得分:0)

使用上面关于axios 0.19.0的答案中的示例代码,axios的流选项对我不起作用。椅子和键盘之间可能存在问题,但是无论如何...这是使用request的替代方法。

我最终将请求流调整为异步生成器(当然之间有一个缓冲区)。 这允许使用“流”类型的接口,在该接口中可以对数据的读写进行交错处理……这不能保证低内存消耗。尽可能快地请求管道(“推送”)到我们的Writable,并且没有办法让我们暂停它或将其翻转为“拉”类型的接口(据我所知)。因此,如果我们从缓冲区中读取数据的速度比写入数据的速度慢:缓冲区将变得非常大,内存使用率将会很高。

因此,降低内存使用量至关重要,并且您从http来源解析大文件...然后可能在“流式传输”时对缓冲区大小进行一些监视/报告,以查看消耗代码的速度是较快还是较慢而不是流,所以您知道缓冲区会变大还是变小。当然,如果您使用非常慢的http服务器进行测试...那么所有选择都将关闭。

可以通过设置固定的缓冲区大小并阻塞_write直到发生更多读取(在缓冲区中腾出空间)来解决此问题,即请求必须等待将更多数据写入管道。但是,请求可能会在内部进行缓冲...因此,无论如何,如果数据堆积在请求的末端,这将无助于内存消耗。将不得不检查。

示例代码:

const request = require('request'),
    Writable = require('stream').Writable,
    EventEmitter = require('events');

module.exports = function (url, MAX_BYTES=1024) {
    var response = new ResponseBuffer(MAX_BYTES);

    request
        .get(url)
        .on('error', function(err) { throw err; })
        .pipe(response)
        .on('error', function(err) { throw err; });

    return response.reader();
};

class ResponseBuffer extends Writable {
    constructor (MAX_BYTES=1024) {
        super();
        this.buffer = '';
        this.open = true;
        this.done = null;  // callback to call when done reading.
        this.MAX_BYTES = MAX_BYTES;
        this.events = new EventEmitter();
    }
    _write(chunk, enc, next) {
        this.buffer += chunk;
        this.events.emit('data');
        next();
    }
    _final(done) {
        this.open = false; // signal to reader to return after buffer empty.
        return done();
    }
    async * reader () {
        while (true) {
            if (this.buffer.length == 0) {
                // buffer empty and Writable !open. return.
                if (!this.open) { return; }
                else { // buffer empty.  wait for data.
                    await new Promise(resolve => this.events.once('data', resolve));
                }
            }
            let read_bytes = this.buffer.length < this.MAX_BYTES ? this.buffer.length : this.MAX_BYTES;
            yield this.buffer.slice(0, read_bytes);
            this.buffer = this.buffer.slice(read_bytes);
        }
    }
}

然后像这样使用它:


const httpModule = require('./path/to/above/module');
var httpGen = httpModule('https://www.google.com'),
    chunk;
for await (chunk of httpGen) {
    // do something with chunk.
}

另一种方法(如果您特别关注内存使用情况)是仅下载到磁盘(流式传输到文件编写器),然后从磁盘上增量读取(您可以使fs.createReadStream(...)异步访问)