Solicitação síncrona em Node.js

99

Se eu precisar chamar 3 http API em ordem sequencial, qual seria a melhor alternativa para o seguinte código:

http.get({ host: 'www.example.com', path: '/api_1.php' }, function(res) { 
  res.on('data', function(d) { 

    http.get({ host: 'www.example.com', path: '/api_2.php' }, function(res) { 
      res.on('data', function(d) { 

        http.get({ host: 'www.example.com', path: '/api_3.php' }, function(res) { 
          res.on('data', function(d) { 


          });
        });
        }
      });
    });
    }
  });
});
}
Howard
fonte
além de limpar isso, não acho que você possa fazer melhor do que isso.
hvgotcodes
2
Por que eles precisam estar em ordem?
Raynos de
11
@Raynos Você pode precisar de alguns dados de api_1 antes de saber o que enviar para api_2
andyortlieb
9
Vale a pena mencionar que Futures está bastante obsoleto, considere usar uma biblioteca mais recente como Bluebird ou Q.
Benjamin Gruenbaum
1
O título e a pergunta se contradizem. Você não está descrevendo uma solicitação síncrona em sua pergunta, mas uma sequência de solicitações, que normalmente ocorreria cada uma de forma assíncrona. Grande diferença - uma chamada síncrona bloqueia e uma sequência de ações assíncronas não bloqueia (bloqueia a IU, bloqueia o servidor de lidar com outras solicitações). Há uma resposta abaixo mencionando a sync-requestbiblioteca, o que é uma boa resposta ao título desta pergunta, mas não uma resposta para o que o código da pergunta implica. A resposta abaixo sobre Promessas é uma resposta melhor para isso. O que você quis dizer?
Jake

Respostas:

69

Usando adiados como Futures.

var sequence = Futures.sequence();

sequence
  .then(function(next) {
     http.get({}, next);
  })
  .then(function(next, res) {
     res.on("data", next);
  })
  .then(function(next, d) {
     http.get({}, next);
  })
  .then(function(next, res) {
    ...
  })

Se você precisa passar o escopo adiante, faça algo assim

  .then(function(next, d) {
    http.get({}, function(res) {
      next(res, d);
    });
  })
  .then(function(next, res, d) { })
    ...
  })
Raynos
fonte
Por favor, tente IcedCoffeScript para que fornece espera e adiamento para nodejs.
Thanigainathan
Isso não bloqueia? Quer dizer, ele está bloqueando a próxima função na linha, mas isso não vai bloquear a execução de outras funções assíncronas, vai?
Oktav,
1
Sim, os métodos adiados não são bloqueadores / assíncronos.
dvlsg
4
a API de promessa ES6 deve substituir isso efetivamente, mesmo de acordo com o autor de "Futures"
Alexander Mills
Futures é muito antigo e obsoleto. Em vez disso, veja q.
Jim Aho
53

Também gosto da solução da Raynos, mas prefiro uma biblioteca de controle de fluxo diferente.

https://github.com/caolan/async

Dependendo se você precisa dos resultados em cada função subsequente, eu usaria série, paralelo ou cascata.

Séries quando precisam ser executadas em série, mas você não precisa necessariamente dos resultados em cada chamada de função subsequente.

Paralelo, se eles podem ser executados em paralelo, você não precisa dos resultados de cada um durante cada função paralela e precisa de um retorno de chamada quando todos tiverem sido concluídos.

Waterfall se você quiser transformar os resultados em cada função e passar para a próxima

endpoints = 
 [{ host: 'www.example.com', path: '/api_1.php' },
  { host: 'www.example.com', path: '/api_2.php' },
  { host: 'www.example.com', path: '/api_3.php' }];

async.mapSeries(endpoints, http.get, function(results){
    // Array of results
});
Josh
fonte
9
var http = requer ('http');
Elle Mundy de
7
Hah. example.com é na verdade um domínio projetado para esse tipo de coisa. Uau.
meawoppl
O código async.series não funciona, pelo menos a partir de async v0.2.10. series () leva apenas dois argumentos e executará os elementos do primeiro argumento como funções, portanto, async gera um erro ao tentar executar os objetos como funções.
tampa
1
Você pode fazer algo semelhante ao pretendido com este código usando forEachAsync ( github.com/FuturesJS/forEachAsync ).
tampa
Isso faz exatamente o que eu queria. Obrigado!
aProperFox
33

Você pode fazer isso usando minha biblioteca de nó comum :

function get(url) {
  return new (require('httpclient').HttpClient)({
    method: 'GET',
      url: url
    }).finish().body.read().decodeToString();
}

var a = get('www.example.com/api_1.php'), 
    b = get('www.example.com/api_2.php'),
    c = get('www.example.com/api_3.php');
Oleg
fonte
3
merda, eu votei pensando que iria funcionar, mas não :(require(...).HttpClient is not a constructor
moeiscool
30

pedido de sincronização

De longe, o mais fácil que encontrei e usei é o sync-request e ele suporta o node e o navegador!

var request = require('sync-request');
var res = request('GET', 'http://google.com');
console.log(res.body.toString('utf-8'));

É isso, sem configuração maluca, sem instalações complexas de lib, embora tenha um fallback de lib. Apenas funciona. Tentei outros exemplos aqui e fiquei perplexo quando havia muita configuração extra para fazer ou as instalações não funcionavam!

Notas:

O exemplo que sync-request usa não funciona bem quando você usa res.getBody(), tudo o que get body faz é aceitar uma codificação e converter os dados de resposta. Basta fazer em res.body.toString(encoding)vez disso.

Jemiloii
fonte
Descobri que a solicitação de sincronização é muito lenta. Acabei usando outro github.com/dhruvbird/http-sync que é 10 vezes mais rápido no meu caso.
Filip Spiridonov
eu não tive nenhuma corrida lenta para isso. Isso gera um processo filho. Quantos cpus seu sistema usa e qual versão de nó você está usando? Adoraria saber para determinar se preciso mudar ou não.
jemiloii
Eu concordo com Filip, isso é lento.
Rambo7
A mesma coisa que perguntei ao flip, mas não obtive resposta: quantos cpus seu sistema usa e qual versão de nó você está usando?
jemiloii de
isso usa uma grande quantidade de CPU, não recomendado para uso de produção.
moeiscool
20

Eu usaria uma função recursiva com uma lista de apis

var APIs = [ '/api_1.php', '/api_2.php', '/api_3.php' ];
var host = 'www.example.com';

function callAPIs ( host, APIs ) {
  var API = APIs.shift();
  http.get({ host: host, path: API }, function(res) { 
    var body = '';
    res.on('data', function (d) {
      body += d; 
    });
    res.on('end', function () {
      if( APIs.length ) {
        callAPIs ( host, APIs );
      }
    });
  });
}

callAPIs( host, APIs );

editar: solicitar versão

var request = require('request');
var APIs = [ '/api_1.php', '/api_2.php', '/api_3.php' ];
var host = 'www.example.com';
var APIs = APIs.map(function (api) {
  return 'http://' + host + api;
});

function callAPIs ( host, APIs ) {
  var API = APIs.shift();
  request(API, function(err, res, body) { 
    if( APIs.length ) {
      callAPIs ( host, APIs );
    }
  });
}

callAPIs( host, APIs );

editar: solicitar / versão assíncrona

var request = require('request');
var async = require('async');
var APIs = [ '/api_1.php', '/api_2.php', '/api_3.php' ];
var host = 'www.example.com';
var APIs = APIs.map(function (api) {
  return 'http://' + host + api;
});

async.eachSeries(function (API, cb) {
  request(API, function (err, res, body) {
    cb(err);
  });
}, function (err) {
  //called when all done, or error occurs
});
generalhenry
fonte
Este é o método que utilizei, pois tenho uma lista variável de solicitações a fazer (600 itens e crescendo). Dito isso, há um problema com seu código: o evento 'data' será emitido várias vezes por solicitação se a saída da API for maior que o tamanho do bloco. Você deseja "armazenar" os dados assim: var body = ''; res.on ('data', function (data) {body + = data;}). on ('end', function () {callback (body); if (APIs.length) callAPIs (host, APIs);} );
Ankit Aggarwal
Atualizada. Eu só queria mostrar como o problema pode ser simplificado / mais flexível por meio da recursão. Pessoalmente, sempre uso o módulo de solicitação para esse tipo de coisa, pois permite pular os vários callbacks com facilidade.
generalhenry
@generalhenry, como faria isso se quisesse usar o módulo de solicitação? Você pode oferecer um trecho de código que atinja o acima usando solicitação?
Scotty,
Eu adicionei uma versão de solicitação e uma versão de solicitação / assíncrona.
generalhenry
5

Parece que as soluções para este problema não têm fim, aqui está mais uma :)

// do it once.
sync(fs, 'readFile')

// now use it anywhere in both sync or async ways.
var data = fs.readFile(__filename, 'utf8')

http://alexeypetrushin.github.com/synchronize

Alex Craft
fonte
Embora a biblioteca que você vinculou ofereça uma solução para o problema do OP, em seu exemplo, fs.readFile está sempre sincronizado.
Eric de
1
Não, você pode fornecer retorno de chamada explicitamente e usá-lo como versão assíncrona, se desejar.
Alex Craft
1
o exemplo era para solicitações http, não para comunicação do sistema de arquivos.
Seth
5

Outra possibilidade é configurar um retorno de chamada que rastreie as tarefas concluídas:

function onApiResults(requestId, response, results) {
    requestsCompleted |= requestId;

    switch(requestId) {
        case REQUEST_API1:
            ...
            [Call API2]
            break;
        case REQUEST_API2:
            ...
            [Call API3]
            break;
        case REQUEST_API3:
            ...
            break;
    }

    if(requestId == requestsNeeded)
        response.end();
}

Em seguida, basta atribuir um ID para cada um e você pode definir seus requisitos para quais tarefas devem ser concluídas antes de fechar a conexão.

const var REQUEST_API1 = 0x01;
const var REQUEST_API2 = 0x02;
const var REQUEST_API3 = 0x03;
const var requestsNeeded = REQUEST_API1 | REQUEST_API2 | REQUEST_API3;

Ok, não é bonito. É apenas outra maneira de fazer chamadas sequenciais. É uma pena que o NodeJS não forneça as chamadas síncronas mais básicas. Mas eu entendo o que é a atração para a assincronicidade.

Nate
fonte
4

use sequenty.

sudo npm install sequenty

ou

https://github.com/AndyShin/sequenty

muito simples.

var sequenty = require('sequenty'); 

function f1(cb) // cb: callback by sequenty
{
  console.log("I'm f1");
  cb(); // please call this after finshed
}

function f2(cb)
{
  console.log("I'm f2");
  cb();
}

sequenty.run([f1, f2]);

também você pode usar um loop como este:

var f = [];
var queries = [ "select .. blah blah", "update blah blah", ...];

for (var i = 0; i < queries.length; i++)
{
  f[i] = function(cb, funcIndex) // sequenty gives you cb and funcIndex
  {
    db.query(queries[funcIndex], function(err, info)
    {
       cb(); // must be called
    });
  }
}

sequenty.run(f); // fire!
Andy Shin
fonte
3

Usar a biblioteca de solicitações pode ajudar a minimizar o problema:

var request = require('request')

request({ uri: 'http://api.com/1' }, function(err, response, body){
    // use body
    request({ uri: 'http://api.com/2' }, function(err, response, body){
        // use body
        request({ uri: 'http://api.com/3' }, function(err, response, body){
            // use body
        })
    })
})

Mas para o máximo de grandiosidade, você deve tentar alguma biblioteca de fluxo de controle como Step - ela também permitirá que você paralelize as solicitações, presumindo que seja aceitável:

var request = require('request')
var Step    = require('step')

// request returns body as 3rd argument
// we have to move it so it works with Step :(
request.getBody = function(o, cb){
    request(o, function(err, resp, body){
        cb(err, body)
    })
}

Step(
    function getData(){
        request.getBody({ uri: 'http://api.com/?method=1' }, this.parallel())
        request.getBody({ uri: 'http://api.com/?method=2' }, this.parallel())
        request.getBody({ uri: 'http://api.com/?method=3' }, this.parallel())
    },
    function doStuff(err, r1, r2, r3){
        console.log(r1,r2,r3)
    }
)
Ricardo Tomasi
fonte
3

A partir de 2018 e usando módulos ES6 e Promises, podemos escrever uma função como esta:

import { get } from 'http';

export const fetch = (url) => new Promise((resolve, reject) => {
  get(url, (res) => {
    let data = '';
    res.on('end', () => resolve(data));
    res.on('data', (buf) => data += buf.toString());
  })
    .on('error', e => reject(e));
});

e então em outro módulo

let data;
data = await fetch('http://www.example.com/api_1.php');
// do something with data...
data = await fetch('http://www.example.com/api_2.php');
// do something with data
data = await fetch('http://www.example.com/api_3.php');
// do something with data

O código precisa ser executado em um contexto assíncrono (usando a asyncpalavra-chave)

vdegenne
fonte
2

Existem muitas bibliotecas de fluxo de controle - eu gosto do conseq (... porque eu o escrevi). Além disso, on('data')pode disparar várias vezes, então use uma biblioteca de wrapper REST como restler .

Seq()
  .seq(function () {
    rest.get('http://www.example.com/api_1.php').on('complete', this.next);
  })
  .seq(function (d1) {
    this.d1 = d1;
    rest.get('http://www.example.com/api_2.php').on('complete', this.next);
  })
  .seq(function (d2) {
    this.d2 = d2;
    rest.get('http://www.example.com/api_3.php').on('complete', this.next);
  })
  .seq(function (d3) {
    // use this.d1, this.d2, d3
  })
Nornagon
fonte
2

Isso foi bem respondido por Raynos. Ainda assim, houve mudanças na biblioteca de sequências desde que a resposta foi postada.

Para fazer a sequência funcionar, siga este link: https://github.com/FuturesJS/sequence/tree/9daf0000289954b85c0925119821752fbfb3521e .

É assim que você pode fazer isso funcionar depois de npm install sequence:

var seq = require('sequence').Sequence;
var sequence = seq.create();

seq.then(function call 1).then(function call 2);
adityah
fonte
1

Esta é minha versão de @ andy-shin sequencialmente com argumentos em array em vez de index:

function run(funcs, args) {
    var i = 0;
    var recursive = function() {
        funcs[i](function() {
            i++;
            if (i < funcs.length)
                recursive();
        }, args[i]);
    };
    recursive();
}
wieczorek1990
fonte
1

... 4 anos depois ...

Aqui está uma solução original com o framework Danf (você não precisa de nenhum código para este tipo de coisas, apenas algumas configurações):

// config/common/config/sequences.js

'use strict';

module.exports = {
    executeMySyncQueries: {
        operations: [
            {
                order: 0,
                service: 'danf:http.router',
                method: 'follow',
                arguments: [
                    'www.example.com/api_1.php',
                    'GET'
                ],
                scope: 'response1'
            },
            {
                order: 1,
                service: 'danf:http.router',
                method: 'follow',
                arguments: [
                    'www.example.com/api_2.php',
                    'GET'
                ],
                scope: 'response2'
            },
            {
                order: 2,
                service: 'danf:http.router',
                method: 'follow',
                arguments: [
                    'www.example.com/api_3.php',
                    'GET'
                ],
                scope: 'response3'
            }
        ]
    }
};

Use o mesmo ordervalor para as operações que deseja executar em paralelo.

Se você quiser ser ainda mais curto, pode usar um processo de coleta:

// config/common/config/sequences.js

'use strict';

module.exports = {
    executeMySyncQueries: {
        operations: [
            {
                service: 'danf:http.router',
                method: 'follow',
                // Process the operation on each item
                // of the following collection.
                collection: {
                    // Define the input collection.
                    input: [
                        'www.example.com/api_1.php',
                        'www.example.com/api_2.php',
                        'www.example.com/api_3.php'
                    ],
                    // Define the async method used.
                    // You can specify any collection method
                    // of the async lib.
                    // '--' is a shorcut for 'forEachOfSeries'
                    // which is an execution in series.
                    method: '--'
                },
                arguments: [
                    // Resolve reference '@@.@@' in the context
                    // of the input item.
                    '@@.@@',
                    'GET'
                ],
                // Set the responses in the property 'responses'
                // of the stream.
                scope: 'responses'
            }
        ]
    }
};

Dê uma olhada na visão geral da estrutura para obter mais informações.

Gnucki
fonte
1

Cheguei aqui porque precisava limitar a taxa de http.request (~ 10k consultas de agregação para pesquisa elástica para construir um relatório analítico). O seguinte apenas sufocou minha máquina.

for (item in set) {
    http.request(... + item + ...);
}

Minhas URLs são muito simples, então isso pode não se aplicar trivialmente à pergunta original, mas acho que é potencialmente aplicável e vale a pena escrever aqui para leitores que chegam aqui com problemas semelhantes aos meus e que querem uma solução JavaScript trivial sem biblioteca.

Meu trabalho não dependia da ordem e minha primeira abordagem para resolver isso foi envolvê-lo em um script de shell para fragmentá-lo (porque sou novo em JavaScript). Isso foi funcional, mas não satisfatório. Minha resolução de JavaScript no final era fazer o seguinte:

var stack=[];
stack.push('BOTTOM');

function get_top() {
  var top = stack.pop();
  if (top != 'BOTTOM')
    collect(top);
}

function collect(item) {
    http.request( ... + item + ...
    result.on('end', function() {
      ...
      get_top();
    });
    );
}

for (item in set) {
   stack.push(item);
}

get_top();

Parece uma recursão mútua entre collect e get_top . Não tenho certeza se está em vigor porque o sistema é assíncrono e a função collect é concluída com um retorno de chamada armazenado para o evento em. ('Fim' .

Acho que é geral o suficiente para se aplicar à pergunta original. Se, como no meu cenário, a sequência / conjunto for conhecido, todos os URLs / chaves podem ser colocados na pilha em uma etapa. Se eles são calculados conforme você avança, a função on ('end' pode enviar o próximo url na pilha antes de get_top () . Se houver, o resultado tem menos aninhamento e pode ser mais fácil de refatorar quando a API que você está chamando alterar.

Eu percebo que isso é efetivamente equivalente à versão recursiva simples do @generalhenry acima (então eu votei a favor!)

irwinj
fonte
0

Super Solicitação

Este é outro módulo síncrono baseado em solicitações e usa promessas. Super simples de usar, funciona bem com testes de mocha.

npm install super-request

request("http://domain.com")
    .post("/login")
    .form({username: "username", password: "password"})
    .expect(200)
    .expect({loggedIn: true})
    .end() //this request is done 
    //now start a new one in the same session 
    .get("/some/protected/route")
    .expect(200, {hello: "world"})
    .end(function(err){
        if(err){
            throw err;
        }
    });
Jemiloii
fonte
0

Este código pode ser usado para executar uma série de promessas de forma síncrona e sequencial, após a qual você pode executar seu código final na .then()chamada.

const allTasks = [() => promise1, () => promise2, () => promise3];

function executePromisesSync(tasks) {
  return tasks.reduce((task, nextTask) => task.then(nextTask), Promise.resolve());
}

executePromisesSync(allTasks).then(
  result => console.log(result),
  error => console.error(error)
);
galatas
fonte
0

Na verdade, consegui exatamente o que você (e eu) queríamos, sem o uso de await, Promises ou inclusões de qualquer biblioteca (externa) (exceto a nossa).

Veja como fazer:

Vamos fazer um módulo C ++ para ir com node.js, e essa função de módulo C ++ fará a solicitação HTTP e retornará os dados como uma string, e você pode usar isso diretamente fazendo:

var myData = newModule.get(url);

VOCÊ ESTÁ PRONTO para começar?

Passo 1: crie uma nova pasta em outro lugar do seu computador, estamos usando esta pasta apenas para construir o arquivo module.node (compilado do C ++), você pode movê-lo mais tarde.

Na nova pasta (coloquei a minha em mynewFolder / src para organização):

npm init

então

npm install node-gyp -g

agora faça 2 novos arquivos: 1, chamado something.cpp e para colocar este código nele (ou modifique-o se quiser):

#pragma comment(lib, "urlmon.lib")
#include <sstream>
#include <WTypes.h>  
#include <node.h>
#include <urlmon.h> 
#include <iostream>
using namespace std;
using namespace v8;

Local<Value> S(const char* inp, Isolate* is) {
    return String::NewFromUtf8(
        is,
        inp,
        NewStringType::kNormal
    ).ToLocalChecked();
}

Local<Value> N(double inp, Isolate* is) {
    return Number::New(
        is,
        inp
    );
}

const char* stdStr(Local<Value> str, Isolate* is) {
    String::Utf8Value val(is, str);
    return *val;
}

double num(Local<Value> inp) {
    return inp.As<Number>()->Value();
}

Local<Value> str(Local<Value> inp) {
    return inp.As<String>();
}

Local<Value> get(const char* url, Isolate* is) {
    IStream* stream;
    HRESULT res = URLOpenBlockingStream(0, url, &stream, 0, 0);

    char buffer[100];
    unsigned long bytesReadSoFar;
    stringstream ss;
    stream->Read(buffer, 100, &bytesReadSoFar);
    while(bytesReadSoFar > 0U) {
        ss.write(buffer, (long long) bytesReadSoFar);
        stream->Read(buffer, 100, &bytesReadSoFar);
    }
    stream->Release();
    const string tmp = ss.str();
    const char* cstr = tmp.c_str();
    return S(cstr, is);
}

void Hello(const FunctionCallbackInfo<Value>& arguments) {
    cout << "Yo there!!" << endl;

    Isolate* is = arguments.GetIsolate();
    Local<Context> ctx = is->GetCurrentContext();

    const char* url = stdStr(arguments[0], is);
    Local<Value> pg = get(url,is);

    Local<Object> obj = Object::New(is);
    obj->Set(ctx,
        S("result",is),
        pg
    );
    arguments.GetReturnValue().Set(
       obj
    );

}

void Init(Local<Object> exports) {
    NODE_SET_METHOD(exports, "get", Hello);
}

NODE_MODULE(cobypp, Init);

Agora faça um novo arquivo no mesmo diretório chamado something.gype coloque (algo como) isto nele:

{
   "targets": [
       {
           "target_name": "cobypp",
           "sources": [ "src/cobypp.cpp" ]
       }
   ]
}

Agora, no arquivo package.json, adicione: "gypfile": true,

Agora: no console, node-gyp rebuild

Se passar por todo o comando e disser "ok" no final sem erros, você está (quase) pronto para prosseguir, se não, deixe um comentário.

Mas se funcionar, vá para build / Release / cobypp.node (ou o que for chamado para você), copie-o para a pasta node.js principal e, em seguida, para node.js:

var myCPP = require("./cobypp")
var myData = myCPP.get("http://google.com").result;
console.log(myData);

..

response.end(myData);//or whatever
Bluejayke
fonte