Synchroniczne żądanie w Node.js.

99

Gdybym potrzebował wywołać 3 http API w kolejności sekwencyjnej, jaka byłaby lepsza alternatywa dla następującego kodu:

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
źródło
poza sprzątaniem tego, nie sądzę, że możesz zrobić coś lepszego.
hvgotcodes
2
Dlaczego muszą być w porządku?
Raynos
11
@Raynos Możesz potrzebować pewnych danych z api_1, zanim będziesz wiedzieć, co wysłać do api_2
andyortlieb
9
Warto wspomnieć, że Futures jest dość przestarzałe, rozważ użycie nowszej biblioteki, takiej jak Bluebird lub Q.
Benjamin Gruenbaum
1
Tytuł i pytanie są ze sobą sprzeczne. W swoim pytaniu nie opisujesz żądania synchronicznego, ale sekwencję żądań, które normalnie występują asynchronicznie. Duża różnica - synchroniczne wywołania bloków, a sekwencja asynchronicznych akcji nie blokuje (blokuje UI, blokuje serwerowi obsługę innych żądań). Poniżej znajduje się odpowiedź wspominająca o sync-requestbibliotece, która jest dobrą odpowiedzią na tytuł tego pytania, ale nie jest odpowiedzią na to, co sugeruje kod pytania. Poniższa odpowiedź dotycząca obietnic jest lepszą odpowiedzią na to pytanie. Co miałeś na myśli?
Jake

Odpowiedzi:

69

Korzystanie z odroczonych, takich jak 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) {
    ...
  })

Jeśli chcesz przekazać zakres, po prostu zrób coś takiego

  .then(function(next, d) {
    http.get({}, function(res) {
      next(res, d);
    });
  })
  .then(function(next, res, d) { })
    ...
  })
Raynos
źródło
Spróbuj IcedCoffeScript, który zapewnia oczekiwanie i odroczenie dla nodejs.
Thanigainathan,
Czy to nie blokuje? Chodzi mi o to, że blokuje następną funkcję w wierszu, ale to nie blokuje wykonywania innych funkcji asynchronicznych, prawda?
Oktav
1
Tak, metody odroczone nie są blokowane / asynchroniczne.
dvlsg
4
ES6 Promise API powinno to skutecznie zastąpić, nawet według autora "Futures"
Alexandra Millsa
Futures są bardzo stare i przestarzałe. Zamiast tego zobacz q.
Jim Aho
53

Podoba mi się również rozwiązanie Raynos, ale wolę inną bibliotekę sterowania przepływem.

https://github.com/caolan/async

W zależności od tego, czy potrzebujesz wyników w każdej kolejnej funkcji, użyłbym serii, równoległości lub wodospadu.

Serie, gdy muszą być wykonywane seryjnie, ale niekoniecznie potrzebujesz wyników w każdym kolejnym wywołaniu funkcji.

Równoległe, jeśli mogą być wykonywane równolegle, nie potrzebujesz wyników z każdej funkcji równoległej i potrzebujesz wywołania zwrotnego po zakończeniu wszystkich.

Wodospad, jeśli chcesz zmienić wyniki w każdej funkcji i przejść do następnej

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
źródło
9
var http = require ('http');
Elle Mundy
7
Hah. example.com jest w rzeczywistości domeną zaprojektowaną do tego typu rzeczy. Łał.
meawoppl
Kod async.series nie działa, przynajmniej od wersji async 0.2.10. series () przyjmuje maksymalnie dwa argumenty i wykona elementy pierwszego argumentu jako funkcje, więc async zgłasza błąd podczas próby wykonania obiektów jako funkcji.
pokrywa
1
Możesz zrobić coś podobnego do tego, co jest przeznaczone dla tego kodu, używając forEachAsync ( github.com/FuturesJS/forEachAsync ).
pokrywa
Robi dokładnie to, czego chciałem. Dziękuję Ci!
aProperFox
33

Możesz to zrobić za pomocą mojej biblioteki 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');
Oleg
źródło
3
cholera, zagłosowałem za myśleniem, że to zadziała i nie działa :(require(...).HttpClient is not a constructor
moeiscool
30

sync-request

Zdecydowanie najłatwiejszym, jaki znalazłem i użyłem, jest żądanie synchronizacji i obsługuje zarówno węzeł, jak i przeglądarkę!

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

To wszystko, bez szalonej konfiguracji, bez skomplikowanych instalacji lib, chociaż ma rezerwę lib. Po prostu działa. Wypróbowałem tutaj inne przykłady i byłem zaskoczony, gdy było dużo dodatkowej konfiguracji lub instalacje nie działały!

Uwagi:

Przykład, którego używa żądanie synchronizacji, nie działa dobrze, gdy używasz res.getBody(), wszystko, co robi get body, to zaakceptowanie kodowania i przekonwertowanie danych odpowiedzi. Po prostu zrób to res.body.toString(encoding).

jemiloii
źródło
Zauważyłem, że żądanie synchronizacji jest bardzo wolne. Skończyło się na tym, że korzystałem z innego github.com/dhruvbird/http-sync, który jest 10 razy szybszy w moim przypadku.
Filip Spiridonov
nie miałem do tego żadnych wolnych biegów. Powoduje to proces potomny. Ile procesorów używa twój system i jakiej wersji węzła używasz? Chciałbym wiedzieć, czy muszę się zmienić, czy nie.
jemiloii
Zgadzam się z Filipem, to jest powolne.
Rambo 7
To samo, o które poprosiłem flip, ale nie otrzymałem odpowiedzi: ile procesorów używa twój system i jakiej wersji węzła używasz?
jemiloii
wymaga to dużej ilości procesora, co nie jest zalecane do użytku produkcyjnego.
moeiscool
20

Użyłbym funkcji rekurencyjnej z listą 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 );

edycja: żądanie wersji

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

edycja: wersja żądania / asynchroniczna

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
źródło
To jest metoda, którą zastosowałem, ponieważ mam zmienną listę żądań do wysłania (600 pozycji i rośnie). To powiedziawszy, wystąpił problem z twoim kodem: zdarzenie „data” będzie emitowane wiele razy na żądanie, jeśli dane wyjściowe API są większe niż rozmiar fragmentu. Chcesz "buforować" dane w ten sposób: var body = ''; res.on ('data', function (data) {body + = data;}). on ('end', function () {callback (body); if (APIs.length) callAPIs (host, APIs);} );
Ankit Aggarwal
Zaktualizowano. Chciałem tylko pokazać, jak można uprościć / uelastycznić problem dzięki rekurencji. Osobiście zawsze używam modułu request do tego typu rzeczy, ponieważ pozwala on z łatwością pominąć wiele wywołań zwrotnych.
generalhenry
@generalhenry, jak mam to zrobić, gdybym chciał użyć modułu żądania? Czy możesz zaoferować fragment kodu, który spełnia powyższe wymagania za pomocą żądania?
Scotty
Dodałem wersję żądania i wersję żądania / async.
generalhenry,
5

Wygląda na to, że rozwiązania tego problemu nigdy się nie kończą, oto jeszcze jedno :)

// 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
źródło
Chociaż biblioteka, którą łączysz, oferuje rozwiązanie problemu OP, w twoim przykładzie fs.readFile jest zawsze zsynchronizowana.
Eric
1
Nie, możesz jawnie podać wywołanie zwrotne i użyć go jako wersji asynchronicznej, jeśli chcesz.
Alex Craft
1
przykład dotyczył jednak żądań http, a nie komunikacji systemu plików.
Seth
5

Inną możliwością jest skonfigurowanie wywołania zwrotnego, które śledzi ukończone zadania:

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

Następnie po prostu przypisz identyfikator do każdego z nich i możesz ustawić wymagania, dla których zadania muszą zostać zakończone przed zamknięciem połączenia.

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

Okej, to nie jest ładne. To tylko inny sposób wykonywania połączeń sekwencyjnych. Szkoda, że ​​NodeJS nie zapewnia najbardziej podstawowych wywołań synchronicznych. Ale rozumiem, czym kusi asynchroniczność.

Nate
źródło
4

użyj sekwencji.

sudo npm zainstaluj sekwencję

lub

https://github.com/AndyShin/sequenty

bardzo prosty.

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

możesz również użyć pętli takiej jak ta:

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
źródło
3

Korzystanie z biblioteki żądań może pomóc zminimalizować problem:

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

Ale dla maksymalnej niesamowitości powinieneś wypróbować jakąś bibliotekę kontroli przepływu, taką jak Step - pozwoli ci to również na zrównoleglenie żądań, zakładając, że jest to akceptowalne:

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
źródło
3

Od 2018 roku i używając modułów ES6 i Promises, możemy napisać taką funkcję:

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

a potem w innym 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

Kod musi być wykonywany w kontekście asynchronicznym (przy użyciu asyncsłowa kluczowego)

vdegenne
źródło
2

Istnieje wiele bibliotek kontroli przepływu - lubię conseq (... ponieważ to napisałem). Ponadto on('data')może uruchamiać się kilka razy, więc użyj biblioteki opakowującej REST, takiej jak 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
źródło
2

Na to dobrze odpowiedział Raynos. Jednak od czasu opublikowania odpowiedzi zaszły zmiany w bibliotece sekwencji.

Aby sekwencja działała, kliknij ten link: https://github.com/FuturesJS/sequence/tree/9daf0000289954b85c0925119821752fbfb3521e .

Oto, jak możesz to uruchomić po npm install sequence:

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

seq.then(function call 1).then(function call 2);
adityah
źródło
1

Oto moja wersja @ andy-shin sekwencyjnie z argumentami w tablicy zamiast indeksu:

function run(funcs, args) {
    var i = 0;
    var recursive = function() {
        funcs[i](function() {
            i++;
            if (i < funcs.length)
                recursive();
        }, args[i]);
    };
    recursive();
}
wieczorek1990
źródło
1

...4 lata później...

Oto oryginalne rozwiązanie z frameworkiem Danf (nie potrzebujesz żadnego kodu do tego typu rzeczy, tylko trochę konfiguracji):

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

Użyj tej samej orderwartości dla operacji, które chcesz wykonywać równolegle.

Jeśli chcesz być jeszcze krótszy, możesz skorzystać z procesu windykacji:

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

Zapoznaj się z omówieniem struktury, aby uzyskać więcej informacji.

Gnucki
źródło
1

Wylądowałem tutaj, ponieważ musiałem ograniczyć http.request (~ 10k zapytań agregujących do elastycznego wyszukiwania w celu zbudowania raportu analitycznego). Następujące po prostu zakrztusiły moją maszynę.

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

Moje adresy URL są bardzo proste, więc może to nie mieć zastosowania do pierwotnego pytania, ale myślę, że jest to zarówno potencjalnie możliwe, jak i warte napisania tutaj dla czytelników, którzy trafiają tutaj z problemami podobnymi do moich i chcą trywialnego rozwiązania JavaScript bez biblioteki.

Moja praca nie była zależna od zamówienia, a moim pierwszym podejściem do tego było zawinięcie go w skrypt powłoki, aby go podzielić (ponieważ jestem nowy w JavaScript). To było funkcjonalne, ale niezadowalające. Ostatecznie moje rozwiązanie JavaScript polegało na wykonaniu następujących czynności:

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

Wygląda na to, że wzajemna rekurencja między zbieraniem i get_top . Nie jestem pewien, czy to działa, ponieważ system jest asynchroniczny, a funkcja collect kończy się wywołaniem zwrotnym zapisanym dla zdarzenia o godzinie on. ('End' .

Myślę, że wystarczy odnosić się do pierwotnego pytania. Jeśli, tak jak w moim scenariuszu, sekwencja / zestaw jest znana, wszystkie adresy URL / klucze można umieścić na stosie w jednym kroku. Jeśli są obliczane na bieżąco , funkcja on ('end' może umieścić następny adres URL na stosie tuż przed get_top () . Jeśli już, wynik ma mniej zagnieżdżenia i może być łatwiejszy do refaktoryzacji podczas wywoływania interfejsu API zmiany.

Zdaję sobie sprawę, że jest to równoważne z prostą rekurencyjną wersją @ generalhenry powyżej (więc zagłosowałem za tym!)

irwinj
źródło
0

Super żądanie

To kolejny moduł synchroniczny oparty na żądaniach i korzystający z obietnic. Super prosty w użyciu, działa dobrze z testami mokki.

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
źródło
0

Ten kod może być użyty do wykonania tablicy obietnic synchronicznie i sekwencyjnie, po czym można wykonać swój końcowy kod w .then()wywołaniu.

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)
);
galatów
źródło
0

Właściwie dostałem dokładnie to, czego ty (i ja) chciałeś, bez użycia czekania, obietnic lub włączeń jakiejkolwiek (zewnętrznej) biblioteki (z wyjątkiem naszej własnej).

Oto jak to zrobić:

Zamierzamy stworzyć moduł C ++, który będzie współpracował z node.js, a ta funkcja modułu C ++ wykona żądanie HTTP i zwróci dane jako ciąg znaków, a możesz użyć tego bezpośrednio, wykonując:

var myData = newModule.get(url);

CZY JESTEŚ GOTOWY, aby zacząć?

Krok 1: utwórz nowy folder w innym miejscu na swoim komputerze, używamy tego folderu tylko do budowania pliku module.node (skompilowanego z C ++), możesz go przenieść później.

W nowym folderze (umieściłem mój w mynewFolder / src w celu uporządkowania):

npm init

następnie

npm install node-gyp -g

teraz stwórz 2 nowe pliki: 1, nazwane coś.cpp i umieść w nim ten kod (lub zmodyfikuj, jeśli chcesz):

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

Teraz utwórz nowy plik w tym samym katalogu o nazwie something.gypi umieść w nim (coś podobnego):

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

Teraz w pliku package.json dodaj: "gypfile": true,

Teraz: w konsoli node-gyp rebuild

Jeśli przejdzie przez całą komendę i na końcu powie "ok" bez błędów, jesteś (prawie) gotowy, jeśli nie, zostaw komentarz.

Ale jeśli to zadziała, przejdź do build / Release / cobypp.node (lub cokolwiek, o co ci chodzi), skopiuj go do głównego folderu node.js, a następnie do node.js:

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

..

response.end(myData);//or whatever
bluejayke
źródło