Richiesta sincrona in Node.js


99

Se devo chiamare 3 API http in ordine sequenziale, quale sarebbe un'alternativa migliore al seguente codice:

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) { 


          });
        });
        }
      });
    });
    }
  });
});
}

oltre a ripulirlo, non credo che tu possa fare di meglio.
hvgotcodes

2
Perché devono essere in ordine?
Raynos

11
@ Raynos Potresti aver bisogno di alcuni dati da api_1 prima di sapere cosa inviare a api_2
andyortlieb

9
Vale la pena ricordare che Futures è piuttosto deprecato, considera l'utilizzo di una libreria più recente come Bluebird o Q.
Benjamin Gruenbaum

1
Il titolo e la domanda si contraddicono. Non stai descrivendo una richiesta sincrona nella tua domanda ma una sequenza di richieste, che normalmente si verificano ciascuna in modo asincrono. Grande differenza: una chiamata sincrona si blocca e una sequenza di azioni asincrone non si blocca (blocca l'interfaccia utente, impedisce al server di gestire altre richieste). C'è una risposta di seguito che menziona la sync-requestlibreria, che è una buona risposta al titolo di questa domanda, ma non una risposta per ciò che implica il codice della domanda. La risposta di seguito sulle promesse è una risposta migliore per questo. Cosa intendevi?
Jake

Risposte:


69

Utilizzando differiti come 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 hai bisogno di passare l'ambito, fai qualcosa del genere

  .then(function(next, d) {
    http.get({}, function(res) {
      next(res, d);
    });
  })
  .then(function(next, res, d) { })
    ...
  })

Si prega di provare IcedCoffeScript a che fornisce await e defer per nodejs.
Thanigainathan

Non è questo blocco? Voglio dire che sta bloccando per la prossima funzione in linea, ma questo non bloccherà l'esecuzione di altre funzioni asincrone, vero?
Oktav

1
Sì, i metodi differiti sono non bloccanti / asincroni.
dvlsg

4
l'API ES6 Promise dovrebbe sostituire efficacemente questo, anche secondo l'autore di "Futures"
Alexander Mills

I futures sono molto vecchi e deprecati. Vedi q invece.
Jim Aho

53

Mi piace anche la soluzione di Raynos, ma preferisco una libreria di controllo del flusso diversa.

https://github.com/caolan/async

A seconda che tu abbia bisogno dei risultati in ogni funzione successiva, userei serie, parallela o cascata.

Serie quando devono essere eseguite in serie, ma non sono necessariamente necessari i risultati in ogni successiva chiamata di funzione.

Parallelo se possono essere eseguiti in parallelo, non è necessario i risultati di ciascuno durante ogni funzione parallela e occorre una richiamata quando tutti sono stati completati.

Cascata se vuoi trasformare i risultati in ciascuna funzione e passare alla successiva

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

9
var http = require ('http');
Elle Mundy

7
Hah. example.com è in realtà un dominio progettato per questo genere di cose. Wow.
meawoppl

Il codice async.series non funziona, almeno a partire da async v0.2.10. series () richiede solo fino a due argomenti ed eseguirà gli elementi del primo argomento come funzioni, quindi async genera un errore nel tentativo di eseguire gli oggetti come funzioni.
coperchio

1
Puoi fare qualcosa di simile a ciò che è inteso con questo codice usando forEachAsync ( github.com/FuturesJS/forEachAsync ).
coperchio

Questo fa esattamente quello che volevo. Grazie!
aProperFox

33

Puoi farlo usando la mia libreria Common Node :

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

3
merda, ho votato positivamente pensando che avrebbe funzionato e non funziona :(require(...).HttpClient is not a constructor
moeiscool

30

richiesta di sincronizzazione

Di gran lunga il più semplice che ho trovato e utilizzato è la richiesta di sincronizzazione e supporta sia il nodo che il browser!

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

Questo è tutto, nessuna configurazione folle, nessuna installazione di librerie complesse, sebbene abbia un fallback della libreria. Funziona e basta. Ho provato altri esempi qui e sono rimasto perplesso quando c'era molta configurazione extra da fare o le installazioni non funzionavano!

Appunti:

L'esempio che utilizza la richiesta di sincronizzazione non funziona bene quando lo usi res.getBody(), tutto ciò che fa get body è accettare una codifica e convertire i dati di risposta. res.body.toString(encoding)Invece fallo .


Ho scoperto che la richiesta di sincronizzazione è molto lenta .. Ho finito per usare un altro github.com/dhruvbird/http-sync che è 10 volte più veloce nel mio caso.
Filip Spiridonov

non ho avuto corse lente per questo. Questo genera un processo figlio. Quante CPU usa il tuo sistema e quale versione di node stai usando? Mi piacerebbe sapere se devo cambiare o meno.
jemiloii

Sono d'accordo con Filip, questo è lento.
Rambo7

La stessa cosa che ho chiesto a flip ma non ho avuto risposta: quante CPU usa il tuo sistema e quale versione di node stai usando?
jemiloii

questo utilizza una notevole quantità di CPU, non consigliato per l'uso in produzione.
moeiscool

20

Userei una funzione ricorsiva con un elenco di API

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

modifica: richiesta versione

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

modifica: richiesta / versione asincrona

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

Questo è il metodo che ho utilizzato in quanto ho un elenco variabile di richieste da effettuare (600 articoli e in crescita). Detto questo, c'è un problema con il tuo codice: l'evento 'data' verrà emesso più volte per richiesta se l'output dell'API è maggiore della dimensione del blocco. Vuoi "bufferizzare" i dati in questo modo: var body = ''; res.on ('data', function (data) {body + = data;}). on ('end', function () {callback (body); if (APIs.length) callAPIs (host, APIs);} );
Ankit Aggarwal

Aggiornato. Volevo solo mostrare come il problema potrebbe essere reso più semplice / più flessibile attraverso la ricorsione. Personalmente uso sempre il modulo di richiesta per questo genere di cose poiché ti consente di saltare facilmente i callback multipli.
generalhenry

@generalhenry, come farei se volessi utilizzare il modulo di richiesta? Potete offrire uno snippet di codice che soddisfi quanto sopra utilizzando la richiesta?
Scotty

Ho aggiunto una versione richiesta e una versione richiesta / asincrona.
generalhenry

5

Sembra che le soluzioni per questo problema non finiscano mai, eccone un'altra :)

// 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


Anche se la libreria che hai collegato OFFRE una soluzione al problema dell'OP, nel tuo esempio fs.readFile è sempre sincronizzato.
Eric,

1
No, puoi fornire il callback esplicitamente e usarlo come versione asincrona se lo desideri.
Alex Craft

1
l'esempio però era per le richieste http, non per la comunicazione del file system.
Seth

5

Un'altra possibilità è impostare una richiamata che tenga traccia delle attività completate:

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

Quindi assegna semplicemente un ID a ciascuno e puoi impostare i tuoi requisiti per quali attività devono essere completate prima di chiudere la connessione.

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

Va bene, non è carino. È solo un altro modo per effettuare chiamate sequenziali. È un peccato che NodeJS non fornisca le chiamate sincrone più elementari. Ma capisco qual è il fascino dell'asincronicità.


4

usa sequenty.

sudo npm install sequenty

o

https://github.com/AndyShin/sequenty

molto semplice.

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

inoltre puoi usare un ciclo come questo:

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!

3

L'utilizzo della libreria delle richieste può aiutare a minimizzare il cruft:

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

Ma per la massima suggestione dovresti provare alcune librerie di flusso di controllo come Step: ti consentirà anche di parallelizzare le richieste, supponendo che sia accettabile:

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

3

A partire dal 2018 e utilizzando i moduli ES6 e le promesse, possiamo scrivere una funzione come questa:

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 poi in un altro modulo

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

Il codice deve essere eseguito in un contesto asincrono (utilizzando la asyncparola chiave)


2

Ci sono molte librerie del flusso di controllo - mi piace conseq (... perché l'ho scritto.) Inoltre, on('data')può essere attivato più volte, quindi usa una libreria wrapper REST come 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
  })


1

Ecco la mia versione di @ andy-shin in sequenza con argomenti in array invece che in index:

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

1

... 4 anni dopo ...

Ecco una soluzione originale con il framework Danf (non serve alcun codice per questo genere di cose, solo un po 'di configurazione):

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

Utilizzare lo stesso ordervalore per le operazioni che si desidera eseguire in parallelo.

Se vuoi essere ancora più breve, puoi utilizzare un processo di raccolta:

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

Dai un'occhiata alla panoramica del framework per maggiori informazioni.


1

Sono atterrato qui perché avevo bisogno di limitare la frequenza http.request (~ 10.000 query di aggregazione per la ricerca elastica per creare un rapporto analitico). Quanto segue ha appena soffocato la mia macchina.

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

I miei URL sono molto semplici, quindi questo potrebbe non applicarsi banalmente alla domanda originale, ma penso che sia potenzialmente applicabile e valga la pena scrivere qui per i lettori che arrivano qui con problemi simili al mio e che desiderano una banale soluzione JavaScript senza libreria.

Il mio lavoro non dipendeva dall'ordine e il mio primo approccio per creare questo è stato di avvolgerlo in uno script di shell per dividerlo (perché sono nuovo in JavaScript). Era funzionale ma non soddisfacente. La mia risoluzione JavaScript alla fine era di fare quanto segue:

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

Sembra una ricorsione reciproca tra collect e get_top . Non sono sicuro che sia in vigore perché il sistema è asincrono e la funzione collect viene completata con una richiamata nascosta per l'evento su on. ('End' .

Penso che sia abbastanza generale da applicare alla domanda originale. Se, come il mio scenario, la sequenza / set è nota, tutti gli URL / chiavi possono essere inseriti nello stack in un unico passaggio. Se vengono calcolati mentre procedi , la funzione on ('end' può inserire l'URL successivo nello stack appena prima di get_top () . Se non altro, il risultato ha meno nidificazione e potrebbe essere più facile refactoring quando l'API che stai chiamando i cambiamenti.

Mi rendo conto che questo è effettivamente equivalente alla semplice versione ricorsiva di @ generalhenry sopra (quindi l'ho votata al contrario!)


0

Super richiesta

Questo è un altro modulo sincrono basato sulla richiesta e utilizza le promesse. Super semplice da usare, funziona bene con i test di moka.

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

0

Questo codice può essere utilizzato per eseguire una serie di promesse in modo sincrono e sequenziale, dopodiché è possibile eseguire il codice finale nella .then()chiamata.

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

0

In realtà ho ottenuto esattamente ciò che tu (e io) volevamo, senza l'uso di await, Promises o inclusioni di alcuna libreria (esterna) (tranne la nostra).

Ecco come farlo:

Creeremo un modulo C ++ per andare con node.js e quella funzione del modulo C ++ farà la richiesta HTTP e restituirà i dati come una stringa, e puoi usarlo direttamente facendo:

var myData = newModule.get(url);

SEI PRONTO per iniziare?

Passaggio 1: crea una nuova cartella da qualche altra parte sul tuo computer, stiamo usando questa cartella solo per costruire il file module.node (compilato da C ++), puoi spostarlo in seguito.

Nella nuova cartella (ho messo il mio in mynewFolder / src per organiz-ness):

npm init

poi

npm install node-gyp -g

ora crea 2 nuovi file: 1, chiamato something.cpp e per inserire questo codice (o modificarlo se vuoi):

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

Ora crea un nuovo file nella stessa directory chiamata something.gype mettici (qualcosa di simile) questo:

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

Ora nel file package.json, aggiungi: "gypfile": true,

Ora: nella console, node-gyp rebuild

Se esegue l'intero comando e dice "ok" alla fine senza errori, sei (quasi) a posto, altrimenti lascia un commento ..

Ma se funziona, vai a build / Release / cobypp.node (o qualunque cosa sia chiamata per te), copialo nella tua cartella principale node.js, quindi in node.js:

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

..

response.end(myData);//or whatever
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.