Solicitud síncrona en Node.js

99

Si necesito llamar a 3 API http en orden secuencial, ¿cuál sería una mejor alternativa al siguiente 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
fuente
Aparte de limpiar eso, no creo que puedas hacerlo mejor que eso.
hvgotcodes
2
¿Por qué necesitan estar en orden?
Raynos
11
@Raynos Es posible que necesite algunos datos de api_1 antes de saber qué enviar a api_2
andyortlieb
9
Vale la pena mencionar que Futures está bastante desaprobado, considere usar una biblioteca más nueva como Bluebird o Q.
Benjamin Gruenbaum
1
El título y la pregunta se contradicen. No está describiendo una solicitud sincrónica en su pregunta, sino una secuencia de solicitudes, que normalmente ocurrirían de forma asincrónica. Gran diferencia: una llamada sincrónica bloquea y una secuencia de acciones asincrónicas no bloquea (bloquea la interfaz de usuario, bloquea el servidor para que no maneje otras solicitudes). Hay una respuesta a continuación que menciona la sync-requestbiblioteca, que es una buena respuesta al título de esta pregunta, pero no una respuesta a lo que implica el código de la pregunta. La respuesta a continuación sobre Promesas es una mejor respuesta para eso. ¿A qué te refieres?
Jake

Respuestas:

69

Usando diferidos 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) {
    ...
  })

Si necesita pasar el alcance, haga algo como esto

  .then(function(next, d) {
    http.get({}, function(res) {
      next(res, d);
    });
  })
  .then(function(next, res, d) { })
    ...
  })
Raynos
fuente
Pruebe IcedCoffeScript, que proporciona espera y aplazamiento para nodejs.
Thanigainathan
¿Es esto sin bloqueo? Quiero decir que está bloqueando la siguiente función en línea, pero esto no bloqueará la ejecución de otras funciones asíncronas, ¿verdad?
Oktav
1
Sí, los métodos diferidos son no bloqueantes / asíncronos.
dvlsg
4
la API ES6 Promise debería reemplazar esto de manera efectiva, incluso según el autor de "Futures"
Alexander Mills
Futures es muy antiguo y obsoleto. Ver q en su lugar.
Jim Aho
53

También me gusta la solución de Raynos, pero prefiero una biblioteca de control de flujo diferente.

https://github.com/caolan/async

Dependiendo de si necesita los resultados en cada función posterior, usaría serie, paralelo o cascada.

Series cuando deben ejecutarse en serie, pero no necesariamente necesita los resultados en cada llamada de función posterior.

Paralelo, si se pueden ejecutar en paralelo, no necesita los resultados de cada uno durante cada función en paralelo, y necesita una devolución de llamada cuando todas se hayan completado.

Cascada si desea transformar los resultados en cada función y pasar a la siguiente

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
fuente
9
var http = require ('http');
Elle Mundy
7
Ja. example.com es en realidad un dominio diseñado para este tipo de cosas. Guau.
meawoppl
El código async.series no funciona, al menos a partir de async v0.2.10. series () solo toma hasta dos argumentos y ejecutará los elementos del primer argumento como funciones, por lo que async arroja un error al intentar ejecutar los objetos como funciones.
tapa
1
Puede hacer algo similar a lo que se pretende con este código usando forEachAsync ( github.com/FuturesJS/forEachAsync ).
tapa
Esto hace exactamente lo que quería. ¡Gracias!
aProperFox
33

Puede hacer esto usando mi biblioteca de nodo común :

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
fuente
3
mierda, voté a favor pensando que funcionaría y no :(require(...).HttpClient is not a constructor
moeiscool
30

solicitud de sincronización

Con mucho, el más fácil que he encontrado y utilizado es la solicitud de sincronización y es compatible con el nodo y el navegador.

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

Eso es todo, sin configuración loca, sin instalaciones de lib complejas, aunque tiene un respaldo de lib. Simplemente funciona. ¡He probado otros ejemplos aquí y me quedé perplejo cuando había mucha configuración adicional por hacer o las instalaciones no funcionaron!

Notas:

El ejemplo que usa la solicitud de sincronización no funciona bien cuando lo usa res.getBody(), todo lo que hace get body es aceptar una codificación y convertir los datos de respuesta. Solo hazlo en su res.body.toString(encoding)lugar.

jemiloii
fuente
Encontré que la solicitud de sincronización es muy lenta. Terminé usando otro github.com/dhruvbird/http-sync que es 10 veces más rápido en mi caso.
Filip Spiridonov
No he tenido carreras lentas. Esto genera un proceso hijo. ¿Cuántos cpus usa su sistema y qué versión de nodo está usando? Me encantaría saber para determinar si necesito cambiar o no.
jemiloii
Estoy de acuerdo con Filip, esto es lento.
Rambo7
Lo mismo que le pregunté a flip pero no obtuve respuesta: ¿Cuántos cpus usa su sistema y qué versión de nodo está usando?
jemiloii
esto usa una gran cantidad de CPU, no recomendado para uso en producción.
moeiscool
20

Usaría una función recursiva con una 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 versión

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: solicitud / versión asincrónica

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
fuente
Este es el método que empleé, ya que tengo una lista variable de solicitudes para realizar (600 elementos y en aumento). Dicho esto, hay un problema con su código: el evento 'datos' se emitirá varias veces por solicitud si la salida de la API es mayor que el tamaño del fragmento. Quiere "almacenar" los datos de esta manera: var body = ''; res.on ('data', function (data) {body + = data;}). on ('end', function () {callback (body); if (APIs.length) callAPIs (host, APIs);} );
Ankit Aggarwal
Actualizado. Solo quería mostrar cómo el problema podría simplificarse / flexibilizarse mediante la recursividad. Personalmente, siempre uso el módulo de solicitud para este tipo de cosas, ya que te permite omitir las múltiples devoluciones de llamada con facilidad.
generalhenry
@generalhenry, ¿cómo haría esto si quisiera usar el módulo de solicitud? ¿Puede ofrecer un fragmento de código que logre lo anterior mediante request?
Scotty
Agregué una versión de solicitud y una versión de solicitud / asincrónica.
generalhenry
5

Parece que las soluciones para este problema no tienen fin, aquí hay una más :)

// 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
fuente
Aunque la biblioteca que vinculó SÍ ofrece una solución al problema del OP, en su ejemplo, fs.readFile siempre está sincronizado.
Eric
1
No, puede proporcionar la devolución de llamada explícitamente y usarla como versión asincrónica si lo desea.
Alex Craft
1
el ejemplo fue para las solicitudes http, no para la comunicación del sistema de archivos.
Seth
5

Otra posibilidad es configurar una devolución de llamada que rastrea las tareas completadas:

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();
}

Luego, simplemente asigne una ID a cada uno y podrá configurar sus requisitos para qué tareas deben completarse antes de cerrar la conexión.

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

Está bien, no es bonito. Es solo otra forma de realizar llamadas secuenciales. Es lamentable que NodeJS no proporcione las llamadas sincrónicas más básicas. Pero entiendo cuál es el atractivo de la asincronía.

Nate
fuente
4

use sequenty.

sudo npm install sequenty

o

https://github.com/AndyShin/sequenty

muy simple.

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]);

también puedes usar un bucle 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
fuente
3

El uso de la biblioteca de solicitudes puede ayudar a minimizar el 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
        })
    })
})

Pero para una máxima genialidad, debería probar alguna biblioteca de flujo de control como Step; también le permitirá paralelizar las solicitudes, asumiendo que es aceptable:

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
fuente
3

A partir de 2018 y usando los módulos ES6 y Promesas, podemos escribir una función como esa:

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));
});

y luego en otro 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

El código debe ejecutarse en un contexto asincrónico (usando una asyncpalabra clave)

vdegenne
fuente
2

Hay muchas bibliotecas de flujo de control: me gusta conseq (... porque lo escribí). Además, on('data')puede dispararse varias veces, así que use una biblioteca de envoltura 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
fuente
2

Esto ha sido bien respondido por Raynos. Sin embargo, ha habido cambios en la biblioteca de secuencias desde que se publicó la respuesta.

Para que la secuencia funcione, siga este enlace: https://github.com/FuturesJS/sequence/tree/9daf0000289954b85c0925119821752fbfb3521e .

Así es como puede hacer que funcione después npm install sequence:

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

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

Aquí está mi versión de @ andy-shin secuencialmente con argumentos en matriz en lugar de índice:

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

...4 años después...

Aquí hay una solución original con el framework Danf (no necesitas ningún código para este tipo de cosas, solo algunas configuraciones):

// 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'
            }
        ]
    }
};

Utilice el mismo ordervalor para las operaciones que desee ejecutar en paralelo.

Si desea ser aún más corto, puede usar un proceso de recolección:

// 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'
            }
        ]
    }
};

Eche un vistazo a la descripción general del marco para obtener más información.

Gnucki
fuente
1

Aterricé aquí porque necesitaba limitar la tasa de http.request (~ 10k consultas de agregación a la búsqueda elástica para crear un informe analítico). Lo siguiente acaba de ahogar mi máquina.

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

Mis URL son muy simples, por lo que es posible que esto no se aplique trivialmente a la pregunta original, pero creo que es potencialmente aplicable y vale la pena escribirlo aquí para los lectores que aterrizan aquí con problemas similares al mío y que desean una solución trivial sin biblioteca de JavaScript.

Mi trabajo no dependía del pedido y mi primer enfoque para hacer esto fue envolverlo en un script de shell para dividirlo (porque soy nuevo en JavaScript). Eso fue funcional pero no satisfactorio. Mi resolución de JavaScript al final fue hacer lo siguiente:

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 una recursividad mutua entre collect y get_top . No estoy seguro de que esté en efecto porque el sistema es asincrónico y la función recopilar se completa con una devolución de llamada guardada para el evento en el. ('End' .

Creo que es lo suficientemente general como para aplicarse a la pregunta original. Si, como en mi escenario, se conoce la secuencia / conjunto, todas las URL / claves se pueden insertar en la pila en un solo paso. Si se calculan sobre la marcha , la función on ('end' puede enviar la siguiente URL en la pila justo antes de get_top () . En todo caso, el resultado tiene menos anidamiento y podría ser más fácil de refactorizar cuando la API que está llamando cambios.

Me doy cuenta de que esto es efectivamente equivalente a la versión recursiva simple de @ generalhenry anterior (¡así que voté a favor de eso!)

irwinj
fuente
0

Super solicitud

Este es otro módulo síncrono que se basa en una solicitud y utiliza promesas. Súper simple de usar, funciona bien con pruebas de moca.

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
fuente
0

Este código se puede utilizar para ejecutar una serie de promesas de forma sincrónica y secuencial, después de lo cual puede ejecutar su código final en la .then()llamada.

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)
);
galatianos
fuente
0

De hecho, obtuve exactamente lo que tú (y yo) queríamos, sin el uso de await, Promises o inclusiones de ninguna biblioteca (externa) (excepto la nuestra).

He aquí cómo hacerlo:

Vamos a crear un módulo C ++ que vaya con node.js, y esa función del módulo C ++ hará la solicitud HTTP y devolverá los datos como una cadena, y puede usar eso directamente haciendo:

var myData = newModule.get(url);

¿ESTÁS LISTO para empezar?

Paso 1: cree una nueva carpeta en otro lugar de su computadora, solo estamos usando esta carpeta para construir el archivo module.node (compilado desde C ++), puede moverlo más tarde.

En la nueva carpeta (puse la mía en mynewFolder / src para organizar-ness):

npm init

luego

npm install node-gyp -g

ahora cree 2 archivos nuevos: 1, llamado something.cpp y para poner este código en él (o modificarlo si lo desea):

#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);

Ahora cree un nuevo archivo en el mismo directorio llamado something.gypy coloque (algo como) esto en él:

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

Ahora en el archivo package.json, agregue: "gypfile": true,

Ahora: en la consola, node-gyp rebuild

Si pasa por todo el comando y dice "ok" al final sin errores, está (casi) listo, si no, deje un comentario.

Pero si funciona, vaya a build / Release / cobypp.node (o lo que sea que le llame), cópielo en su carpeta principal node.js, luego en node.js:

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

..

response.end(myData);//or whatever
bluejayke
fuente