Синхронный запрос в Node.js


99

Если мне нужно вызвать 3 http API в последовательном порядке, что было бы лучшей альтернативой следующему коду:

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


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

кроме очистки этого, я не думаю, что вы можете сделать лучше, чем это.
hvgotcodes

2
Зачем они должны быть в порядке?
Raynos

11
@Raynos Вам могут понадобиться данные из api_1, прежде чем вы узнаете, что отправить в api_2
andyortlieb

9
Стоит отметить, что Futures устарела, подумайте об использовании более новой библиотеки, такой как Bluebird или Q.
Бенджамин Грюнбаум,

1
Название и вопрос противоречат друг другу. В своем вопросе вы описываете не синхронный запрос, а последовательность запросов, каждый из которых обычно выполняется асинхронно. Большая разница - синхронный вызов блокирует, а последовательность асинхронных действий не блокирует (блокирует UI, блокирует сервер от обработки других запросов). Ниже приводится ответ с упоминанием sync-requestбиблиотеки, который является хорошим ответом на заголовок этого вопроса, но не является ответом на то, что подразумевает код вопроса. Ответ ниже о обещаниях - лучший ответ на это. Что вы имели в виду?
Джейк

Ответы:


69

Использование отложенных файлов вроде 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) {
    ...
  })

Если вам нужно передать область видимости, просто сделайте что-то вроде этого

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

Пожалуйста, попробуйте IcedCoffeScript, который предоставляет await и defer для nodejs.
Thanigainathan

Это не блокирует? Я имею в виду, что это блокирует следующую функцию в строке, но это не блокирует выполнение других асинхронных функций, не так ли?
Октав

1
Да, отложенные методы неблокирующие / асинхронные.
dvlsg

4
API ES6 Promise должен эффективно заменить это, даже по словам автора книги «Futures»
Александра Миллса

Futures очень старый и не рекомендуется. См. Вместо этого q.
Джим Ахо,

53

Мне тоже нравится решение Raynos, но я предпочитаю другую библиотеку управления потоком.

https://github.com/caolan/async

В зависимости от того, нужны ли вам результаты в каждой последующей функции, я бы использовал серию, параллель или водопад.

Серии, когда они должны выполняться последовательно, но вам не обязательно нужны результаты при каждом последующем вызове функции.

Параллельно, если они могут выполняться параллельно, вам не нужны результаты каждой из них во время каждой параллельной функции, и вам нужен обратный вызов, когда все они будут завершены.

Waterfall, если вы хотите преобразовать результаты в каждой функции и перейти к следующей

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
Ха. example.com на самом деле является доменом, предназначенным для такого рода вещей. Вот это да.
meawoppl 02

Код async.series не работает, по крайней мере, с async v0.2.10. series () принимает до двух аргументов и будет выполнять элементы первого аргумента как функции, поэтому async выдает ошибку при попытке выполнить объекты как функции.
крышка

1
Вы можете сделать что-то похожее на то, что предназначено для этого кода, используя forEachAsync ( github.com/FuturesJS/forEachAsync ).
крышка

Это именно то, что я хотел. Спасибо!
aProperFox

33

Вы можете сделать это с помощью моей библиотеки 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
дерьмо, я поддержал, думая, что это сработает, а это не так :(require(...).HttpClient is not a constructor
moeiscool

30

запрос синхронизации

Безусловно, самый простой, который я нашел и использовал, - это запрос синхронизации, и он поддерживает как узел, так и браузер!

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

Вот и все, без сумасшедшей конфигурации, без сложной установки библиотеки, хотя у нее есть резервная версия библиотеки. Просто работает. Я пробовал здесь другие примеры и был озадачен, когда нужно было выполнить много дополнительных настроек или установки не работали!

Ноты:

Пример, который использует sync-request, не очень хорош, когда вы его используете res.getBody(), все, что делает get body, - это принимает кодировку и преобразует данные ответа. Просто сделай res.body.toString(encoding)вместо этого.


Я обнаружил, что запрос на синхронизацию выполняется очень медленно ... В итоге я использовал еще один github.com/dhruvbird/http-sync, который в моем случае в 10 раз быстрее.
Филип Спиридонов

у меня не было медленных пробежек для этого. Это порождает дочерний процесс. Сколько процессоров использует ваша система и какую версию узла вы используете? Я хотел бы знать, нужно ли мне переключаться или нет.
jemiloii 08

Я согласен с Филипом, это медленно.
Rambo7

То же самое, что я спросил у flip, но не получил ответа: сколько процессоров использует ваша система и какую версию узла вы используете?
jemiloii

при этом используется серьезное количество ЦП, что не рекомендуется для производственного использования.
moeiscool

20

Я бы использовал рекурсивную функцию со списком 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 );

изменить: запросить версию

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

изменить: запрос / асинхронная версия

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

Это метод, который я использовал, поскольку у меня есть переменный список запросов (600 элементов и он постоянно растет). Тем не менее, существует проблема с вашим кодом: событие data будет генерироваться несколько раз на запрос, если вывод API превышает размер блока. Вы хотите «буферизовать» данные следующим образом: var body = ''; res.on ('data', function (data) {body + = data;}). on ('end', function () {callback (body); if (APIs.length) callAPIs (host, APIs);} );
Анкит Аггарвал

Обновлено. Я просто хотел показать, как проблему можно упростить / сделать более гибкой с помощью рекурсии. Лично я всегда использую модуль запроса для такого рода вещей, поскольку он позволяет с легкостью пропустить несколько обратных вызовов.
generalhenry

@generalhenry, как бы мне это сделать, если бы я хотел использовать модуль запроса? Можете ли вы предложить фрагмент кода, который выполняет вышеуказанное с помощью запроса?
Скотти

Я добавил версию запроса и версию запроса / async.
Generalhenry

5

Кажется, решения этой проблемы бесконечны, вот еще одно :)

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


Хотя связанная вами библиотека ДЕЙСТВИТЕЛЬНО предлагает решение проблемы OP, в вашем примере fs.readFile всегда синхронизируется.
Эрик

1
Нет, вы можете явно указать обратный вызов и использовать его как асинхронную версию, если хотите.
Alex Craft

1
пример был для HTTP-запросов, а не для взаимодействия с файловой системой.
Сет

5

Другая возможность - настроить обратный вызов, который отслеживает выполненные задачи:

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

Затем просто назначьте идентификатор каждому, и вы можете настроить свои требования, для которых задачи должны быть выполнены перед закрытием соединения.

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

Ладно, это некрасиво. Это еще один способ делать последовательные звонки. К сожалению, NodeJS не предоставляет самые простые синхронные вызовы. Но я понимаю, в чем приманка асинхронности.


4

используйте последовательность.

sudo npm установить sequenty

или

https://github.com/AndyShin/sequenty

очень просто.

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

также вы можете использовать такой цикл:

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

Использование библиотеки запросов может помочь свести к минимуму беспорядок:

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

Но для максимального удовольствия вы должны попробовать какую-нибудь библиотеку управления потоком, такую ​​как Step - она ​​также позволит вам распараллеливать запросы, если это приемлемо:

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

Начиная с 2018 года, используя модули и обещания ES6, мы можем написать такую ​​функцию:

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

а затем в другом модуле

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

Код должен выполняться в асинхронном контексте (с использованием asyncключевого слова)


2

Существует множество библиотек потока управления - мне нравится consq (... потому что я его написал). Кроме того, он on('data')может запускаться несколько раз, поэтому используйте библиотеку-оболочку REST, например 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

На это хорошо ответил Райнос. Однако с момента публикации ответа в библиотеке последовательностей произошли изменения.

Чтобы заставить последовательность работать, перейдите по этой ссылке: https://github.com/FuturesJS/sequence/tree/9daf0000289954b85c0925119821752fbfb3521e .

Вот как вы можете заставить его работать после npm install sequence:

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

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

1

Вот моя версия @andy-shin последовательно с аргументами в массиве вместо индекса:

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

1

... 4 года спустя ...

Вот оригинальное решение с фреймворком Danf (вам не нужен код для такого рода вещей, только некоторая конфигурация):

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

Используйте одно и то же orderзначение для операций, которые вы хотите выполнять параллельно.

Если вы хотите быть еще короче, вы можете использовать процесс сбора:

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

Взгляните на обзор платформы для получения дополнительной информации.


1

Я попал сюда, потому что мне нужно было ограничить скорость http.request (~ 10 тыс. Запросов агрегирования для эластичного поиска для создания аналитического отчета). Следующее просто душило мою машину.

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

Мои URL-адреса очень просты, поэтому это может не тривиально относиться к исходному вопросу, но я думаю, что это потенциально применимо и стоит написать здесь для читателей, которые попадают сюда с проблемами, похожими на мои, и которым нужно тривиальное решение без библиотеки JavaScript.

Моя работа не зависела от заказа, и мой первый подход к этому заключался в том, чтобы обернуть его в сценарий оболочки, чтобы разбить его (потому что я новичок в JavaScript). Это было функционально, но неудовлетворительно. В конечном итоге мое решение JavaScript заключалось в следующем:

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

Похоже на взаимную рекурсию между collect и get_top . Я не уверен, что это действует, потому что система асинхронна, а функция collect завершается обратным вызовом, сохраненным для события в on. ('End' .

Я думаю, что это достаточно общий вопрос, чтобы применить его к исходному вопросу. Если, как в моем сценарии, последовательность / набор известны, все URL-адреса / ключи могут быть помещены в стек за один шаг. Если они рассчитываются по ходу выполнения, функция on ('end' может поместить следующий URL-адрес в стек непосредственно перед get_top () . Во всяком случае, результат имеет меньшую вложенность и может быть легче рефакторинг, когда API, который вы вызываете изменения.

Я понимаю, что это фактически эквивалентно простой рекурсивной версии @ generalhenry, приведенной выше (так что я поддержал это!)


0

Супер запрос

Это еще один синхронный модуль, который основан на запросе и использует обещания. Супер простой в использовании, хорошо работает с тестами мокко.

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

Этот код можно использовать для синхронного и последовательного выполнения массива обещаний, после чего вы можете выполнить свой последний код в .then()вызове.

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

Я получил именно то, что вы (и я) хотели, без использования await, обещаний или включения какой-либо (внешней) библиотеки (кроме нашей собственной).

Вот как это сделать:

Мы собираемся создать модуль C ++ для работы с node.js, и эта функция модуля C ++ будет выполнять HTTP-запрос и возвращать данные в виде строки, и вы можете использовать это напрямую, выполнив:

var myData = newModule.get(url);

ВЫ ГОТОВЫ приступить к работе?

Шаг 1: создайте новую папку где-нибудь еще на вашем компьютере, мы используем эту папку только для создания файла module.node (скомпилированного из C ++), вы можете переместить его позже.

В новой папке (я поместил свою в mynewFolder / src для упорядоченности):

npm init

затем

npm install node-gyp -g

Теперь создайте 2 новых файла: 1 с именем something.cpp и поместите в него этот код (или измените его, если хотите):

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

Теперь создайте новый файл в том же каталоге с именем something.gypи поместите в него (что-то вроде) это:

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

Теперь в файле package.json добавьте: "gypfile": true,

Теперь: в консоли, node-gyp rebuild

Если он проходит через всю команду и говорит "ОК" в конце без ошибок, вам (почти) хорошо, если нет, оставьте комментарий.

Но если это сработает, перейдите в build / Release / cobypp.node (или как там он вам нужен), скопируйте его в свою основную папку node.js, а затем в node.js:

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

..

response.end(myData);//or whatever
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.