Beberapa tahun yang lalu kami menerbitkan RESTinio , kerangka kerja OpenSource C ++ kecil kami untuk menyematkan server HTTP dalam aplikasi C ++. RESTinio tidak menjadi mega-populer selama waktu ini, tetapi itu tidak hilang . Seseorang memilihnya untuk dukungan "asli" untuk Windows, seseorang untuk beberapa fitur individual (seperti dukungan sendfile), seseorang untuk rasio fitur, kemudahan penggunaan dan penyesuaian. Tapi, saya pikir, pada awalnya banyak RESTinio tertarik oleh "Halo, Dunia" singkat ini:
#include <restinio/all.hpp> int main() { restinio::run( restinio::on_this_thread() .port(8080) .address("localhost") .request_handler([](auto req) { return req->create_response().set_body("Hello, World!").done(); })); return 0; }
Ini benar-benar semua yang diperlukan untuk menjalankan server HTTP di dalam aplikasi C ++.
Dan meskipun kami selalu mencoba mengatakan bahwa fitur utama yang kami gunakan secara umum di RESTinio adalah pemrosesan permintaan masuk yang tidak sinkron, kami terkadang masih menghadapi pertanyaan tentang apa yang harus dilakukan jika di dalam request_handler Anda harus melakukan operasi yang panjang.
Dan karena pertanyaan seperti itu relevan, maka Anda dapat membicarakannya lagi dan memberikan beberapa contoh kecil.
Sedikit referensi tentang asal-usulnya
Kami memutuskan untuk membuat server HTTP yang dapat disematkan kami setelah beberapa kali berturut-turut menghadapi tugas yang sangat mirip: perlu untuk mengatur input HTTP untuk aplikasi C ++ yang ada atau perlu untuk menulis layanan microser yang diperlukan untuk menggunakan kembali C "heavy" yang sudah ada. kode ny. Fitur umum dari tugas-tugas ini adalah bahwa pemrosesan aplikasi permintaan dapat berlangsung selama puluhan detik.
Secara kasar, untuk satu milidetik, server HTTP memilah permintaan HTTP baru, tetapi untuk mengeluarkan respons HTTP, perlu beralih ke beberapa layanan lain atau melakukan beberapa perhitungan panjang. Jika Anda memproses permintaan HTTP dalam mode sinkron, maka server HTTP akan membutuhkan kumpulan ribuan utas kerja, yang hampir tidak dapat dianggap sebagai ide yang baik bahkan dalam kondisi modern.
Ini jauh lebih nyaman ketika server HTTP dapat bekerja hanya pada satu utas yang berfungsi, di mana I / O dilakukan dan penangan permintaan dipanggil. Penangan permintaan hanya mendelegasikan pemrosesan aktual dari beberapa utas kerja lainnya dan mengembalikan kontrol ke server HTTP. Ketika, jauh di kemudian hari, di suatu tempat di utas lainnya, informasi siap merespons permintaan, respons HTTP dihasilkan secara otomatis yang mengambil server HTTP dan mengirimkan respons ini ke klien yang sesuai.
Karena kami tidak pernah menemukan versi siap pakai yang sederhana dan nyaman untuk digunakan, itu adalah cross-platform dan mendukung Windows sebagai platform "asli", akan memberikan kinerja yang lebih atau kurang layak, dan, yang paling penting, akan dipertajam khusus untuk asinkron. bekerja, maka pada awal 2017 kami mulai mengembangkan RESTinio.
Kami ingin membuat server HTTP tertanam asinkron, mudah digunakan, membebaskan pengguna dari beberapa kekhawatiran rutin, sementara lebih atau kurang produktif, lintas platform dan memungkinkan konfigurasi fleksibel untuk kondisi yang berbeda. Tampaknya berhasil, tapi mari kita serahkan kepada pengguna untuk menilai ...
Jadi, ada permintaan masuk yang membutuhkan banyak waktu pemrosesan. Apa yang harus dilakukan
Utas kerja RESTinio / Asio
Terkadang pengguna RESTinio tidak memikirkan tentang utas kerja apa dan bagaimana tepatnya menggunakan RESTinio. Misalnya, seseorang mungkin menganggap bahwa ketika RESTinio diluncurkan pada satu utas yang berfungsi (menggunakan run(on_this_thread(...))
, seperti dalam contoh di atas), maka pada utas kerja ini RESTinio hanya memanggil penangan permintaan. Sedangkan untuk I / O RESTinio membuat utas terpisah di bawah tenda. Dan utas terpisah ini terus melayani koneksi baru ketika utas kerja utama ditempati oleh request_handler.
Faktanya, semua utas yang dialokasikan pengguna untuk RESTinio digunakan baik untuk melakukan operasi I / O maupun untuk memanggil request_handlers. Oleh karena itu, jika Anda memulai server RESTinio melalui proses run(on_this_thread(...))
, maka di dalam proses run()
pada utas saat ini, I / O dan penangan permintaan akan dilakukan.
Secara kasar, RESTinio meluncurkan loop-event Asio, di dalamnya ia memproses koneksi baru, membaca dan mem-parsing data dari koneksi yang ada, menulis data yang siap untuk dikirim, menangani koneksi penutupan, dll. Antara lain, setelah permintaan masuk dibaca dan sepenuhnya diuraikan dari koneksi berikutnya, request_handler yang ditentukan oleh pengguna dipanggil untuk memproses permintaan ini.
Dengan demikian, jika request_handler memblokir operasi utas saat ini, maka perulangan-aksi Asio yang bekerja pada utas yang sama juga diblokir. Semuanya sederhana.
Jika RESTinio dimulai pada kumpulan utas yang berfungsi (mis. Dengan run(on_thread_pool(...))
, seperti dalam contoh ini ), maka hal yang hampir sama terjadi: loop peristiwa Asia-event diluncurkan pada setiap utas dari kumpulan tersebut. Oleh karena itu, jika beberapa request_handler mulai melipatgandakan matriks besar, maka ini akan memblokir utas yang berfungsi dalam kumpulan dan operasi I / O tidak lagi dilayani pada utas ini.
Oleh karena itu, ketika menggunakan RESTinio, tugas pengembang adalah untuk menyelesaikan request_handlers-nya secara wajar dan, lebih disukai, tidak terlalu lama.
Apakah Anda memerlukan kumpulan alur kerja untuk RESTinio / Asio?
Jadi, ketika request_handler ditentukan oleh pengguna memblokir utas yang dipanggil untuk waktu yang lama, utas ini kehilangan kemampuan untuk memproses operasi I / O. Tetapi bagaimana jika request_handler membutuhkan banyak waktu untuk membentuk respons? Misalkan dia melakukan semacam operasi komputasi yang berat, waktu yang, pada prinsipnya, tidak dapat dipersingkat menjadi beberapa milidetik?
Salah satu pengguna mungkin berpikir bahwa karena RESTinio dapat bekerja di kumpulan utas yang berfungsi, maka cukup tentukan ukuran kumpulan yang lebih besar dan hanya itu.
Sayangnya, ini hanya akan berfungsi dalam kasus sederhana ketika Anda memiliki beberapa koneksi paralel. Dan intensitas permintaannya rendah. Jika jumlah kueri paralel mencapai ribuan (setidaknya hanya beberapa ratus), maka mudah untuk mendapatkan situasi ketika semua utas kerja kumpulan akan sibuk memproses permintaan yang sudah diterima. Dan tidak akan ada lagi utas yang tersisa untuk melakukan operasi I / O. Akibatnya, server akan kehilangan responsifnya. Termasuk RESTinio akan kehilangan kemampuan untuk memproses timeout yang secara otomatis dihitung oleh RESTinio ketika menerima koneksi baru dan ketika memproses permintaan.
Oleh karena itu, jika Anda perlu melakukan operasi pemblokiran yang panjang untuk melayani permintaan yang masuk, lebih baik mengalokasikan hanya satu utas kerja untuk RESTinio, tetapi menetapkan aliran kerja yang besar untuk melakukan operasi yang sama ini. Penangan permintaan hanya akan memasukkan permintaan berikutnya dalam beberapa antrian, dari mana permintaan akan diambil dan diserahkan untuk diproses.
Kami melihat contoh skema ini secara rinci ketika kami berbicara tentang proyek demo Udang kami di artikel ini: " Udang: Skala dan Bagikan Gambar HTTP di C ++ Modern Menggunakan ImageMagic ++, SObjectizer, dan RESTinio ."
Contoh pendelegasian pemrosesan permintaan ke utas kerja individual
Di atas, saya mencoba menjelaskan mengapa tidak perlu melakukan pemrosesan yang panjang tepat di dalam request_handler. Dari mana hasil yang jelas berasal: pemrosesan permintaan yang panjang harus didelegasikan ke beberapa utas kerja lainnya. Mari kita lihat bagaimana ini terlihat.
Dalam dua contoh di bawah ini, kita perlu utas kerja tunggal untuk menjalankan RESTinio dan utas kerja lainnya untuk mensimulasikan pemrosesan permintaan yang panjang. Dan juga kita perlu semacam antrian pesan untuk mentransfer permintaan dari utas RESTinio ke utas yang bekerja terpisah.
Tidak mudah bagi saya untuk membuat implementasi antrian pesan thread-safe baru di lutut saya untuk dua contoh ini, jadi saya menggunakan SObjectizer asli saya dan mchainsnya, yang merupakan saluran CSP. Anda dapat membaca lebih lanjut tentang mchain di sini: " Pertukaran informasi antara utas yang bekerja tanpa rasa sakit? Saluran CSP untuk membantu kami ."
Menyimpan objek request_handle
Teknik dasar di mana delegasi pemrosesan permintaan dibangun adalah transfer objek request_handle_t
suatu tempat.
Ketika RESTinio memanggil request_handler yang ditentukan oleh pengguna untuk memproses permintaan yang masuk, objek tipe request_handle_t
diteruskan ke request_handle_t
ini. Jenis ini tidak lebih dari penunjuk pintar ke parameter permintaan yang diterima. Jadi jika nyaman bagi seseorang untuk berpikir bahwa request_handle_t
adalah shared_ptr
, maka Anda dapat dengan aman memikirkannya. shared_ptr
ini adalah.
Dan karena request_handle_t
adalah shared_ptr
, kita dapat dengan aman melewatkan pointer pintar ini di suatu tempat. Apa yang akan kita lakukan dalam contoh di bawah ini.
Jadi, kami membutuhkan utas dan saluran kerja terpisah untuk berkomunikasi dengannya. Mari kita buat semuanya:
int main() {
Tubuh dari utas kerja itu sendiri terletak di dalam fungsi processing_thread_func()
, yang akan kita bahas nanti.
Sekarang kami sudah memiliki utas yang terpisah dan saluran untuk berkomunikasi dengannya. Anda dapat memulai server RESTinio:
Logika untuk server ini sangat sederhana. Jika permintaan GET telah tiba untuk '/', maka kami mendelegasikan pemrosesan permintaan utas tunggal. Untuk melakukan ini, kami melakukan dua operasi penting:
- mengirim objek
request_handle_t
ke saluran CSP. Sementara objek ini disimpan di dalam saluran CSP atau di tempat lain, RESTinio tahu bahwa permintaan tersebut masih hidup; - kami mengembalikan nilai
restinio::request_accepted()
dari penangan permintaan. Ini membuat RESTinio mengerti bahwa permintaan telah diterima untuk diproses dan bahwa koneksi dengan klien tidak dapat ditutup.
Fakta bahwa request_handler tidak segera menghasilkan respons RESTinio tidak mengganggu. Setelah restinio::request_accepted()
dikembalikan, maka pengguna bertanggung jawab untuk memroses permintaan dan suatu hari nanti respons terhadap permintaan akan dihasilkan.
Jika penangan permintaan mengembalikan restinio::request_rejected()
, maka RESTinio memahami bahwa permintaan tidak akan diproses dan akan mengembalikan kesalahan 501 kepada klien.
Jadi, kami memperbaiki hasil awal: instance request_handle_t
dapat dikirimkan di suatu tempat, karena itu, pada kenyataannya, std::shared_ptr
. Saat instance ini aktif, RESTinio menganggap bahwa permintaan sedang diproses. Jika penangan permintaan mengembalikan restinio::request_accepted()
, maka RESTinio tidak akan khawatir bahwa respons terhadap permintaan belum dihasilkan saat ini.
Sekarang kita bisa melihat implementasi dari utas yang sangat terpisah ini:
void processing_thread_func(so_5::mchain_t req_ch) {
Logikanya di sini sangat sederhana: kami menerima permintaan awal dalam bentuk pesan handle_request
dan meneruskannya kepada diri kami sendiri dalam bentuk pesan timeout_elapsed
tertunda untuk beberapa waktu acak. Kami melakukan pemrosesan permintaan yang sebenarnya hanya setelah menerima timeout_elapsed
.
Pembaruan. Ketika metode done()
dipanggil pada utas yang terpisah, RESTinio diberi tahu bahwa respons siap pakai telah muncul yang perlu ditulis ke koneksi TCP. RESTinio memulai operasi penulisan, tetapi operasi I / O itu sendiri tidak akan dieksekusi jika done()
dipanggil, tetapi di mana RESTinio melakukan I / O dan memanggil request_handlers. Yaitu dalam contoh ini, done()
dipanggil pada utas yang terpisah, dan operasi tulis akan dilakukan pada utas utama, di mana restinio::run()
berfungsi.
Pesan-pesan itu sendiri adalah sebagai berikut:
struct handle_request { restinio::request_handle_t m_req; }; struct timeout_elapsed { restinio::request_handle_t m_req; std::chrono::milliseconds m_pause; };
Yaitu utas yang terpisah mengambil request_handle_t
dan menyimpannya sampai ada kesempatan untuk membentuk respons yang lengkap. Dan ketika kesempatan ini muncul, create_response()
dipanggil pada objek permintaan tersimpan dan respons dikembalikan ke RESTinio. Maka RESTinio sudah dalam konteks kerjanya menulis tanggapan sehubungan dengan klien yang sesuai.
Di sini, instance request_handle_t
disimpan dalam pesan tertunda timeout_elapsed
, karena tidak ada pemrosesan nyata dalam contoh primitif ini. Dalam aplikasi nyata, request_handle_t
dapat disimpan dalam semacam antrian atau di dalam beberapa objek yang dibuat untuk memproses permintaan.
Kode lengkap untuk contoh ini dapat ditemukan di antara contoh RESTinio biasa .
Beberapa catatan kode kecil
Konstruksi ini menetapkan properti RESTinio yang harus dimiliki oleh server RESTinio:
Untuk contoh ini, saya perlu RESTinio untuk mencatat tindakan pemrosesan permintaannya. Oleh karena itu, saya mengatur logger_t
menjadi berbeda dari null_logger_t
default. Tapi sejak itu RESTinio akan bekerja, pada kenyataannya, pada beberapa utas (RESTinio memproses permintaan yang masuk pada utas utama, tetapi tanggapan datang darinya dari utas yang bekerja terpisah), maka Anda memerlukan logger yang aman utas, yang digunakan bersama dengan shared_ostream_logger_t
.
Di dalam processing_thread_func()
, fungsi SObjectizer select()
, yang agak mirip dengan konstruksi Go-shn pilih: Anda dapat membaca dan memproses pesan dari beberapa saluran sekaligus. Fungsi select()
berfungsi sampai semua saluran yang dilewati ditutup. Atau sampai dia secara paksa diberi tahu bahwa inilah saatnya untuk mengakhiri.
Pada saat yang sama, jika saluran untuk komunikasi dengan server RESTinio ditutup, maka tidak ada gunanya melanjutkan pekerjaan. Oleh karena itu, di select()
, respons untuk menutup salah satu saluran ditentukan: segera setelah saluran ditutup, bendera berhenti dinaikkan. Dan ini akan mengarah pada penyelesaian select()
dan keluar dari processing_thread_func()
.
Menyimpan objek response_builder
Pada contoh sebelumnya, kami mempertimbangkan kasus sederhana saat dimungkinkan untuk menyimpan request_handle_t
hingga kami dapat segera memberikan seluruh respons terhadap permintaan tersebut.
Tetapi mungkin ada skenario yang lebih kompleks ketika, misalnya, Anda perlu memberikan jawaban di bagian. Artinya, kami menerima permintaan, kami hanya dapat langsung membentuk bagian pertama dari respons. Kami membentuknya. Kemudian, setelah beberapa waktu, kami memiliki kesempatan untuk membentuk bagian kedua dari jawaban. Kemudian, setelah beberapa waktu lagi, kita dapat membentuk bagian selanjutnya, dll.
Selain itu, mungkin diinginkan bagi kita bahwa semua bagian ini hilang begitu kita membentuknya. Yaitu Pertama, bagian pertama dari jawaban sehingga klien dapat mengurangkannya, lalu yang kedua, kemudian yang ketiga, dll.
RESTinio memungkinkan Anda melakukan ini karena berbagai jenis responsce_builders . Secara khusus, tipe seperti user_controlled_output dan chunked_output .
Dalam hal ini, tidak cukup untuk menyimpan request_handle_t
, karena request_handle_t
akan berguna sampai panggilan pertama ke create_reponse()
. Selanjutnya kita perlu bekerja dengan response_builder. Baiklah ...
Yah, tidak apa-apa. Response_builder adalah tipe yang dapat dipindahkan, agak mirip dengan unique_ptr. Jadi kita juga bisa menyimpannya selama kita membutuhkannya. Dan untuk menunjukkan tampilannya, kami sedikit mengulangi contoh di atas. Biarkan fungsi processing_thread_func()
membentuk respons di bagian-bagian.
Ini sama sekali tidak sulit.
Pertama-tama kita perlu memutuskan jenis-jenis yang membutuhkan:
struct handle_request { restinio::request_handle_t m_req; };
Pesan handle_request
tetap tidak berubah. Namun dalam pesan timeout_elapsed
kami sekarang menyimpan bukan request_handle_t
, tetapi response_builder dari jenis yang kami butuhkan. Ditambah penghitung dari bagian yang tersisa. Segera setelah penghitung ini diatur ulang, permintaan layanan akan berakhir.
Sekarang kita dapat melihat versi baru dari fungsi processing_thread_func()
:
void processing_thread_func(so_5::mchain_t req_ch) { std::random_device rd; std::mt19937 generator{rd()}; std::uniform_int_distribution<> pause_generator{350, 3500}; auto delayed_ch = so_5::create_mchain(req_ch->environment()); bool stop = false; select( so_5::from_all() .on_close([&stop](const auto &) { stop = true; }) .stop_on([&stop]{ return stop; }), case_(req_ch, [&](handle_request cmd) {
Yaitu , . . .
Upd. flush()
, done()
: RESTinio , I/O- , flush()
, , RESTinio - request_handler-. Yaitu flush()
, , , restinio::run()
.
, RESTinio :
[2019-05-13 15:02:35.106] TRACE: starting server on 127.0.0.1:8080 [2019-05-13 15:02:35.106] INFO: init accept #0 [2019-05-13 15:02:35.106] INFO: server started on 127.0.0.1:8080 [2019-05-13 15:02:39.050] TRACE: accept connection from 127.0.0.1:49280 on socket #0 [2019-05-13 15:02:39.050] TRACE: [connection:1] start connection with 127.0.0.1:49280 [2019-05-13 15:02:39.050] TRACE: [connection:1] start waiting for request [2019-05-13 15:02:39.050] TRACE: [connection:1] continue reading request [2019-05-13 15:02:39.050] TRACE: [connection:1] received 78 bytes [2019-05-13 15:02:39.050] TRACE: [connection:1] request received (#0): GET / [2019-05-13 15:02:39.050] TRACE: [connection:1] append response (#0), flags: { not_final_parts, connection_keepalive }, write group size: 1 [2019-05-13 15:02:39.050] TRACE: [connection:1] start next write group for response (#0), size: 1 [2019-05-13 15:02:39.050] TRACE: [connection:1] start response (#0): HTTP/1.1 200 OK [2019-05-13 15:02:39.050] TRACE: [connection:1] sending resp data, buf count: 1, total size: 167 [2019-05-13 15:02:39.050] TRACE: [connection:1] outgoing data was sent: 167 bytes [2019-05-13 15:02:39.050] TRACE: [connection:1] finishing current write group [2019-05-13 15:02:39.050] TRACE: [connection:1] should keep alive [2019-05-13 15:02:40.190] TRACE: [connection:1] append response (#0), flags: { not_final_parts, connection_keepalive }, write group size: 3 [2019-05-13 15:02:40.190] TRACE: [connection:1] start next write group for response (#0), size: 3 [2019-05-13 15:02:40.190] TRACE: [connection:1] sending resp data, buf count: 3, total size: 42 [2019-05-13 15:02:40.190] TRACE: [connection:1] outgoing data was sent: 42 bytes [2019-05-13 15:02:40.190] TRACE: [connection:1] finishing current write group [2019-05-13 15:02:40.190] TRACE: [connection:1] should keep alive [2019-05-13 15:02:43.542] TRACE: [connection:1] append response (#0), flags: { not_final_parts, connection_keepalive }, write group size: 3 [2019-05-13 15:02:43.542] TRACE: [connection:1] start next write group for response (#0), size: 3 [2019-05-13 15:02:43.542] TRACE: [connection:1] sending resp data, buf count: 3, total size: 42 [2019-05-13 15:02:43.542] TRACE: [connection:1] outgoing data was sent: 42 bytes [2019-05-13 15:02:43.542] TRACE: [connection:1] finishing current write group [2019-05-13 15:02:43.542] TRACE: [connection:1] should keep alive [2019-05-13 15:02:46.297] TRACE: [connection:1] append response (#0), flags: { not_final_parts, connection_keepalive }, write group size: 3 [2019-05-13 15:02:46.297] TRACE: [connection:1] start next write group for response (#0), size: 3 [2019-05-13 15:02:46.297] TRACE: [connection:1] sending resp data, buf count: 3, total size: 42 [2019-05-13 15:02:46.297] TRACE: [connection:1] append response (#0), flags: { final_parts, connection_keepalive }, write group size: 1 [2019-05-13 15:02:46.297] TRACE: [connection:1] outgoing data was sent: 42 bytes [2019-05-13 15:02:46.298] TRACE: [connection:1] finishing current write group [2019-05-13 15:02:46.298] TRACE: [connection:1] should keep alive [2019-05-13 15:02:46.298] TRACE: [connection:1] start next write group for response (#0), size: 1 [2019-05-13 15:02:46.298] TRACE: [connection:1] sending resp data, buf count: 1, total size: 5 [2019-05-13 15:02:46.298] TRACE: [connection:1] outgoing data was sent: 5 bytes [2019-05-13 15:02:46.298] TRACE: [connection:1] finishing current write group [2019-05-13 15:02:46.298] TRACE: [connection:1] should keep alive [2019-05-13 15:02:46.298] TRACE: [connection:1] start waiting for request [2019-05-13 15:02:46.298] TRACE: [connection:1] continue reading request [2019-05-13 15:02:46.298] TRACE: [connection:1] EOF and no request, close connection [2019-05-13 15:02:46.298] TRACE: [connection:1] close [2019-05-13 15:02:46.298] TRACE: [connection:1] close: close socket [2019-05-13 15:02:46.298] TRACE: [connection:1] close: timer canceled [2019-05-13 15:02:46.298] TRACE: [connection:1] close: reset responses data [2019-05-13 15:02:46.298] TRACE: [connection:1] destructor called
, RESTinio 167 . , , RESTinio .
, RESTinio - response_builder , .
. , , . response_builder . , responce_builder , ..
.
, ?
, request_handler- - . , , ?
RESTinio , - request_handler-. - , , RESTinio . , . , :
[2019-05-13 15:32:23.618] TRACE: starting server on 127.0.0.1:8080 [2019-05-13 15:32:23.618] INFO: init accept #0 [2019-05-13 15:32:23.618] INFO: server started on 127.0.0.1:8080 [2019-05-13 15:32:26.768] TRACE: accept connection from 127.0.0.1:49502 on socket #0 [2019-05-13 15:32:26.768] TRACE: [connection:1] start connection with 127.0.0.1:49502 [2019-05-13 15:32:26.768] TRACE: [connection:1] start waiting for request [2019-05-13 15:32:26.768] TRACE: [connection:1] continue reading request [2019-05-13 15:32:26.768] TRACE: [connection:1] received 78 bytes [2019-05-13 15:32:26.768] TRACE: [connection:1] request received (#0): GET / [2019-05-13 15:32:30.768] TRACE: [connection:1] handle request timed out [2019-05-13 15:32:30.768] TRACE: [connection:1] close [2019-05-13 15:32:30.768] TRACE: [connection:1] close: close socket [2019-05-13 15:32:30.768] TRACE: [connection:1] close: timer canceled [2019-05-13 15:32:30.768] TRACE: [connection:1] close: reset responses data [2019-05-13 15:32:31.768] WARN: [connection:1] try to write response, while socket is closed [2019-05-13 15:32:31.768] TRACE: [connection:1] destructor called
- . , , RESTinio , .. .
- handle_request_timeout
, RESTinio- ( ).
Kesimpulan
, , RESTinio â , . , RESTinio, , RESTinio, .
RESTinio , , , : ? - ? - ? - - ?
PS. RESTinio , SObjectizer, . , - RESTinio , : " C++ HTTP- ", " HTTP- C++: RESTinio, libcurl. 1 ", " Shrimp: HTTP C++ ImageMagic++, SObjectizer RESTinio "