RESTinio هو مشروع صغير نسبيًا ، وهو خادم HTTP غير متزامن مضمّن في تطبيقات C ++. الميزة المميزة له هي الاستخدام الواسع الانتشار لقوالب C ++. سواء في التنفيذ أو في API العامة.
يتم استخدام قوالب C ++ في RESTinio بنشاط حتى أن المقال الأول الذي تحدث عن RESTinio على Habr كان يسمى " قوالب C ++ ثلاثية الطوابق في تنفيذ خادم HTTP غير متزامن مدمج مع وجه بشري ."
قوالب ثلاثة طوابق. وهذا ، بشكل عام ، لم يكن شخصية الكلام.
ومؤخرا ، قمنا بتحديث RESTinio مرة أخرى ، ولإضافة وظائف جديدة إلى الإصدار 0.5.1 ، كان علينا أن نجعل "عدد الطوابق" من القوالب أعلى. لذلك في الأماكن ، تعد قوالب C ++ في RESTinio مكونة من أربعة طوابق بالفعل.

وإذا كان شخص ما يتساءل عن سبب احتياجنا لهذا وكيف استخدمنا القوالب ، فابق معنا ، ستكون هناك بعض التفاصيل تحت الخفض. من غير المحتمل أن يجد معلمو C ++ Inveterate شيئًا جديدًا لأنفسهم ، لكن ألقاب C ++ الأقل تقدمًا ستتمكن من معرفة كيفية استخدام القوالب لإدراج / إزالة أجزاء من الوظيفة. تقريبا في البرية.
حالة اتصال المستمع
الميزة الرئيسية التي تم إنشاء الإصدار 0.5.1 لها هي القدرة على إبلاغ المستخدم بأن حالة الاتصال بخادم HTTP قد تغيرت. على سبيل المثال ، "سقط" العميل ، مما جعل الأمر غير ضروري لمعالجة الطلبات من هذا العميل التي لا تزال تنتظر في الطابور.
سُئلنا في بعض الأحيان عن هذه الميزة والآن وصلت أيدينا إلى تنفيذها. لكن منذ ذلك الحين لم يسأل الجميع عن هذه الميزة ، كان يُعتقد أنه يجب أن يكون اختياريًا: إذا احتاج بعض المستخدمين إليها ، فدعها تتضمن ذلك بوضوح ، ويجب ألا يدفع الباقي شيئًا عن وجودها في RESTinio.
ونظرًا لأنه يتم تعيين الخصائص الرئيسية لخادم HTTP في RESTinio عبر "الصفات" ، فقد تقرر تمكين / تعطيل الاستماع على حالة الاتصال من خلال خصائص الخادم.
كيف يمكن للمستخدم تعيين المستمع الخاص بهم لحالة الاتصال؟
من أجل تعيين المستمع الخاص بك لحالة الاتصالات ، يجب على المستخدم إجراء ثلاث خطوات.
الخطوة رقم 1: قم بتعريف الفصل الخاص بك ، والذي يجب أن يحتوي على طريقة تغيير الحالة غير الثابتة في النموذج التالي:
void state_changed( const restinio::connection_state::notice_t & notice) noexcept;
على سبيل المثال ، يمكن أن يكون شيء مثل:
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_}; .... } ... };
الخطوة رقم 2: داخل خصائص الخادم ، تحتاج إلى تحديد typedef يسمى connection_state_listener_t
، والذي يجب أن يشير إلى اسم النوع الذي تم إنشاؤه في الخطوة رقم 1:
struct my_traits : public restinio::default_traits_t { using connection_state_listener_t = my_state_listener; };
وفقًا لذلك ، يجب استخدام هذه الخصائص عند بدء تشغيل خادم HTTP:
restinio::run(restinio::on_thread_pool<my_traits>(8)...);
الخطوة رقم 3: يجب على المستخدم إنشاء مثيل لمستمعه وتمرير هذا المؤشر من خلال Shared_ptr في معلمات الخادم:
restinio::run( restinio::on_thread_pool<my_traits>(8) .port(8080) .address("localhost") .request_handler(...) .connection_state_listener(std::make_shared<my_state_listener>(...)) ) );
إذا لم يقم المستخدم بإجراء connection_state_listener
طريقة connection_state_listener
، فسيتم طرح استثناء عند بدء تشغيل خادم HTTP: الشمال لا يمكن أن يعمل إذا أراد المستخدم استخدام مستمع الحالة ولكنه لم يحدد مستمع الحالة هذا.
وإذا لم تقم بتعيين connection_state_listener_t؟
إذا قام المستخدم بتعيين اسم connection_state_listener_t
في خصائص الخادم ، فيجب عليه استدعاء طريقة connection_state_listener
لتعيين معلمات الخادم. ولكن إذا كان المستخدم لا يحدد connection_state_listener_t
؟
في هذه الحالة ، سيظل اسم connection_state_listener_t
موجودًا في خصائص الخادم ، لكن هذا الاسم سوف يشير إلى النوع الخاص restinio::connection_state::noop_listener_t
.
في الواقع ، يحدث ما يلي: في RESTinio ، عند تحديد السمات العادية ، يتم تعيين القيمة connection_state_listener_t
. شيء مثل:
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; }; }
وعندما يرث المستخدم من restinio::default_traits_t
، يتم توارث التعريف القياسي لـ connection_state_listener_t
أيضًا. ولكن إذا connection_state_listener_t
تعريف الاسم الجديد connection_state_listener_t
في فئة الخلف:
struct my_traits : public restinio::default_traits_t { using connection_state_listener_t = my_state_listener; ... };
ثم يخفي الاسم الجديد التعريف الموروث لـ connection_state_listener_t
. وإذا لم يكن هناك تعريف جديد ، فسيظل التعريف القديم مرئيًا.
لذلك إذا لم يحدد المستخدم القيمة الخاصة به لـ connection_state_listener_t
، فسيستخدم RESTinio القيمة الافتراضية ، noop_listener_t
، والتي تتم معالجتها بواسطة RESTinio بطريقة خاصة. على سبيل المثال:
- لا يقوم RESTinio بتخزين shared_ptr على الإطلاق في هذه الحالة من أجل
connection_state_listener_t
. ووفقًا لذلك ، يُحظر إجراء استدعاء لطريقة connection_state_listener
(تؤدي هذه المكالمة إلى حدوث خطأ في وقت الترجمة) ؛ - لا يقوم RESTinio بإجراء أي مكالمات إضافية تتعلق بتغيير حالة الاتصال.
وحول كيفية تحقيق كل هذا وسيتم مناقشته أدناه.
كيف يتم تنفيذ هذا في RESTinio؟
لذا ، في كود RESTinio ، تحتاج إلى التحقق من القيمة التي يحددها connection_state_listener_t
في خصائص الخادم ، واعتمادًا على هذه القيمة:
- لتخزين أو عدم تخزين مثيل Shared_ptr لكائن من النوع
connecton_state_listener_t
؛ - السماح أو حظر المكالمات بطرق
connection_state_listener
لضبط معلمات خادم HTTP ؛ - تحقق أو لا تحقق من وجود مؤشر حالي إلى كائن من النوع
connection_state_listener_t
قبل بدء تشغيل خادم HTTP ؛ - قم بإجراء أو عدم إجراء مكالمات إلى الأسلوب
state_changed
عندما state_changed
حالة الاتصال بالعميل.
تتم إضافته أيضًا إلى الشروط الحدودية التي لا يزال RESTinio يطورها كمكتبة لـ C ++ 14 ، وبالتالي ، لا يمكنك استخدام إمكانيات C ++ 17 في التنفيذ (نفس الشيء إذا كانت constexpr).
كل هذا يتم تنفيذه من خلال الحيل البسيطة: فئات القوالب وتخصصاتها للنوع restinio::connection_state::noop_listener_t
. على سبيل المثال ، فيما يلي كيفية إجراء التخزين Shared_ptr لكائن من نوع connection_state_listener_t
في معلمات الخادم. الجزء الأول:
template< typename Listener > struct connection_state_listener_holder_t { ...
يتم تعريف بنية القالب هنا إما أنه يحتوي على محتوى مفيد أم لا. فقط للنوع noop_listener_t
، ليس له محتوى مفيد.
والجزء الثاني:
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 > { ... };
يتم توريث الفئة التي تحتوي على معلمات خادم HTTP من connection_state_listener_holder_t
. وبالتالي ، فإن معلمات الخادم تُظهر إما Shared_ptr لكائن من نوع connection_state_listener_t
، أو لا.
يجب أن أقول أن تخزين أو عدم تخزين share_ptr في المعلمات هي الزهور. ولكن التوت ذهب عند محاولة جعل الأساليب المخصصة للعمل مع مستمع الحالة في basic_server_settings_t
متاحة فقط إذا كان connection_state_listener_t
مختلفًا عن noop_listener_t
.
من الناحية المثالية ، كنت أرغب في جعل المترجم "لا يراهم" على الإطلاق. لكنني تعرضت للتعذيب لكتابة الشروط الخاصة std::enable_if
أجل إخفاء هذه الأساليب. لذلك ، كان يقتصر ببساطة على إضافة 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(); }
كانت هناك لحظة أخرى عندما شعرت بالأسف لأنه في C ++ إذا كان constexpr ليس ثابتًا كما لو كان في D. وبشكل عام في C ++ 14 لا يوجد شيء مماثل :(
هنا يمكنك أيضًا الاطلاع على مدى توفر طريق ensure_valid_connection_state_listener
. تسمى هذه الطريقة في منشئ http_server_t
للتحقق من أن معلمات الخادم تحتوي على جميع القيم اللازمة:
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() } {
في الوقت نفسه ، داخل ensure_valid_connection_state_listener
طريقة ensure_valid_connection_state_listener
الموروثة من connection_state_listener_holder_t
، والتي ، إما بسبب الاختيار connection_state_listener_holder_t
، إما إجراء فحص فعلي أو عدم القيام بأي شيء.
تم استخدام الحيل المماثلة إما للاتصال state_changed
الحالية التي تم state_changed
إذا أراد المستخدم استخدام مستمع الحالة ، أو لم يتصل بأي شيء آخر.
أولاً ، نحتاج إلى خيار state_listener_holder_t
آخر:
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 { } }; }
على عكس connection_state_listener_holder_t
، الذي تم عرضه مسبقًا والذي تم استخدامه لتخزين مستمع حالة الاتصال في معلمات الخادم بالكامل (على سبيل المثال ، في الكائنات من النوع basic_server_settings_t
) ، سيتم استخدام state_listener_holder_t
هذا لأغراض مماثلة ، ولكن ليس في معلمات الخادم بالكامل ، ولكن الاتصال:
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 >; ...
هناك نوعان من الميزات هنا.
أولاً ، تهيئة state_listener_holder_t
. هو إما حاجة أم لا. لكن state_listener_holder_t
وحده يعلم بذلك. لذلك ، يقوم المنشئ connection_settings_t
ببساطة "بسحب" مُنشئ state_listener_holder_t
، كما يقولون ، فقط في حالة:
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() }
state_listener_holder_t
المُنشئ state_listener_holder_t
إما بتنفيذ الإجراءات اللازمة أو لا يقوم بأي شيء على الإطلاق (في الحالة الأخيرة ، فإن المترجم state_listener_holder_t
أو أقل لن ينشئ أي رمز لتهيئة state_listener_holder_t
).
ثانياً ، إن state_listner_holder_t::call_state_listener
الذي يجعل المكالمة state_changed
إلى مستمع الحالة. أم لا ، إذا لم يكن هناك مستمع دولة. call_state_listener
في الأماكن التي يشخص فيها RESTinio حدوث تغيير في حالة الاتصال. على سبيل المثال ، عندما يتم اكتشاف أن الاتصال قد تم إغلاقه:
void close() { m_logger.trace( [&]{ return fmt::format( "[connection:{}] close", connection_id() ); } ); ...
يتم تمرير call_state_listener
إلى call_state_listener
، حيث يتم notice_t
كائن notice_t
بمعلومات حالة الاتصال. إذا كان هناك مستمع فعلي ، فسيتم استدعاء lambda بالفعل ، وسيتم تمرير القيمة التي يتم إرجاعها بها إلى حالة state_changed
.
ومع ذلك ، إذا لم يكن هناك مستمع ، فسيكون call_state_listener
فارغًا ، وبالتالي لن يتم استدعاء أي لامدا. في الواقع ، المحول البرمجي العادي ببساطة يلقي جميع المكالمات إلى call_state_listener
فارغة. وفي هذه الحالة ، في الكود الذي تم إنشاؤه لن يكون هناك أي شيء على الإطلاق يتعلق بحالة الاتصال التي يمكن الوصول إليها من قبل المستمع.
أيضا مانع IP
في RESTinio-0.5.1 ، بالإضافة إلى مستمع حالة الاتصال ، تمت إضافة شيء مثل مانع IP . أي يمكن للمستخدم تحديد كائن يقوم RESTinio "بسحبه" لكل اتصال وارد جديد. إذا قال مانع IP - أنه يمكنك العمل مع الاتصال ، فإن RESTinio يبدأ في الصيانة المعتادة للاتصال الجديد (يقرأ الطلب ويوزعه ، يستدعي معالج الطلب ، يتحكم في مهل التحكم ، إلخ). ولكن إذا كان مانع IP يحظر العمل مع الاتصال ، فإن RESTinio يغلق هذا الاتصال بغباء ولا يفعل أي شيء أكثر من ذلك.
مثل مستمع الحالة ، يعتبر IP-blocker ميزة اختيارية. لاستخدام مانع IP ، يجب تمكينه بشكل صريح. من خلال خصائص خادم HTTP. تماما مثل مع مستمع حالة الاتصال. وتطبيق دعم مانع IP في RESTinio يستخدم نفس التقنيات التي سبق وصفها أعلاه. لذلك ، لن نتحدث عن كيفية استخدام مانع IP داخل RESTinio. بدلاً من ذلك ، فكر في مثال يكون فيه مانع IP ومستمع الحالة هما نفس الكائن.
تحليل المثال القياسي ip_blocker
في الإصدار 0.5.1 ، يتم تضمين مثال آخر في الأمثلة RESTinio القياسية: ip_blocker . يوضح هذا المثال كيف يمكنك تحديد عدد الاتصالات المتزامنة بالخادم من عنوان IP واحد.
سيتطلب هذا ليس فقط مانع IP ، والذي سيسمح أو يمنع قبول الاتصالات. ولكن أيضا مستمع لحالة الاتصال. هناك حاجة إلى مستمع لتتبع لحظات إنشاء وإغلاق الاتصالات.
في الوقت نفسه ، سيحتاج كل من مانع IP والمستمع إلى نفس مجموعة البيانات. لذلك ، فإن أبسط الحلول هو جعل مانع IP والإصغاء نفس الكائن.
لا مشكلة ، يمكننا القيام بذلك بسهولة:
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:
هنا ليس لدينا أي ميراث من أي واجهات أو تجاوزات للطرق الافتراضية الموروثة. الشرط الوحيد للمستمع هو وجود أسلوب state_changed
. هذا الشرط هو راض.
وبالمثل ، مع الشرط الوحيد لحاصرات IP: هل هناك طريقة inspect
مع التوقيع المطلوب؟ هناك! لذلك كل شيء على ما يرام.
ثم يبقى لتحديد الخصائص الصحيحة لخادم HTTP:
struct my_traits_t : public restinio::default_traits_t { using logger_t = restinio::shared_ostream_logger_t;
بعد ذلك ، يبقى فقط إنشاء مثيل blocker_t
وتمريره في المعلمات إلى خادم 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) ); } ) );
استنتاج
حول قوالب C ++
في رأيي ، قوالب C ++ هي ما يطلق عليه البنادق الكبيرة جدًا. أي ميزة قوية للغاية لدرجة أنه لا بد لك من التفكير مليا في كيفية وكيفية استخدامها. لذلك ، فإن مجتمع C ++ الحديث كما لو كان منقسمًا إلى عدة معسكرات متحاربة.
يفضل ممثلو أحدهم الابتعاد عن القوالب. نظرًا لأن القوالب معقدة ، فإنها تنشئ أطوالًا غير قابلة للقراءة لأوراق رسائل الخطأ غير القابلة للقراءة ، مما يزيد بشكل كبير من وقت الترجمة. ناهيك عن الأساطير الحضرية حول رمز الانتفاخ والحد من الأداء.
يعتقد ممثلو معسكر آخر (مثلي) أن القوالب هي واحدة من أقوى جوانب C ++. من المحتمل أن تكون القوالب واحدة من المزايا التنافسية القليلة الأكثر جدية لـ C ++ في العالم الحديث. لذلك ، في رأيي ، مستقبل C ++ هو بالضبط القوالب. وسيتم التخلص من بعض المضايقات الحالية المرتبطة بالاستخدام الواسع النطاق للقوالب (مثل التحويل البرمجي الطويل والمكثف للموارد أو رسائل الخطأ غير المعلوماتية) بطريقة أو بأخرى بمرور الوقت.
لذلك ، يبدو لي شخصيًا أن النهج الذي تم اختياره أثناء تنفيذ RESTinio ، وهو الاستخدام الواسع النطاق للقوالب وتحديد خصائص خادم HTTP عبر الخصائص ، ما زال يؤتي ثماره. بفضل هذا ، نحصل على تخصيص جيد لاحتياجات محددة. وفي الوقت نفسه ، بالمعنى الحرفي ، نحن لا ندفع مقابل ما لا نستخدمه.
ومع ذلك ، من ناحية أخرى ، يبدو أن البرمجة في قوالب C ++ لا تزال معقدة بشكل غير معقول. تشعر بشكل خاص عندما تضطر إلى البرمجة بشكل مستمر ، ولكن عند التبديل بين الأنشطة المختلفة. سيتم صرف انتباهك لبضعة أسابيع عن الترميز ، ثم ستعود وتبدأ في الغباء بشكل مفتوح وعلى وجه التحديد إذا لزم الأمر ، أو إخفاء بعض الطرق باستخدام SFINAE أو التحقق من وجود طريقة مع توقيع معين على الكائن.
لذلك من الجيد وجود قوالب في C ++. سيكون من الأفضل لو تم إحضارها إلى مثل هذه الحالة ، حتى مبتدئي مثلي يمكنهم بسهولة استخدام قوالب C ++ دون الحاجة إلى دراسة cppreference و stackoverflow كل 10-15 دقيقة.
حول الحالة الحالية لـ RESTinio والوظائف المستقبلية لـ RESTinio. وليس فقط RESTinio
في الوقت الحالي ، تطور RESTinio وفقًا لمبدأ "عندما يكون هناك وقت وهناك قائمة أمنيات". على سبيل المثال ، في خريف عام 2018 وفي شتاء عام 2019 ، لم يكن لدينا الكثير من الوقت لتطوير RESTinio. لقد أجابوا على أسئلة المستخدمين ، وأجروا تغييرات طفيفة ، ولكن لسبب آخر لم تكن مواردنا كافية.
ولكن في أواخر ربيع عام 2019 ، كان هناك وقت لـ RESTinio ، وقمنا أولاً بتصنيع RESTinio 0.5.0 ، ثم 0.5.1 . في الوقت نفسه ، تم استنفاد المعروض من قائمة الامنيات لدينا وغيرها. أي ما أردنا أن نراه في RESTinio وما أخبرنا به المستخدمون ، موجود بالفعل في RESTinio.
من الواضح ، يمكن ملء RESTinio مع أكثر من ذلك بكثير. لكن ماذا بالضبط؟
وهنا الجواب بسيط للغاية: فقط ما يطلب منا الدخول إلى RESTinio. لذلك ، إذا كنت تريد أن ترى شيئًا تحتاجه في RESTinio ، فاخذ الوقت الكافي لإخبارنا به (على سبيل المثال ، من خلال المشكلات على GitHub أو BitBucket ، إما من خلال مجموعة Google ، أو مباشرة في التعليقات هنا على Habré) . لن تقول أي شيء - لن تتلقى أي شيء ؛)
في الواقع ، فإن الوضع نفسه ينطبق على مشاريعنا الأخرى ، خاصة مع SObjectizer . سيتم إصدار إصداراتها الجديدة عند استلام قائمة الأمنيات الواضحة.
حسنًا ، أخيرًا ، أود أن أقدم كل من لم يجرب RESTinio حتى الآن: جربه مجانا لا تؤذي. مثل ذلك فجأة. وإذا كنت لا تحب ذلك ، فقم بمشاركة ما هو بالضبط. سيساعدنا ذلك في جعل RESTinio أكثر ملاءمة ووظيفية.