RESTinio adalah proyek yang relatif kecil, yang merupakan server HTTP asinkron yang dibangun ke dalam aplikasi C ++. Fitur karakteristiknya adalah penggunaan C ++ template yang tersebar luas, bisa dikatakan luas. Baik dalam implementasi maupun dalam API publik.
Templat C ++ di RESTinio digunakan dengan sangat aktif sehingga artikel pertama yang berbicara tentang RESTinio di Habr disebut " Templat C ++ tiga lantai dalam implementasi server HTTP asinkron tertanam dengan wajah manusia ."
Templat tiga lantai. Dan ini, secara umum, bukan kiasan.
Dan baru-baru ini, kami sekali lagi memperbarui RESTinio, dan untuk menambahkan fungsionalitas baru ke versi 0.5.1, kami harus membuat "jumlah lantai" dari templat lebih tinggi. Jadi di tempat-tempat template C ++ di RESTinio sudah empat lantai.

Dan jika seseorang bertanya-tanya mengapa kami membutuhkan ini dan bagaimana kami menggunakan templat, lalu tetap bersama kami, akan ada beberapa detail di bawah potongan. Guru C ++ yang lazim tidak mungkin menemukan sesuatu yang baru untuk diri mereka sendiri, tetapi nama panggilan C ++ yang kurang canggih akan dapat melihat bagaimana templat digunakan untuk menyisipkan / menghapus bagian-bagian fungsionalitas. Hampir di alam liar.
Pendengar Status Koneksi
Fitur utama untuk versi 0.5.1 dibuat adalah kemampuan untuk memberi tahu pengguna bahwa status koneksi ke server HTTP telah berubah. Misalnya, klien "jatuh" dan ini membuatnya tidak perlu memproses permintaan dari klien ini yang masih mengantri.
Kami kadang ditanya tentang fitur ini dan sekarang tangan kami mencapai implementasinya. Tapi sejak itu tidak semua orang bertanya tentang fitur ini, ia berpikir bahwa itu harus opsional: jika beberapa pengguna membutuhkannya, maka biarkan ia secara eksplisit memasukkannya, dan semua yang lain tidak membayar apa pun untuk keberadaannya di RESTinio.
Dan karena karakteristik utama dari server HTTP di RESTinio diatur melalui "properties" (ciri-ciri), diputuskan untuk mengaktifkan / menonaktifkan mendengarkan status koneksi melalui properti server.
Bagaimana cara pengguna mengatur pendengar mereka sendiri untuk status koneksi?
Untuk mengatur pendengar Anda untuk status koneksi, pengguna harus melakukan tiga langkah.
Langkah # 1: tentukan kelas Anda sendiri, yang seharusnya memiliki metode state_changed non-statis dari formulir berikut:
void state_changed( const restinio::connection_state::notice_t & notice) noexcept;
Misalnya, bisa jadi seperti:
class my_state_listener { std::mutex lock_; ... public: void state_changed(const restinio::connection_state::notice_t & notice) noexcept { std::lock_guard<std::mutex> l{lock_}; .... } ... };
Langkah # 2: di dalam properti server, Anda perlu mendefinisikan typedef yang disebut connection_state_listener_t
, yang harus merujuk pada nama jenis yang dibuat pada langkah # 1:
struct my_traits : public restinio::default_traits_t { using connection_state_listener_t = my_state_listener; };
Dengan demikian, properti ini harus digunakan ketika memulai server HTTP:
restinio::run(restinio::on_thread_pool<my_traits>(8)...);
Langkah # 3: pengguna harus membuat instance pendengarnya dan meneruskan pointer ini melalui shared_ptr di parameter server:
restinio::run( restinio::on_thread_pool<my_traits>(8) .port(8080) .address("localhost") .request_handler(...) .connection_state_listener(std::make_shared<my_state_listener>(...)) ) );
Jika pengguna tidak melakukan panggilan ke metode connection_state_listener
, pengecualian akan dilemparkan ketika memulai server HTTP: utara tidak dapat bekerja jika pengguna ingin menggunakan negara pendengar, tetapi tidak menentukan pendengar ini.
Dan jika Anda tidak mengatur connection_state_listener_t?
Jika pengguna menetapkan nama connection_state_listener_t
di properti server, maka ia harus memanggil metode connection_state_listener
untuk mengatur parameter server. Tetapi jika pengguna tidak menentukan connection_state_listener_t
?
Dalam hal ini, nama connection_state_listener_t
akan tetap ada di properti server, tetapi nama ini akan menunjuk ke tipe khusus restinio::connection_state::noop_listener_t
.
Bahkan, yang berikut ini terjadi: di RESTinio, ketika mendefinisikan sifat-sifat reguler, nilai connection_state_listener_t
diatur. Sesuatu seperti:
namespace restinio { struct default_traits_t { using time_manager_t = asio_time_manager_t; using logger_t = null_logger_t; ... using connection_state_listener_t = connection_state::noop_listener_t; }; }
Dan ketika pengguna mewarisi dari restinio::default_traits_t
, definisi standar dari connection_state_listener_t
juga diwarisi. Tetapi jika nama baru connection_state_listener_t
didefinisikan dalam kelas penerus:
struct my_traits : public restinio::default_traits_t { using connection_state_listener_t = my_state_listener; ... };
kemudian nama baru menyembunyikan definisi yang diwarisi untuk connection_state_listener_t
. Dan jika tidak ada definisi baru, maka definisi lama tetap terlihat.
Jadi jika pengguna tidak menentukan nilainya sendiri untuk connection_state_listener_t
, maka RESTinio akan menggunakan nilai default, noop_listener_t
, yang diproses oleh RESTinio dengan cara khusus. Sebagai contoh:
- RESTinio sama sekali tidak menyimpan shared_ptr dalam hal ini untuk
connection_state_listener_t
. Dan, karenanya, panggilan ke metode connection_state_listener
dilarang (panggilan seperti itu akan menyebabkan kesalahan waktu kompilasi); - RESTinio tidak melakukan panggilan tambahan terkait perubahan status koneksi.
Dan hanya tentang bagaimana semua ini tercapai dan akan dibahas di bawah ini.
Bagaimana ini diterapkan di RESTinio?
Jadi, dalam kode RESTinio, Anda perlu memeriksa nilai apa yang dimiliki definisi connection_state_listener_t
di properti server dan, tergantung pada nilai ini:
- untuk menyimpan atau tidak menyimpan instance shared_ptr untuk objek bertipe
connecton_state_listener_t
; - mengizinkan atau melarang panggilan ke metode
connection_state_listener
untuk mengatur parameter server HTTP; - memeriksa atau tidak memeriksa keberadaan pointer saat ini ke objek bertipe
connection_state_listener_t
sebelum memulai operasi server HTTP; - melakukan atau tidak melakukan panggilan ke metode
state_changed
ketika kondisi koneksi ke klien berubah.
Juga ditambahkan ke kondisi batas yang masih dikembangkan RESTinio sebagai pustaka untuk C ++ 14, oleh karena itu, Anda tidak dapat menggunakan kemampuan C ++ 17 dalam implementasi (sama jika constexpr).
Semua ini diimplementasikan melalui trik sederhana: kelas templat dan spesialisasi mereka untuk jenis restinio::connection_state::noop_listener_t
. Sebagai contoh, berikut adalah bagaimana penyimpanan shared_ptr dilakukan untuk objek bertipe connection_state_listener_t
dalam parameter server. Bagian satu:
template< typename Listener > struct connection_state_listener_holder_t { ...
Struktur template didefinisikan di sini yang memiliki konten yang bermanfaat atau tidak. Hanya untuk tipe noop_listener_t
, ia tidak memiliki konten yang berguna.
Dan bagian dua:
template<typename Derived, typename Traits> class basic_server_settings_t : public socket_type_dependent_settings_t< Derived, typename Traits::stream_socket_t > , protected connection_state_listener_holder_t< typename Traits::connection_state_listener_t > , protected ip_blocker_holder_t< typename Traits::ip_blocker_t > { ... };
Kelas yang berisi parameter untuk server HTTP diwarisi dari connection_state_listener_holder_t
. Dengan demikian, parameter server menunjukkan shared_ptr untuk objek bertipe connection_state_listener_t
, atau tidak.
Saya harus mengatakan bahwa menyimpan atau tidak menyimpan shared_ptr dalam parameter adalah bunga. Tapi beri pergi ketika mencoba untuk membuat metode yang dimaksudkan untuk bekerja dengan pendengar negara di basic_server_settings_t
tersedia hanya jika connection_state_listener_t
berbeda dari noop_listener_t
.
Idealnya, saya ingin membuat kompiler โtidak melihat merekaโ sama sekali. Tapi saya disiksa untuk menulis kondisi untuk std::enable_if
untuk menyembunyikan metode ini. Oleh karena itu, itu hanya sebatas menambahkan static_asser:
Derived & connection_state_listener( std::shared_ptr< typename Traits::connection_state_listener_t > listener ) & { static_assert( has_actual_connection_state_listener, "connection_state_listener(listener) can't be used " "for the default connection_state::noop_listener_t" ); this->m_connection_state_listener = std::move(listener); return reference_to_derived(); } Derived && connection_state_listener( std::shared_ptr< typename Traits::connection_state_listener_t > listener ) && { return std::move(this->connection_state_listener(std::move(listener))); } const std::shared_ptr< typename Traits::connection_state_listener_t > & connection_state_listener() const noexcept { static_assert( has_actual_connection_state_listener, "connection_state_listener() can't be used " "for the default connection_state::noop_listener_t" ); return this->m_connection_state_listener; } void ensure_valid_connection_state_listener() { this->check_valid_connection_state_listener_pointer(); }
Hanya ada saat lain ketika saya menyesal bahwa dalam C + + jika constexpr tidak sama dengan statis jika dalam D. Dan secara umum di C ++ 14 tidak ada yang serupa :(
Di sini Anda juga dapat melihat ketersediaan metode ensure_valid_connection_state_listener
. Metode ini dipanggil dalam konstruktor http_server_t
untuk memverifikasi bahwa parameter server berisi semua nilai yang diperlukan:
template<typename D> http_server_t( io_context_holder_t io_context, basic_server_settings_t< D, Traits > && settings ) : m_io_context{ io_context.giveaway_context() } , m_cleanup_functor{ settings.giveaway_cleanup_func() } {
Pada saat yang sama, di dalam metode ensure_valid_connection_state_listener
metode ensure_valid_connection_state_listener
diwarisi dari connection_state_listener_holder_t
check_valid_connection_state_listener_pointer
, yang, karena spesialisasi connection_state_listener_holder_t
, baik melakukan pemeriksaan aktual atau tidak melakukan apa-apa.
Trik serupa digunakan baik untuk memanggil state_changed
saat ini jika pengguna ingin menggunakan negara pendengar, atau tidak memanggil apa pun sebaliknya.
Pertama, kita membutuhkan opsi state_listener_holder_t
lain:
namespace connection_settings_details { template< typename Listener > struct state_listener_holder_t { std::shared_ptr< Listener > m_connection_state_listener; template< typename Settings > state_listener_holder_t( const Settings & settings ) : m_connection_state_listener{ settings.connection_state_listener() } {} template< typename Lambda > void call_state_listener( Lambda && lambda ) const noexcept { m_connection_state_listener->state_changed( lambda() ); } }; template<> struct state_listener_holder_t< connection_state::noop_listener_t > { template< typename Settings > state_listener_holder_t( const Settings & ) { } template< typename Lambda > void call_state_listener( Lambda && ) const noexcept { } }; }
Tidak seperti connection_state_listener_holder_t
, yang diperlihatkan sebelumnya dan yang digunakan untuk menyimpan pendengar status koneksi dalam parameter seluruh server (mis., Pada objek tipe basic_server_settings_t
), state_listener_holder_t
ini akan digunakan untuk tujuan yang sama, tetapi tidak dalam parameter seluruh server, tetapi dari parameter terpisah dari seluruh server koneksi:
template < typename Traits > struct connection_settings_t final : public std::enable_shared_from_this< connection_settings_t< Traits > > , public connection_settings_details::state_listener_holder_t< typename Traits::connection_state_listener_t > { using connection_state_listener_holder_t = connection_settings_details::state_listener_holder_t< typename Traits::connection_state_listener_t >; ...
Ada dua fitur di sini.
Pertama, menginisialisasi state_listener_holder_t
. Itu diperlukan atau tidak. Tetapi hanya state_listener_holder_t
tahu tentang itu. Oleh karena itu, connection_settings_t
constructor cukup "menarik" konstruktor state_listener_holder_t
, seperti kata mereka, untuk berjaga-jaga:
template < typename Settings > connection_settings_t( Settings && settings, http_parser_settings parser_settings, timer_manager_handle_t timer_manager ) : connection_state_listener_holder_t{ settings } , m_request_handler{ settings.request_handler() }
Dan konstruktor state_listener_holder_t
melakukan tindakan yang diperlukan atau tidak melakukan apa-apa sama sekali (dalam kasus terakhir, kompiler yang kurang lebih masuk akal tidak akan menghasilkan kode apa pun untuk menginisialisasi state_listener_holder_t
).
Kedua, itu adalah metode state_listner_holder_t::call_state_listener
, yang membuat panggilan state_changed
menjadi pendengar status. Atau tidak, jika tidak ada pendengar negara. call_state_listener
di tempat-tempat RESTinio mendiagnosis perubahan dalam kondisi koneksi. Misalnya, ketika terdeteksi bahwa koneksi telah ditutup:
void close() { m_logger.trace( [&]{ return fmt::format( "[connection:{}] close", connection_id() ); } ); ...
call_state_listener
diteruskan ke call_state_listener
, dari mana objek notice_t
dengan informasi status koneksi dikembalikan. Jika ada pendengar yang sebenarnya, maka lambda ini memang akan dipanggil, dan nilai yang dikembalikan olehnya akan diteruskan ke state_changed
.
Namun, jika tidak ada pendengar, maka call_state_listener
akan kosong dan, karenanya, lambda tidak akan dipanggil. Bahkan, kompiler normal hanya membuang semua panggilan ke call_state_listener
kosong. Dan dalam hal ini, dalam kode yang dihasilkan tidak akan ada yang terkait dengan status koneksi yang sedang diakses oleh pendengar.
Juga pemblokir IP
Dalam RESTinio-0.5.1, selain pendengar status koneksi, hal seperti IP-blocker ditambahkan. Yaitu pengguna dapat menentukan objek yang RESTinio akan "tarik" untuk setiap koneksi masuk yang baru. Jika pemblokir IP mengatakan bahwa Anda dapat bekerja dengan koneksi, maka RESTinio memulai pemeliharaan koneksi baru yang biasa (pembacaan dan parsing permintaan, memanggil penangan permintaan, mengontrol timeout, dll). Tetapi jika IP-blocker melarang bekerja dengan koneksi, maka RESTinio dengan bodohnya menutup koneksi ini dan tidak melakukan apa-apa lagi dengannya.
Seperti pendengar negara, IP-blocker adalah fitur opsional. Untuk menggunakan pemblokir IP, Anda harus mengaktifkannya secara eksplisit. Melalui properti dari server HTTP. Sama seperti dengan pendengar status koneksi. Dan implementasi dukungan pemblokir IP di RESTinio menggunakan teknik yang sama yang telah dijelaskan di atas. Karenanya, kami tidak akan membahas bagaimana IP-blocker digunakan di dalam RESTinio. Sebagai gantinya, pertimbangkan sebuah contoh di mana pemblokir IP dan pendengar status adalah objek yang sama.
Analisis contoh standar ip_blocker
Dalam versi 0.5.1, contoh lain termasuk dalam contoh RESTinio standar: ip_blocker . Contoh ini menunjukkan bagaimana Anda dapat membatasi jumlah koneksi bersamaan ke server dari satu alamat IP.
Ini tidak hanya akan memerlukan pemblokir IP, yang akan mengizinkan atau melarang penerimaan koneksi. Tetapi juga pendengar untuk status koneksi. Seorang pendengar diperlukan untuk melacak saat-saat membuat dan menutup koneksi.
Pada saat yang sama, pemblokir IP dan pendengar akan membutuhkan kumpulan data yang sama. Oleh karena itu, solusi paling sederhana adalah membuat pemblokir dan pendengar IP objek yang sama.
Tidak masalah, kami dapat dengan mudah melakukan ini:
class blocker_t { std::mutex m_lock; using connections_t = std::map< restinio::asio_ns::ip::address, std::vector< restinio::connection_id_t > >; connections_t m_connections; public:
Di sini kita tidak memiliki warisan dari antarmuka apa pun atau penggantian metode virtual bawaan. Satu-satunya persyaratan bagi pendengar adalah keberadaan metode state_changed
. Persyaratan ini terpenuhi.
Demikian juga, dengan satu-satunya persyaratan untuk pemblokir IP: apakah ada metode inspect
dengan tanda tangan yang diperlukan? Ada! Jadi semuanya baik-baik saja.
Maka tetap untuk menentukan properti yang benar untuk server HTTP:
struct my_traits_t : public restinio::default_traits_t { using logger_t = restinio::shared_ostream_logger_t;
Kemudian tinggal membuat instance dari blocker_t
dan meneruskannya ke parameter ke server HTTP:
auto blocker = std::make_shared<blocker_t>(); restinio::run( ioctx, restinio::on_thread_pool<my_traits_t>( std::thread::hardware_concurrency() ) .port( 8080 ) .address( "localhost" ) .connection_state_listener( blocker ) .ip_blocker( blocker ) .max_pipelined_requests( 4 ) .handle_request_timeout( std::chrono::seconds{20} ) .request_handler( [&ioctx](auto req) { return handler( ioctx, std::move(req) ); } ) );
Kesimpulan
Tentang C ++ Templates
Menurut pendapat saya, template C ++ adalah apa yang disebut senjata terlalu besar. Yaitu fitur yang sangat kuat sehingga Anda tanpa sadar harus berpikir tentang bagaimana dan bagaimana penggunaannya dibenarkan. Oleh karena itu, komunitas C ++ modern seolah-olah dibagi menjadi beberapa kubu yang bertikai.
Perwakilan salah satu dari mereka lebih memilih untuk menjauh dari template. Karena templatnya kompleks, mereka menghasilkan panjang lembar pesan kesalahan yang tidak terbaca, yang secara signifikan meningkatkan waktu kompilasi. Belum lagi legenda urban tentang kembung kode dan mengurangi kinerja.
Perwakilan dari kamp lain (seperti saya) percaya bahwa template adalah salah satu aspek paling kuat dari C ++. Bahkan mungkin bahwa templat adalah salah satu dari sedikit keunggulan kompetitif paling serius dari C ++ di dunia modern. Oleh karena itu, menurut saya, masa depan C ++ justru merupakan templat. Dan beberapa ketidaknyamanan saat ini yang terkait dengan penggunaan template yang luas (seperti kompilasi yang panjang dan padat sumber daya atau pesan kesalahan yang tidak informatif) akan dihilangkan dengan satu atau lain cara dari waktu ke waktu.
Oleh karena itu, menurut saya pribadi bahwa pendekatan yang dipilih selama implementasi RESTinio, yaitu meluasnya penggunaan templat dan menentukan karakteristik server HTTP melalui properti, masih terbayar. Berkat ini, kami mendapatkan kustomisasi yang baik untuk kebutuhan spesifik. Dan pada saat yang sama, dalam arti harfiah, kita tidak membayar apa yang tidak kita gunakan.
Namun, di sisi lain, tampaknya pemrograman dalam template C ++ masih terlalu rumit. Anda terutama merasakannya ketika Anda harus memprogram tidak terus-menerus, tetapi ketika beralih di antara berbagai aktivitas. Anda akan terganggu selama beberapa minggu dari pengkodean, maka Anda akan kembali dan mulai dengan jujur โโdan khususnya bodoh jika perlu, menyembunyikan beberapa metode menggunakan SFINAE atau memeriksa apakah ada metode dengan tanda tangan tertentu pada objek.
Jadi bagus ada template di C ++. Akan lebih baik jika mereka dibawa ke keadaan seperti itu sehingga bahkan pemula seperti saya dapat dengan mudah menggunakan C ++ templates tanpa harus mempelajari cppreference dan stackoverflow setiap 10-15 menit.
Tentang keadaan RESTinio saat ini dan fungsionalitas RESTinio di masa mendatang. Dan bukan hanya RESTinio
Saat ini, RESTinio sedang mengembangkan prinsip "ketika ada waktu dan ada keinginan". Misalnya, pada musim gugur 2018 dan pada musim dingin 2019, kami tidak punya banyak waktu untuk pengembangan RESTinio. Mereka menjawab pertanyaan pengguna, membuat perubahan kecil, tetapi untuk sesuatu yang lebih, sumber daya kami tidak cukup.
Tetapi pada akhir musim semi 2019, ada waktu untuk RESTinio, dan kami pertama kali membuat RESTinio 0.5.0 , dan kemudian 0.5.1 . Pada saat yang sama, persediaan Daftar Keinginan kami dan orang lain habis. Yaitu apa yang ingin kami lihat di RESTinio dan apa yang diberitahukan pengguna tentang kami, sudah ada di RESTinio.
Jelas, RESTinio dapat diisi dengan lebih banyak lagi. Tapi apa sebenarnya?
Dan di sini jawabannya sangat sederhana: hanya apa yang diminta untuk masuk ke RESTinio. Karenanya, jika Anda ingin melihat sesuatu yang Anda butuhkan di RESTinio, maka luangkan waktu untuk memberi tahu kami tentang hal itu (misalnya, melalui masalah pada GitHub atau BitBucket , baik melalui grup Google , atau langsung di komentar di sini di Habrรฉ) . Anda tidak akan mengatakan apa pun - Anda tidak akan menerima apa-apa;)
Sebenarnya, situasi yang sama adalah dengan proyek kami yang lain, khususnya dengan SObjectizer . Versi baru mereka akan dirilis setelah menerima Daftar Keinginan dimengerti.
Yah, dan akhirnya, saya ingin menawarkan kepada semua orang yang belum mencoba RESTinio: coba gratis tidak sakit. Tiba-tiba menyukainya. Dan jika Anda tidak menyukainya, maka bagikan apa tepatnya. Ini akan membantu kami untuk membuat RESTinio lebih nyaman dan fungsional.