Permintaan sinkron di Node.js


99

Jika saya perlu memanggil 3 http API secara berurutan, apa alternatif yang lebih baik untuk kode berikut:

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


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

selain membersihkannya, saya rasa Anda tidak bisa melakukan lebih baik dari itu.
hvgotcodes

2
Mengapa mereka harus teratur?
Raynos

11
@Raynos Anda mungkin memerlukan beberapa data dari api_1 sebelum Anda tahu apa yang harus dikirim ke api_2
andyortlieb

9
Perlu disebutkan bahwa Futures cukup usang, pertimbangkan untuk menggunakan perpustakaan yang lebih baru seperti Bluebird atau Q.
Benjamin Gruenbaum

1
Judul dan pertanyaan saling bertentangan. Anda tidak mendeskripsikan permintaan sinkron dalam pertanyaan Anda, melainkan urutan permintaan, yang biasanya masing-masing terjadi secara asinkron. Perbedaan besar - panggilan sinkron memblokir, dan urutan tindakan asinkron tidak memblokir (memblokir UI, memblokir server dari menangani permintaan lain). Ada jawaban di bawah ini yang menyebutkan sync-requestperpustakaan, yang merupakan jawaban yang bagus untuk judul pertanyaan ini, tetapi bukan jawaban untuk apa yang tersirat dalam kode pertanyaan. Jawaban di bawah tentang Promises adalah jawaban yang lebih baik untuk itu. Apa maksudmu
Jake

Jawaban:


69

Menggunakan deferreds like 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) {
    ...
  })

Jika Anda perlu meneruskan cakupan maka lakukan saja sesuatu seperti ini

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

Silakan coba IcedCoffeScript yang menyediakan menunggu dan menunda nodejs.
Thanigainathan

Apakah ini tidak memblokir? Maksud saya ini memblokir untuk fungsi berikutnya sejalan tetapi ini tidak akan memblokir eksekusi fungsi asinkron lainnya, bukan?
Oktav

1
Ya, metode yang ditangguhkan tidak memblokir / asinkron.
dvlsg

4
ES6 Promise API harus secara efektif menggantikan ini, bahkan menurut penulis "Futures"
Alexander Mills

Masa depan sudah sangat tua dan usang. Lihat q sebagai gantinya.
Jim Aho

53

Saya suka solusi Raynos juga, tetapi saya lebih suka perpustakaan kontrol aliran yang berbeda.

https://github.com/caolan/async

Bergantung pada apakah Anda memerlukan hasil di setiap fungsi berikutnya, saya akan menggunakan seri, paralel, atau air terjun.

Seri ketika mereka harus dijalankan secara serial, tetapi Anda tidak memerlukan hasil di setiap pemanggilan fungsi berikutnya.

Paralel jika dapat dijalankan secara paralel, Anda tidak memerlukan hasil dari masing-masing selama setiap fungsi paralel, dan Anda memerlukan panggilan balik ketika semua telah selesai.

Air terjun jika Anda ingin mengubah hasil di setiap fungsi dan meneruskan ke fungsi berikutnya

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 = membutuhkan ('http');
Elle Mundy

7
Hah. example.com sebenarnya adalah domain yang dirancang untuk hal semacam ini. Wow.
meawoppl

Kode async.series tidak berfungsi, setidaknya pada async v0.2.10. series () hanya membutuhkan hingga dua argumen dan akan mengeksekusi elemen argumen pertama sebagai fungsi, jadi async melontarkan kesalahan saat mencoba mengeksekusi objek sebagai fungsi.
tutup

1
Anda dapat melakukan sesuatu yang mirip dengan apa yang dimaksudkan dengan kode ini menggunakan forEachAsync ( github.com/FuturesJS/forEachAsync ).
tutup

Ini melakukan apa yang saya inginkan. Terima kasih!
aProperFox

33

Anda dapat melakukan ini menggunakan pustaka Common Node saya :

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
Sial, saya pikir itu akan berhasil dan tidak :(require(...).HttpClient is not a constructor
moeiscool

30

permintaan sinkronisasi

Sejauh ini, yang paling mudah saya temukan dan gunakan adalah permintaan sinkronisasi dan mendukung node dan browser!

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

Itu saja, tidak ada konfigurasi gila, tidak ada instalasi lib yang rumit, meskipun ia memiliki cadangan lib. Bekerja saja. Saya telah mencoba contoh lain di sini dan bingung ketika ada banyak pengaturan tambahan yang harus dilakukan atau pemasangan tidak berhasil!

Catatan:

Contoh yang digunakan permintaan sinkronisasi tidak berfungsi dengan baik saat Anda menggunakannya res.getBody(), semua yang dilakukan get body adalah menerima encoding dan mengonversi data respons. Lakukan saja res.body.toString(encoding)sebagai gantinya.


Saya menemukan bahwa permintaan sinkronisasi sangat lambat .. Saya akhirnya menggunakan github.com/dhruvbird/http-sync yang 10 kali lebih cepat dalam kasus saya.
Filip Spiridonov

saya belum pernah berjalan lambat untuk itu. Ini melahirkan proses anak. Berapa banyak CPU yang digunakan sistem Anda dan versi node apa yang Anda gunakan? Saya ingin tahu untuk menentukan apakah saya perlu beralih atau tidak.
jemiloii

Saya setuju dengan Filip, ini lambat.
Rambo7

Hal yang sama saya tanyakan pada flip tetapi tidak mendapat jawaban: Berapa banyak CPU yang digunakan sistem Anda dan versi node apa yang Anda gunakan?
jemiloii

ini menggunakan CPU dalam jumlah besar, tidak disarankan untuk penggunaan produksi.
moeiscool

20

Saya akan menggunakan fungsi rekursif dengan daftar 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 );

edit: versi permintaan

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: request / versi async

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

Ini adalah metode yang saya gunakan karena saya memiliki daftar variabel permintaan yang harus dibuat (600 item dan terus bertambah). Meskipun demikian, ada masalah dengan kode Anda: peristiwa 'data' akan dipancarkan beberapa kali per permintaan jika keluaran API lebih besar dari ukuran potongan. Anda ingin "menyangga" data seperti ini: var body = ''; res.on ('data', function (data) {body + = data;}). on ('end', function () {callback (body); if (APIs.length) callAPIs (host, APIs);} );
Ankit Aggarwal

Diperbarui. Saya hanya ingin menunjukkan bagaimana masalahnya dapat dibuat lebih sederhana / lebih fleksibel melalui rekursi. Secara pribadi saya selalu menggunakan modul permintaan untuk hal semacam ini karena memungkinkan Anda melewati beberapa panggilan balik dengan mudah.
generalhenry

@ Generalhenry, bagaimana saya melakukan ini jika saya ingin menggunakan modul permintaan? Dapatkah Anda menawarkan potongan kode yang mencapai permintaan menggunakan di atas?
Scotty

Saya menambahkan versi permintaan dan versi permintaan / async.
generalhenry

5

Sepertinya solusi untuk masalah ini tidak pernah berakhir, ini satu lagi :)

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


Meskipun pustaka yang Anda tautkan TIDAK menawarkan solusi untuk masalah OP, dalam contoh Anda, fs.readFile selalu sinkron.
Eric

1
Tidak, Anda dapat menyediakan callback secara eksplisit dan menggunakannya sebagai versi asinkron jika diinginkan.
Alex Craft

1
contohnya adalah untuk permintaan http, bukan komunikasi sistem file.
Seth

5

Kemungkinan lainnya adalah mengatur panggilan balik yang melacak tugas yang sudah selesai:

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

Kemudian cukup tetapkan ID untuk masing-masing dan Anda dapat mengatur persyaratan Anda untuk tugas mana yang harus diselesaikan sebelum menutup koneksi.

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

Oke, ini tidak bagus. Ini hanyalah cara lain untuk melakukan panggilan berurutan. Sayangnya NodeJS tidak menyediakan panggilan sinkron yang paling dasar. Tapi saya mengerti apa iming-iming asinkronitas itu.


4

gunakan berurutan.

sudo npm menginstal sequenty

atau

https://github.com/AndyShin/sequenty

sangat sederhana.

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

Anda juga dapat menggunakan loop seperti ini:

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

Menggunakan pustaka permintaan dapat membantu meminimalkan 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
        })
    })
})

Tetapi untuk kehebatan maksimum Anda harus mencoba beberapa pustaka aliran kontrol seperti Langkah - ini juga akan memungkinkan Anda untuk memparalelkan permintaan, dengan asumsi itu dapat diterima:

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

Mulai 2018 dan menggunakan modul dan Janji ES6, kita dapat menulis fungsi seperti itu:

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

dan kemudian di modul lain

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

Kode perlu dijalankan dalam konteks asynchronous (menggunakan asynckata kunci)


2

Ada banyak pustaka aliran kontrol - Saya suka conseq (... karena saya yang menulisnya.) Juga, on('data')dapat diaktifkan beberapa kali, jadi gunakan pustaka pembungkus REST seperti 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

Berikut versi saya dari @ andy-shin secara berurutan dengan argumen dalam array, bukan indeks:

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

1

... 4 tahun kemudian ...

Berikut ini solusi asli dengan kerangka kerja Danf (Anda tidak memerlukan kode apa pun untuk hal semacam ini, hanya beberapa konfigurasi):

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

Gunakan nilai yang sama orderuntuk operasi yang ingin Anda jalankan secara paralel.

Jika Anda ingin menjadi lebih pendek, Anda dapat menggunakan proses pengumpulan:

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

Lihat ikhtisar kerangka kerja untuk informasi lebih lanjut.


1

Saya mendarat di sini karena saya perlu menilai-membatasi http.request (~ 10k kueri agregasi ke pencarian elastis untuk membuat laporan analitis). Berikut ini baru saja mencekik mesin saya.

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

URL saya sangat sederhana jadi ini mungkin tidak berlaku sepele untuk pertanyaan asli tetapi saya pikir itu berpotensi berlaku dan layak ditulis di sini untuk pembaca yang mendarat di sini dengan masalah yang mirip dengan saya dan yang menginginkan solusi JavaScript tanpa perpustakaan yang sepele.

Pekerjaan saya tidak tergantung pada pesanan dan pendekatan pertama saya untuk membungkus ini adalah membungkusnya dalam skrip shell untuk memotongnya (karena saya baru mengenal JavaScript). Itu fungsional tapi tidak memuaskan. Resolusi JavaScript saya pada akhirnya adalah melakukan hal berikut:

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

Sepertinya rekursi timbal balik antara collect dan get_top . Saya tidak yakin ini berlaku karena sistemnya asinkron dan fungsi kumpulkan selesai dengan callback yang disimpan untuk acara di on. ('End' .

Saya pikir ini cukup umum untuk diterapkan pada pertanyaan awal. Jika, seperti skenario saya, urutan / set diketahui, semua URL / kunci dapat didorong ke tumpukan dalam satu langkah. Jika mereka dihitung saat Anda melanjutkan, fungsi on ('end' dapat mendorong url berikutnya di tumpukan tepat sebelum get_top () . Jika ada, hasilnya memiliki lebih sedikit sarang dan mungkin lebih mudah untuk direfraktor ketika API yang Anda panggil perubahan.

Saya menyadari ini secara efektif setara dengan versi rekursif sederhana @ generalhenry di atas (jadi saya memberi suara positif!)


0

Permintaan Super

Ini adalah modul sinkron lain yang didasarkan pada permintaan dan menggunakan promise. Sangat mudah digunakan, bekerja dengan baik dengan tes mocha.

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

Kode ini dapat digunakan untuk menjalankan larik promise secara sinkron & berurutan setelah Anda dapat menjalankan kode terakhir dalam .then()panggilan tersebut.

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

Saya benar-benar mendapatkan apa yang Anda (dan saya) inginkan, tanpa menggunakan await, Promises, atau inklusi dari pustaka (eksternal) apa pun (kecuali milik kita sendiri).

Berikut cara melakukannya:

Kita akan membuat modul C ++ untuk digunakan bersama node.js, dan fungsi modul C ++ itu akan membuat permintaan HTTP dan mengembalikan data sebagai string, dan Anda dapat menggunakannya secara langsung dengan melakukan:

var myData = newModule.get(url);

APAKAH ANDA SIAP untuk memulai?

Langkah 1: buat folder baru di tempat lain di komputer Anda, kami hanya menggunakan folder ini untuk membuat file module.node (dikompilasi dari C ++), Anda dapat memindahkannya nanti.

Di folder baru (saya meletakkan milik saya di mynewFolder / src untuk pengaturan-ness):

npm init

kemudian

npm install node-gyp -g

sekarang buat 2 file baru: 1, bernama sesuatu.cpp dan untuk memasukkan kode ini di dalamnya (atau memodifikasinya jika Anda mau):

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

Sekarang buat file baru di direktori yang sama yang disebut something.gypdan letakkan (seperti) ini di dalamnya:

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

Sekarang di file package.json, tambahkan: "gypfile": true,

Sekarang: di konsol, node-gyp rebuild

Jika itu melewati seluruh perintah dan mengatakan "ok" di akhir tanpa kesalahan, Anda (hampir) baik untuk pergi, jika tidak, maka tinggalkan komentar ..

Tetapi jika berhasil, buka build / Release / cobypp.node (atau apa pun namanya untuk Anda), salin ke folder node.js utama Anda, lalu di node.js:

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

..

response.end(myData);//or whatever
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.