Requête synchrone dans Node.js


99

Si je dois appeler 3 API http dans un ordre séquentiel, quelle serait une meilleure alternative au code suivant:

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


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

à part nettoyer ça, je ne pense pas que vous puissiez faire mieux que ça.
hvgotcodes

2
Pourquoi ont-ils besoin d'être en ordre?
Raynos

11
@Raynos Vous aurez peut-être besoin de certaines données de api_1 avant de savoir quoi envoyer à api_2
andyortlieb

9
Il convient de mentionner que Futures est assez obsolète, pensez à utiliser une bibliothèque plus récente comme Bluebird ou Q.
Benjamin Gruenbaum

1
Le titre et la question se contredisent. Vous ne décrivez pas une demande synchrone dans votre question, mais une séquence de demandes, qui se produiraient normalement chacune de manière asynchrone. Grande différence - un appel synchrone se bloque et une séquence d'actions asynchrones ne bloque pas (bloque l'interface utilisateur, empêche le serveur de traiter d'autres demandes). Il y a une réponse ci-dessous mentionnant la sync-requestbibliothèque, ce qui est une bonne réponse au titre de cette question, mais pas une réponse à ce que le code de la question implique. La réponse ci-dessous à propos des promesses est une meilleure réponse pour cela. Que vouliez-vous dire?
Jake

Réponses:


69

Utilisation de différés comme 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 vous avez besoin de passer la portée, faites quelque chose comme ça

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

Veuillez essayer IcedCoffeScript qui fournit wait et defer pour nodejs.
Thanigainathan

Est-ce non bloquant? Je veux dire qu'il bloque pour la fonction suivante en ligne, mais cela ne bloquera pas l'exécution d'autres fonctions asynchrones, n'est-ce pas?
Oktav

1
Oui, les méthodes différées sont non bloquantes / asynchrones.
dvlsg

4
l'API ES6 Promise devrait effectivement remplacer cela, même selon l'auteur de "Futures"
Alexander Mills

Les contrats à terme sont très anciens et obsolètes. Voir plutôt q.
Jim Aho

53

J'aime aussi la solution de Raynos, mais je préfère une bibliothèque de contrôle de flux différente.

https://github.com/caolan/async

Selon que vous avez besoin des résultats dans chaque fonction suivante, j'utiliserais des séries, des parallèles ou des cascade.

Séries lorsqu'elles doivent être exécutées en série, mais vous n'avez pas nécessairement besoin des résultats à chaque appel de fonction suivant.

Parallèles s'ils peuvent être exécutés en parallèle, vous n'avez pas besoin des résultats de chacun pendant chaque fonction parallèle, et vous avez besoin d'un rappel lorsque tout est terminé.

Cascade si vous souhaitez transformer les résultats de chaque fonction et passer à la suivante

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 est en fait un domaine conçu pour ce genre de choses. Sensationnel.
meawoppl

Le code async.series ne fonctionne pas, du moins depuis async v0.2.10. series () ne prend que deux arguments et exécutera les éléments du premier argument comme des fonctions, donc async renvoie une erreur en essayant d'exécuter les objets en tant que fonctions.
couvercle

1
Vous pouvez faire quelque chose de similaire à ce qui est prévu avec ce code en utilisant forEachAsync ( github.com/FuturesJS/forEachAsync ).
couvercle

Cela fait exactement ce que je voulais. Je vous remercie!
aProperFox

33

Vous pouvez le faire en utilisant ma bibliothèque 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
merde, j'ai voté pour en pensant que cela fonctionnerait et que cela ne fonctionne pas :(require(...).HttpClient is not a constructor
moeiscool

30

demande de synchronisation

De loin, le plus simple que j'ai trouvé et utilisé est la demande de synchronisation et il prend en charge à la fois le nœud et le navigateur!

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

C'est tout, pas de configuration folle, pas d'installation de bibliothèques complexes, bien qu'il y ait une bibliothèque de secours. Fonctionne juste. J'ai essayé d'autres exemples ici et j'étais perplexe quand il y avait beaucoup de configuration supplémentaire à faire ou que les installations ne fonctionnaient pas!

Remarques:

L'exemple utilisé par sync-request ne joue pas bien lorsque vous l'utilisez res.getBody(), tout ce que fait get body est d'accepter un encodage et de convertir les données de réponse. Faites simplement à la res.body.toString(encoding)place.


J'ai trouvé que la demande de synchronisation est très lente. J'ai fini par utiliser un autre github.com/dhruvbird/http-sync qui est 10 fois plus rapide dans mon cas.
Filip Spiridonov

je n'ai pas eu de courses lentes pour cela. Cela engendre un processus enfant. Combien de processeurs votre système utilise-t-il et quelle version de nœud utilisez-vous? J'aimerais savoir si je dois changer ou non.
jemiloii

Je suis d'accord avec Filip, c'est lent.
Rambo 7

Même chose que j'ai demandé à flip mais je n'ai obtenu aucune réponse: combien de processeurs votre système utilise-t-il et quelle version de nœud utilisez-vous?
jemiloii

cela utilise une quantité importante de CPU, non recommandée pour une utilisation en production.
moeiscool

20

J'utiliserais une fonction récursive avec une liste d'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 );

modifier: demander la version

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

edit: demande / version asynchrone

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

C'est la méthode que j'ai employée car j'ai une liste variable de demandes à faire (600 articles et de plus en plus). Cela dit, il y a un problème avec votre code: l'événement 'data' sera émis plusieurs fois par requête si la sortie de l'API est supérieure à la taille du bloc. Vous voulez "tamponner" les données comme ceci: var body = ''; res.on ('data', function (data) {body + = data;}). on ('end', function () {callback (body); if (APIs.length) callAPIs (host, APIs);} );
Ankit Aggarwal

Actualisé. Je voulais juste montrer comment le problème pouvait être rendu plus simple / plus flexible grâce à la récursivité. Personnellement, j'utilise toujours le module de requête pour ce genre de chose car il vous permet de passer facilement les multiples rappels.
generalhenry

@generalhenry, comment puis-je faire cela si je voulais utiliser le module de demande? Pouvez-vous proposer un extrait de code qui répond à la demande d'utilisation ci-dessus?
Scotty

J'ai ajouté une version de demande et une version de demande / asynchrone.
generalhenry

5

Il semble que les solutions à ce problème soient sans fin, en voici une de plus :)

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


Bien que la bibliothèque que vous avez liée offre une solution au problème de l'OP, dans votre exemple, fs.readFile est toujours synchronisé.
Eric

1
Non, vous pouvez fournir un rappel explicitement et l'utiliser comme version asynchrone si vous le souhaitez.
Alex Craft

1
l'exemple concernait les requêtes http, pas la communication du système de fichiers.
Seth

5

Une autre possibilité est de configurer un rappel qui suit les tâches terminées:

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

Ensuite, attribuez simplement un ID à chacun et vous pouvez définir vos exigences pour les tâches à effectuer avant de fermer la connexion.

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

D'accord, ce n'est pas joli. C'est juste une autre façon de passer des appels séquentiels. Il est regrettable que NodeJS ne fournisse pas les appels synchrones les plus élémentaires. Mais je comprends ce qu'est l'attrait de l'asynchronicité.


4

utilisez sequenty.

sudo npm installer sequenty

ou

https://github.com/AndyShin/sequenty

très 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]);

vous pouvez également utiliser une boucle comme celle-ci:

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'utilisation de la bibliothèque de requêtes peut aider à minimiser la cruauté:

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

Mais pour un maximum de génialité, vous devriez essayer une bibliothèque de flux de contrôle comme Step - cela vous permettra également de paralléliser les requêtes, en supposant que cela soit acceptable:

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

À partir de 2018 et en utilisant les modules ES6 et les promesses, nous pouvons écrire une fonction comme celle-ci:

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

puis dans un autre module

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

Le code doit être exécuté dans un contexte asynchrone (à l'aide d'un asyncmot-clé)


2

Il y a beaucoup de bibliothèques de flux de contrôle - j'aime conseq (... parce que je l'ai écrit.) En outre, on('data')peut se déclencher plusieurs fois, alors utilisez une bibliothèque de wrapper REST comme 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
  })

2

Cela a été bien répondu par Raynos. Pourtant, il y a eu des changements dans la bibliothèque de séquences depuis que la réponse a été publiée.

Pour faire fonctionner la séquence, suivez ce lien: https://github.com/FuturesJS/sequence/tree/9daf0000289954b85c0925119821752fbfb3521e .

Voici comment vous pouvez le faire fonctionner après npm install sequence:

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

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

1

Voici ma version de @ andy-shin séquentiellement avec des arguments dans le tableau au lieu de l'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 ans plus tard...

Voici une solution originale avec le framework Danf (vous n'avez besoin d'aucun code pour ce genre de choses, seulement quelques config):

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

Utilisez la même ordervaleur pour les opérations que vous souhaitez exécuter en parallèle.

Si vous souhaitez être encore plus court, vous pouvez utiliser un processus de collecte:

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

Jetez un œil à l' aperçu du framework pour plus d'informations.


1

J'ai atterri ici parce que j'avais besoin de limiter le taux de http.request (~ 10k requêtes d'agrégation à la recherche élastique pour créer un rapport analytique). Ce qui suit vient d'étouffer ma machine.

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

Mes URL sont très simples, donc cela ne s'applique peut-être pas de manière triviale à la question originale, mais je pense que c'est à la fois potentiellement applicable et vaut la peine d'être écrit ici pour les lecteurs qui débarquent ici avec des problèmes similaires aux miens et qui veulent une solution JavaScript triviale sans bibliothèque.

Mon travail ne dépendait pas de l'ordre et ma première approche pour éviter cela était de l'envelopper dans un script shell pour le fragmenter (car je suis nouveau dans JavaScript). C'était fonctionnel mais pas satisfaisant. Ma résolution JavaScript à la fin était de faire ce qui suit:

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

Cela ressemble à une récursivité mutuelle entre collect et get_top . Je ne suis pas sûr que ce soit en vigueur car le système est asynchrone et la fonction collect se termine par un rappel caché pour l'événement à on. ('End' .

Je pense que c'est assez général pour s'appliquer à la question initiale. Si, comme mon scénario, la séquence / l'ensemble est connu, toutes les URL / clés peuvent être poussées sur la pile en une seule étape. S'ils sont calculés au fur et à mesure, la fonction on ('end' peut pousser l'URL suivante sur la pile juste avant get_top () . Si quoi que ce soit, le résultat a moins d'imbrication et peut être plus facile à refactoriser lorsque l'API que vous appelez changements.

Je me rends compte que c'est effectivement équivalent à la version récursive simple de @ generalhenry ci-dessus (donc j'ai voté pour ça!)


0

Super demande

Il s'agit d'un autre module synchrone basé sur la demande et utilisant les promesses. Super simple à utiliser, fonctionne bien avec les tests de 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

Ce code peut être utilisé pour exécuter un tableau de promesses de manière synchrone et séquentielle, après quoi vous pouvez exécuter votre code final dans l' .then()appel.

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

En fait, j'ai eu exactement ce que vous (et moi) vouliez, sans l'utilisation de await, Promises ou inclusions de toute bibliothèque (externe) (sauf la nôtre).

Voici comment procéder:

Nous allons créer un module C ++ pour aller avec node.js, et cette fonction de module C ++ effectuera la requête HTTP et retournera les données sous forme de chaîne, et vous pouvez l'utiliser directement en faisant:

var myData = newModule.get(url);

ÊTES-VOUS PRÊT à commencer?

Étape 1: créez un nouveau dossier ailleurs sur votre ordinateur, nous n'utilisons ce dossier que pour construire le fichier module.node (compilé à partir de C ++), vous pourrez le déplacer plus tard.

Dans le nouveau dossier (j'ai mis le mien dans mynewFolder / src pour organiser-ness):

npm init

puis

npm install node-gyp -g

créez maintenant 2 nouveaux fichiers: 1, appelé something.cpp et pour mettre ce code dedans (ou modifiez-le si vous le souhaitez):

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

Maintenant, créez un nouveau fichier dans le même répertoire appelé something.gypet mettez-y (quelque chose comme) ceci:

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

Maintenant, dans le fichier package.json, ajoutez: "gypfile": true,

Maintenant: dans la console, node-gyp rebuild

S'il passe par toute la commande et dit "ok" à la fin sans erreur, vous êtes (presque) prêt à partir, sinon, laissez un commentaire.

Mais si cela fonctionne, allez dans build / Release / cobypp.node (ou quel que soit son nom), copiez-le dans votre dossier principal node.js, puis dans node.js:

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

..

response.end(myData);//or whatever
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.