Node.js Конвеєр того самого читабельного потоку в декілька цілей, доступних для запису


77

Мені потрібно виконати дві команди послідовно, які потребують зчитування даних з одного потоку. Після передачі потоку в інший буфер спорожняється, тому я не можу знову прочитати дані з цього потоку, тому це не працює:

var spawn = require('child_process').spawn;
var fs = require('fs');
var request = require('request');

var inputStream = request('http://placehold.it/640x360');
var identify = spawn('identify',['-']);

inputStream.pipe(identify.stdin);

var chunks = [];
identify.stdout.on('data',function(chunk) {
  chunks.push(chunk);
});

identify.stdout.on('end',function() {
  var size = getSize(Buffer.concat(chunks)); //width
  var convert = spawn('convert',['-','-scale',size * 0.5,'png:-']);
  inputStream.pipe(convert.stdin);
  convert.stdout.pipe(fs.createWriteStream('half.png'));
});

function getSize(buffer){
  return parseInt(buffer.toString().split(' ')[2].split('x')[0]);
}

Запит скаржиться на це

Error: You cannot pipe after data has been emitted from the response.

і зміна inputStream на fs.createWriteStreamте ж саме, звісно. Я не хочу писати у файл, але я якось повторно використовую потік, який створює запит (або будь-який інший з цього приводу).

Чи є спосіб повторного використання читабельного потоку, як тільки він закінчить трубопровід? Який найкращий спосіб зробити щось подібне до наведеного вище прикладу?


Здається, ви використовуєте imagemick. Ви можете передати значення, як 50%, у масштаб для масштабування. Ви також можете використовувати npmjs.org/package/gm
user568109

2
@ user568109 Так. Тут справа не в цьому. Це більш загальне питання ... це imagemagick, як це може бути будь-яка інша команда / потік
Maroshii

Відповіді:


83

Вам потрібно створити дублікат потоку, переклавши його на два потоки. Ви можете створити простий потік з потоком PassThrough, він просто передає вхід на вихід.

const spawn = require('child_process').spawn;
const PassThrough = require('stream').PassThrough;

const a = spawn('echo', ['hi user']);
const b = new PassThrough();
const c = new PassThrough();

a.stdout.pipe(b);
a.stdout.pipe(c);

let count = 0;
b.on('data', function (chunk) {
  count += chunk.length;
});
b.on('end', function () {
  console.log(count);
  c.pipe(process.stdout);
});

Вихід:

8
hi user

5
Застосовували цей прийом із гачками прикріплення поштового сервера Haraka для передачі вхідного потоку в кілька баз даних облікових записів пошти. Ця відповідь працює.

17
Зауважте, що цей прийом працює лише в тому випадку, коли команда, що породила, видає кількість байтів, які не заповнюють буфери зворотного тиску. ви можете спробувати зробити так, щоб він не вдався за допомогою = spawn ('head', ['-c', '200K', '/ dev / urandom']); Якщо c не виведено, в якийсь момент a.stdout призупинить виведення трубопроводу. b буде стікати і ніколи не закінчиться.
Jerome WAGNER

44
Я збентежений, ви кажете, що ви не можете обробляти один і той самий потік двічі, але ваше рішення полягає в тому, щоб .. обробляти той самий потік двічі (із перетворенням PassThrough). Це здається суперечливим. Це щось особливе у потоках stdout?
BT

7
Я перевірив це, і це, безумовно, працює. Я вважаю, що для вас неправильно говорити "ви не можете обробляти один і той же [потоковий] потік двічі", оскільки це те, що ви робите. Ваша перша заява про неможливість конвеєрувати потік після його закінчення є відповідною причиною.
BT

6
Не використовуйте цей метод, оскільки він створює проблеми, якщо потоки читаються з різною швидкістю. Спробуйте замість цього npmjs.com/package/readable-stream-clone добре працював у мене.
kiwicomb123

12

Перша відповідь працює лише в тому випадку, якщо потоки займають приблизно однакову кількість часу на обробку даних. Якщо це займає значно більше часу, швидше запитує нові дані, отже, перезаписує дані, які все ще використовуються повільнішими (у мене була ця проблема після спроби вирішити це за допомогою повторюваного потоку).

Наступний зразок дуже добре для мене спрацював. Він використовує бібліотеку на основі потоків Stream2, Streamz та Promises для синхронізації асинхронних потоків за допомогою зворотного виклику. На знайомому прикладі з першої відповіді:

spawn = require('child_process').spawn;
pass = require('stream').PassThrough;
streamz = require('streamz').PassThrough;
var Promise = require('bluebird');

a = spawn('echo', ['hi user']);
b = new pass;
c = new pass;   

a.stdout.pipe(streamz(combineStreamOperations)); 

function combineStreamOperations(data, next){
  Promise.join(b, c, function(b, c){ //perform n operations on the same data
  next(); //request more
}

count = 0;
b.on('data', function(chunk) { count += chunk.length; });
b.on('end', function() { console.log(count); c.pipe(process.stdout); });

Яка частина насправді перезаписує дані? Код, який перезаписує, повинен, природно, викликати помилку.
Роберт Сімер,

1

Для загальної проблеми наступний код чудово працює

var PassThrough = require('stream').PassThrough
a=PassThrough()
b1=PassThrough()
b2=PassThrough()
a.pipe(b1)
a.pipe(b2)
b1.on('data', function(data) {
  console.log('b1:', data.toString())
})
b2.on('data', function(data) {
  console.log('b2:', data.toString())
})
a.write('text')

1

А як щодо трубопроводів у два або більше потоків не одночасно?

Наприклад :

var PassThrough = require('stream').PassThrough;
var mybiraryStream = stream.start(); //never ending audio stream
var file1 = fs.createWriteStream('file1.wav',{encoding:'binary'})
var file2 = fs.createWriteStream('file2.wav',{encoding:'binary'})
var mypass = PassThrough
mybinaryStream.pipe(mypass)
mypass.pipe(file1)
setTimeout(function(){
   mypass.pipe(file2);
},2000)

Наведений вище код не видає помилок, але файл2 порожній


якимось чином це мені допомагає!
Sandip

5
Я думаю, ви виявили проблему, але це бентежить, оскільки це не відповідь.
Майкл

1

У мене є інше рішення для одночасного запису в два потоки, природно, час для написання буде додавати два рази, але я використовую його для відповіді на запит на завантаження, де я хочу зберегти копію завантаженого файлу на мій сервер (насправді я використовую резервну копію S3, тому кешую найбільш часто використовувані файли локально, щоб уникнути багаторазової передачі файлів)

/**
 * A utility class made to write to a file while answering a file download request
 */
class TwoOutputStreams {
  constructor(streamOne, streamTwo) {
    this.streamOne = streamOne
    this.streamTwo = streamTwo
  }

  setHeader(header, value) {
    if (this.streamOne.setHeader)
      this.streamOne.setHeader(header, value)
    if (this.streamTwo.setHeader)
      this.streamTwo.setHeader(header, value)
  }

  write(chunk) {
    this.streamOne.write(chunk)
    this.streamTwo.write(chunk)
  }

  end() {
    this.streamOne.end()
    this.streamTwo.end()
  }
}

Потім ви можете використовувати це як звичайний OutputStream

const twoStreamsOut = new TwoOutputStreams(fileOut, responseStream)

і передайте його своєму методу, ніби це відповідь або файлOutputStream


1

Якщо у вас є асинхронні дії на потоках PassThrough, відповіді, розміщені тут, не працюватимуть. Рішення, яке працює для асинхронних операцій, включає буферизацію вмісту потоку, а потім створення потоків із буферизованого результату.

  1. Для буферизації результату ви можете використовувати concat-stream

    const Promise = require('bluebird');
    const concat = require('concat-stream');
    const getBuffer = function(stream){
        return new Promise(function(resolve, reject){
            var gotBuffer = function(buffer){
                resolve(buffer);
            }
            var concatStream = concat(gotBuffer);
            stream.on('error', reject);
            stream.pipe(concatStream);
        });
    }
    
  2. Для створення потоків з буфера ви можете використовувати:

    const { Readable } = require('stream');
    const getBufferStream = function(buffer){
        const stream = new Readable();
        stream.push(buffer);
        stream.push(null);
        return Promise.resolve(stream);
    }
    

1

Ви можете використовувати цей невеликий пакет npm, який я створив:

readable-stream-clone

Завдяки цьому ви можете повторно використовувати читабельні потоки стільки разів, скільки вам потрібно

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.