
Baru-baru ini, saya kebetulan mengerjakan aplikasi yang seharusnya mengontrol kecepatan koneksi keluarnya. Sebagai contoh, menghubungkan ke satu URL, aplikasi harus membatasi dirinya sendiri, katakanlah, 200KiB / detik. Dan menghubungkan ke URL lain - hanya 30KiB / detik.
Poin paling menarik di sini adalah menguji batasan-batasan ini. Saya membutuhkan server HTTP yang akan memberikan lalu lintas pada kecepatan tertentu, misalnya, 512KiB / detik. Kemudian saya bisa melihat apakah aplikasi tersebut benar-benar tahan terhadap kecepatan 200KiB / detik atau jika rusak ke kecepatan yang lebih tinggi.
Tapi di mana mendapatkan server HTTP seperti itu?
Karena saya ada hubungannya dengan server HTTP RESTinio yang tertanam dalam aplikasi C ++, saya belum menemukan yang lebih baik daripada dengan cepat melemparkan server uji HTTP sederhana di lutut saya yang dapat mengirim aliran panjang data keluar ke klien.
Tentang betapa sederhananya dan ingin diceritakan dalam artikel. Pada saat yang sama, cari tahu di komentar apakah ini benar-benar sederhana atau apakah saya menipu diri sendiri. Pada prinsipnya, artikel ini dapat dianggap sebagai kelanjutan dari artikel sebelumnya tentang RESTinio yang disebut "RESTinio adalah server HTTP asinkron. Asynchronous" . Oleh karena itu, jika seseorang tertarik untuk membaca tentang aplikasi RESTinio yang asli, walaupun tidak terlalu serius, maka Anda dapat melakukannya.
Ide umum
Gagasan umum dari server uji yang disebutkan di atas sangat sederhana: ketika klien terhubung ke server dan melakukan permintaan GET HTTP, timer diaktifkan yang berjalan sekali per detik. Ketika timer dipicu, blok data berikutnya dari ukuran yang diberikan dikirim ke klien.
Tapi semuanya sedikit lebih rumit
Jika klien membaca data pada kecepatan yang lebih lambat daripada yang dikirim server, maka hanya mengirim N kilobyte satu detik sekali bukan ide yang baik. Karena data akan mulai menumpuk di soket dan ini tidak akan menghasilkan sesuatu yang baik.
Oleh karena itu, saat mengirim data, disarankan untuk mengontrol kesiapan soket untuk menulis di sisi server HTTP. Selama soket siap (artinya, terlalu banyak data belum terakumulasi di dalamnya), Anda dapat mengirim bagian baru. Tetapi jika tidak siap, maka Anda harus menunggu sampai soket siap untuk direkam.
Kedengarannya masuk akal, tetapi operasi I / O tersembunyi di jeroan ayam itik RESTINO ... Bagaimana saya bisa mengetahui apakah data selanjutnya dapat ditulis atau tidak?
Anda bisa keluar dari situasi ini jika Anda menggunakan notifiers setelah-tulis , yang ada di RESTinio. Sebagai contoh, kita dapat menulis ini:
void request_handler(restinio::request_handle_t req) { req->create_response()
Lambda yang diteruskan ke metode done()
akan dipanggil ketika RESTinio selesai menulis data keluar. Dengan demikian, jika soket tidak siap untuk merekam untuk beberapa waktu, maka lambda tidak akan dipanggil segera, tetapi setelah soket datang ke keadaan semestinya dan menerima semua data keluar.
Karena penggunaan notifiers setelah-tulis, logika server pengujian adalah sebagai berikut:
- kirim kumpulan data berikutnya, hitung waktu ketika kami perlu mengirim kumpulan berikutnya dalam kegiatan normal;
- kami menggantung setelah menulis notifier pada bagian data berikutnya;
- ketika pemberitahuan setelah menulis dipanggil, kami memeriksa untuk melihat apakah kumpulan berikutnya telah tiba. Jika ya, maka segera lakukan pengiriman bagian selanjutnya. Jika tidak, maka cock timer.
Hasilnya, ternyata segera setelah perekaman mulai melambat, pengiriman data baru akan berhenti. Dan lanjutkan ketika soket siap menerima data keluar baru.
Dan sedikit lebih rumit: chunked_output
RESTinio mendukung tiga cara untuk menghasilkan respons terhadap permintaan HTTP . Metode paling sederhana, yang digunakan secara default, tidak cocok dalam kasus ini, karena Saya membutuhkan aliran data keluar yang hampir tak ada habisnya. Dan aliran seperti itu, tentu saja, tidak dapat set_body
ke satu panggilan ke metode set_body
.
Oleh karena itu, server pengujian yang dijelaskan menggunakan apa yang disebut chunked_output . Yaitu saat membuat jawaban, saya menunjukkan kepada RESTinio bahwa jawabannya akan dibentuk sebagian. Kemudian saya secara berkala memanggil metode append_chunk
untuk menambahkan bagian selanjutnya pada jawaban dan flush
untuk menulis bagian yang terakumulasi ke soket.
Dan mari kita lihat kodenya!
Mungkin sudah cukup bahwa kata-kata pembuka sudah cukup dan sudah waktunya untuk beralih ke kode itu sendiri, yang dapat ditemukan dalam repositori ini . Mari kita mulai dengan fungsi request_processor
, yang dipanggil untuk memproses setiap permintaan HTTP yang valid. Pada saat yang sama, mari kita mempelajari fungsi-fungsi yang dipanggil dari request_processor
. Baiklah, maka kita akan melihat bagaimana sebenarnya request_processor
dipetakan ke satu atau permintaan HTTP lain yang masuk.
Fungsi Request_processor dan pembantunya
Fungsi request_processor
dipanggil untuk memproses permintaan GET HTTP yang saya butuhkan. Itu disampaikan sebagai argumen:
- Asio-shny io_context di mana semua pekerjaan dilakukan (itu akan diperlukan, misalnya, untuk pengatur waktu pengadukan);
- ukuran satu bagian dari respons. Yaitu jika saya perlu memberikan aliran keluar dengan kecepatan 512KiB / detik, maka nilai 512KiB akan diteruskan sebagai parameter ini;
- sejumlah bagian dalam respons. Dalam hal aliran harus memiliki panjang yang terbatas. Misalnya, jika Anda ingin memberikan streaming dengan kecepatan 512KiB / detik selama 5 menit, maka nilai 300 akan diteruskan sebagai parameter ini (60 blok per menit selama 5 menit);
- Nah, permintaan yang masuk sendiri untuk diproses.
Di dalam request_processor
, sebuah objek dibuat dengan informasi tentang permintaan dan parameter pemrosesannya, setelah itu proses ini dimulai:
void request_processor( asio_ns::io_context & ctx, std::size_t chunk_size, std::size_t count, restinio::request_handle_t req) { auto data = std::make_shared<response_data>( ctx, chunk_size, req->create_response<output_t>(), count); data->response_ .append_header(restinio::http_field::server, "RESTinio") .append_header_date_field() .append_header( restinio::http_field::content_type, "text/plain; charset=utf-8") .flush(); send_next_portion(data); }
Jenis response_data
, berisi semua parameter yang terkait dengan permintaan, terlihat seperti ini:
struct response_data { asio_ns::io_context & io_ctx_; std::size_t chunk_size_; response_t response_; std::size_t counter_; response_data( asio_ns::io_context & io_ctx, std::size_t chunk_size, response_t response, std::size_t counter) : io_ctx_{io_ctx} , chunk_size_{chunk_size} , response_{std::move(response)} , counter_{counter} {} };
Perlu dicatat di sini bahwa salah satu alasan untuk penampilan struktur response_data
adalah bahwa objek bertipe restinio::response_builder_t<restinio::chunked_output_t>
(yaitu, tipe ini tersembunyi di belakang alias pendek response_t
) adalah tipe yang dapat dipindahkan, tetapi bukan tipe yang dapat disalin (oleh analogi dengan std::unique_ptr
). Oleh karena itu, objek ini tidak bisa hanya ditangkap dalam fungsi lambda, yang kemudian membungkus dirinya dalam std::function
. Tetapi jika Anda menempatkan objek respons dalam instance response_data
dibuat secara dinamis, maka pointer pintar ke instance reponse_data
sudah dapat ditangkap dalam fungsi lambda tanpa masalah, dan kemudian menyimpan lambda ini ke std::function
.
Fungsi Send_next_portion
Fungsi send_next_portion
dipanggil setiap kali ketika perlu untuk mengirim bagian selanjutnya dari respons kepada klien. Tidak ada yang rumit terjadi di dalamnya, sehingga terlihat cukup sederhana dan ringkas:
void send_next_portion(response_data_shptr data) { data->response_.append_chunk(make_buffer(data->chunk_size_)); if(1u == data->counter_) { data->response_.flush(); data->response_.done(); } else { data->counter_ -= 1u; data->response_.flush(make_done_handler(data)); } }
Yaitu kirim bagian selanjutnya. Dan, jika bagian ini adalah yang terakhir, maka kami menyelesaikan pemrosesan permintaan. Dan jika bukan yang terakhir, maka flush
dikirim ke metode flush
, yang dibuat, mungkin, oleh fungsi paling kompleks dari contoh ini.
Fungsi make_done_handler
Fungsi make_done_handler
bertanggung jawab untuk membuat lambda yang akan diteruskan ke RESTinio sebagai pemberi notifikasi setelah menulis. Pemberitahu ini harus memeriksa apakah rekaman bagian tanggapan berikutnya telah berhasil diselesaikan. Jika ya, maka Anda perlu mencari tahu apakah bagian selanjutnya harus segera dikirim (yaitu, ada "rem" di soket dan laju pengiriman tidak dapat dipertahankan), atau setelah jeda. Jika Anda perlu jeda, maka itu disediakan melalui pengukur waktu.
Secara umum, tindakan sederhana, tetapi dalam kode Anda mendapatkan lambda di dalam lambda, yang dapat membingungkan orang-orang yang tidak terbiasa dengan "modern" C ++. Yang tidak beberapa tahun disebut modern;)
auto make_done_handler(response_data_shptr data) { const auto next_timepoint = steady_clock::now() + 1s; return [=](const auto & ec) { if(!ec) { const auto now = steady_clock::now(); if(now < next_timepoint) { auto timer = std::make_shared<asio_ns::steady_timer>(data->io_ctx_); timer->expires_after(next_timepoint - now); timer->async_wait([timer, data](const auto & ec) { if(!ec) send_next_portion(data); }); } else data->io_ctx_.post([data] { send_next_portion(data); }); } }; }
Menurut pendapat saya, kesulitan utama dalam kode ini berasal dari kekhasan pembuatan dan peleton pengatur waktu di Asio. Menurut pendapat saya, ternyata entah bagaimana terlalu bertele-tele. Tapi memang ada, itu benar. Tetapi Anda tidak perlu menarik perpustakaan tambahan apa pun.
Menghubungkan router seperti ekspres
send_next_portion
, send_next_portion
dan make_done_handler
ditunjukkan di atas send_next_portion
merupakan versi pertama dari server pengujian saya, ditulis secara harfiah dalam 15 atau 20 menit.
Tetapi setelah beberapa hari menggunakan server uji ini, ternyata ada kelemahan serius di dalamnya: selalu mengembalikan aliran respons dengan kecepatan yang sama. Dikompilasi pada kecepatan 512KiB / detik - memberikan semua 512KiB / detik. Dikompilasi ulang dengan kecepatan 20KiB / detik - akan memberikan kepada semua orang 20KiB / detik dan tidak ada yang lain. Apa yang merepotkan, karena menjadi perlu untuk dapat menerima jawaban dari "ketebalan" yang berbeda.
Kemudian muncul ide: bagaimana jika kecepatan balik diminta langsung di URL? Misalnya, mereka mengajukan permintaan ke localhost:8080/
dan menerima respons pada kecepatan yang telah ditentukan. Dan jika Anda mengajukan permintaan ke localhost:8080/128K
, maka mereka mulai menerima respons dengan kecepatan 128KiB / detik.
Kemudian pikiran itu melangkah lebih jauh: dalam URL Anda juga dapat menentukan jumlah bagian individu dalam respons. Yaitu permintaan localhost:8080/128K/3000
akan menghasilkan aliran 3000 bagian dengan kecepatan 128KiB / detik.
Tidak masalah RESTinio memiliki kemampuan untuk menggunakan router kueri yang dibuat di bawah pengaruh ExpressJS . Akibatnya, ada fungsi untuk menggambarkan penangan untuk permintaan HTTP yang masuk:
auto make_router(asio_ns::io_context & ctx) { auto router = std::make_unique<router_t>(); router->http_get("/", [&ctx](auto req, auto) { request_processor(ctx, 100u*1024u, 10000u, std::move(req)); return restinio::request_accepted(); }); router->http_get( R"(/:value(\d+):multiplier([MmKkBb]?))", [&ctx](auto req, auto params) { const auto chunk_size = extract_chunk_size(params); if(0u != chunk_size) { request_processor(ctx, chunk_size, 10000u, std::move(req)); return restinio::request_accepted(); } else return restinio::request_rejected(); }); router->http_get( R"(/:value(\d+):multiplier([MmKkBb]?)/:count(\d+))", [&ctx](auto req, auto params) { const auto chunk_size = extract_chunk_size(params); const auto count = restinio::cast_to<std::size_t>(params["count"]); if(0u != chunk_size && 0u != count) { request_processor(ctx, chunk_size, count, std::move(req)); return restinio::request_accepted(); } else return restinio::request_rejected(); }); return router; }
Di sini penanganan permintaan HTTP GET dibentuk untuk tiga jenis URL:
- dari bentuk
http://localhost/
; - dari bentuk
http://localhost/<speed>[<U>]/
; - dari bentuk
http://localhost/<speed>[<U>]/<count>/
Di mana speed
adalah angka yang menentukan kecepatan, dan U
adalah pengganda opsional yang menunjukkan di mana satuan kecepatan diatur. Jadi 128
atau 128b
berarti kecepatan 128 byte per detik. Dan 128k
adalah 128 kilobyte per detik.
Setiap URL memiliki fungsi lambda sendiri, yang memahami parameter yang diterima, jika semuanya baik-baik saja, ia memanggil fungsi request_processor
ditunjukkan di atas.
Fungsi helper extract_chunk_size
sebagai berikut:
std::size_t extract_chunk_size(const restinio::router::route_params_t & params) { const auto multiplier = [](const auto sv) noexcept -> std::size_t { if(sv.empty() || "B" == sv || "b" == sv) return 1u; else if("K" == sv || "k" == sv) return 1024u; else return 1024u*1024u; }; return restinio::cast_to<std::size_t>(params["value"]) * multiplier(params["multiplier"]); }
Di sini, C ++ lambda digunakan untuk meniru fungsi lokal dari bahasa pemrograman lain.
Fungsi utama
Tetap melihat bagaimana semua ini berjalan di fungsi utama:
using router_t = restinio::router::express_router_t<>; ... int main() { struct traits_t : public restinio::default_single_thread_traits_t { using logger_t = restinio::single_threaded_ostream_logger_t; using request_handler_t = router_t; }; asio_ns::io_context io_ctx; restinio::run( io_ctx, restinio::on_this_thread<traits_t>() .port(8080) .address("localhost") .write_http_response_timelimit(60s) .request_handler(make_router(io_ctx))); return 0; }
Apa yang terjadi di sini:
- Karena saya tidak memerlukan router permintaan biasa (yang tidak bisa melakukan apa-apa sama sekali dan meletakkan semua pekerjaan di pundak programmer), saya mendefinisikan properti baru untuk server HTTP saya. Untuk melakukan ini, saya mengambil properti standar dari server HTTP single-threaded (ketik
restinio::default_single_thread_traits_t
) dan menunjukkan bahwa instance router seperti-ekspres akan digunakan sebagai penangan permintaan. Pada saat yang sama, untuk mengontrol apa yang terjadi di dalam, saya menunjukkan bahwa server HTTP menggunakan logger nyata (secara default, null_logger_t
digunakan yang tidak mencatat apa pun). - Karena saya perlu memiringkan pengatur waktu di dalam notifiers setelah-tulis, saya memerlukan contoh io_context yang bisa saya gunakan. Karena itu, saya membuatnya sendiri. Ini memberi saya kesempatan untuk mengirimkan tautan ke io_context saya di fungsi
make_router
. - Tetap hanya untuk memulai server HTTP dalam versi single-threaded pada io_context yang saya buat sebelumnya. Fungsi
restinio::run
akan mengembalikan kontrol hanya ketika server HTTP menyelesaikan tugasnya.
Kesimpulan
Artikel itu tidak menunjukkan kode lengkap dari server pengujian saya, hanya poin utamanya. Kode lengkap, yang sedikit lebih besar karena typedef tambahan dan fungsi tambahan, agak lebih otentik. Anda bisa melihatnya di sini . Pada saat penulisan, ini adalah 185 baris, termasuk baris kosong dan komentar. Nah, 185 baris ini ditulis dalam beberapa pendekatan dengan durasi total hampir tidak lebih dari satu jam.
Saya menyukai hasil ini dan tugasnya menarik. Dalam istilah praktis, alat bantu yang saya butuhkan dengan cepat diperoleh. Dan dalam hal pengembangan lebih lanjut dari RESTinio, beberapa pemikiran muncul.
Secara umum, jika orang lain belum mencoba RESTinio, maka saya mengundang Anda untuk mencoba. Proyek itu sendiri hidup di GitHub . Anda dapat mengajukan pertanyaan atau mengungkapkan saran Anda di grup Google atau di sini di komentar.