الروبيان: مقياس ومشاركة صور HTTP في Modern C ++ مع ImageMagic ++ و SObjectizer و RESTinio



مقدمة


يعمل فريقنا الصغير على تطوير أداتين OpenSource لمطوري C ++ - إطار عمل SObjectizer الفاعل وخادم RESTinio المضمن لـ HTTP. ومع ذلك ، نواجه بانتظام بضعة أسئلة غير تافهة:

  • ما هي الميزات التي يجب إضافتها إلى المكتبة ، وأيها يجب تركها "مفرطة"؟
  • كيف توضح بوضوح الطرق "الصحيحة أيديولوجيا" لاستخدام المكتبة؟

من الجيد أن تظهر الإجابات على مثل هذه الأسئلة أثناء استخدام تطوراتنا في المشاريع الحقيقية ، عندما يأتي إلينا المطورين بشكاواهم أو قائمة أمنياتهم. نظرًا لإرضاء رغبات المستخدمين ، فإننا نملأ أدواتنا بوظائف تمليها الحياة نفسها ، وليس "يتم سحبها من الإصبع".

لكن المعلومات تصل بنا بعيدًا عن جميع المشاكل والصعوبات التي يواجهها المستخدمون. ولا يمكننا دائمًا استخدام المعلومات الواردة ، وخاصة أمثلة الشفرات ، في موادنا العامة.

لذلك ، في بعض الأحيان نفكر في مشاكل صغيرة لأنفسنا ، حل نضطر إلى تحويله من مطوري الأدوات إلى مستخدمين. هذا يسمح لنا بإلقاء نظرة على أدواتنا الخاصة بأعين مختلفة وفهم أنفسنا لما هو جيد وما هو غير جيد وما هو مفقود وما هو أكثر من اللازم.

اليوم نريد أن نقول فقط عن إحدى هذه المهام "الصغيرة" ، التي اجتمع فيها SObjectizer و RESTinio بشكل طبيعي.

تحجيم وتوزيع الصور. لماذا هذا بالضبط؟


كمهمة تجريبية صغيرة لأنفسنا ، اخترنا خادم HTTP الذي يوزع الصور التي تم قياسها عند الطلب. يمكنك وضع الصور في بعض الدليل ، وبدء خادم HTTP ، وتقديم طلب لها من النموذج:

curl "http://localhost:8080/my_picture.jpg?op=resize&max=1920" 

وفي المقابل تحصل على صورة بحجم 1920 بكسل على الجانب الطويل.

وقع الاختيار على هذه المهمة لأنه يوضح تمامًا السيناريوهات التي بدأنا من أجلها تطوير RESTinio في وقت واحد: هناك رمز طويل الأمد ومُصحح في C أو C ++ تحتاج إلى إرفاق إدخال HTTP والبدء في الاستجابة للطلبات الواردة. وفي الوقت نفسه ، وهو أمر مهم ، يمكن أن تستغرق معالجة الطلب للطلب وقتًا طويلاً ، وبالتالي من غير المربح سحب رمز التطبيق مباشرة في سياق IO. يجب أن يكون خادم HTTP غير متزامن: قم بقبول طلب HTTP وتحليله ، وإعطاء الطلب الذي تم تحليله في مكان ما لمزيد من معالجة التطبيق ، ومتابعة خدمة طلب HTTP التالي ، والعودة إلى إعادة الاستجابة إلى طلب HTTP عندما يتم إعداد هذه الاستجابة من قبل شخص ما.

هذا بالضبط ما يحدث عند معالجة طلبات تغيير حجم الصور. خادم HTTP قادر على القيام بعمله المباشر (أي قراءة البيانات ، تحليل طلب HTTP) في جزء من جزء من الثانية. لكن قياس الصورة يمكن أن يستغرق عشرات أو مئات أو حتى آلاف المللي ثانية.

وحيث أن الأمر قد يستغرق الكثير من الوقت لتغيير حجم صورة واحدة ، فأنت بحاجة إلى التأكد من أن خادم HTTP يمكن أن يستمر في العمل أثناء تغيير حجم الصورة. للقيام بذلك ، نحتاج إلى نشر عمل خادم HTTP وقياس الصور إلى سياقات عمل مختلفة. في الحالة البسيطة ، ستكون هذه خيوط عمل مختلفة. حسنًا ، نظرًا لأننا نعيش في معالجات متعددة النواة ، فسيكون لدينا العديد من خيوط العمل. بعضهم سيخدم طلبات HTTP ، والبعض الآخر يعمل مع الصور.

اتضح أنه لتوزيع الصور القابلة للتطوير عبر HTTP ، نحتاج إلى إعادة استخدام رمز C / C ++ المكتوب والمطول (في هذه الحالة ImageMagic ++) ، وخدمة طلبات HTTP بشكل غير متزامن ، وتنفيذ معالجة الطلبات للطلبات في العديد من عمليات سير العمل. مهمة ممتازة لـ RESTinio و SObjectizer ، كما بدا لنا.

وقررنا تسمية روبيان مشروعنا التجريبي.

الروبيان كما هو


ماذا يفعل الروبيان؟


يعمل الروبيان كتطبيق وحدة تحكم ، ويفتح ويستمع إلى المنفذ المحدد ، ويستقبل ويعالج طلبات HTTP GET من النموذج:

 /<image>.<ext> /<image>.<ext>?op=resize&<side>=<value> 

أين:

  • image هو اسم ملف الصورة المراد قياسه. على سبيل المثال ، my_picture or DSCF0069؛
  • ext هو أحد الملحقات التي يدعمها الروبيان (jpg أو jpeg أو png أو gif) ؛
  • الجانب هو مؤشر للجانب الذي يتم تعيين الحجم له. يمكن أن يكون لها قيمة عرض ، في هذه الحالة يتم تحجيم الصورة بحيث يكون العرض الناتج مساوياً للقيمة المحددة ، يتم تحديد ارتفاع الصورة تلقائيًا مع الحفاظ على نسبة العرض إلى الارتفاع. أو قيمة الارتفاع ، في هذه الحالة ، يحدث التحجيم في الارتفاع. إما الحد الأقصى ، في هذه الحالة يكون الجانب الطويل محدودًا ، ويحدد الجمبري نفسه ما إذا كان الجانب الطويل هو الطول أو العرض ؛
  • value هي الحجم الذي يحدث عنده القياس.

إذا تم تحديد اسم الملف فقط في عنوان URL ، بدون عملية تغيير الحجم ، فإن الجمبري ببساطة يعيد الصورة الأصلية في الاستجابة. إذا تم تحديد عملية تغيير الحجم ، فإن الجمبري يغير حجم الصورة المطلوبة ويعطي النسخة المقاسة.

في الوقت نفسه ، يحتفظ الروبيان في الذاكرة بذاكرة تخزين مؤقت للصور التي تم تحجيمها. إذا تم طلب صورة بشكل متكرر بنفس معلمات تغيير الحجم الموجودة بالفعل في ذاكرة التخزين المؤقت ، فسيتم إرجاع القيمة من ذاكرة التخزين المؤقت. إذا لم تكن هناك صورة في ذاكرة التخزين المؤقت ، فستتم قراءة الصورة من القرص ، ويتم قياسها وتخزينها في ذاكرة التخزين المؤقت وإعادتها استجابة.

يتم مسح ذاكرة التخزين المؤقت بشكل دوري. الصور التي عاشت في ذاكرة التخزين المؤقت لأكثر من ساعة منذ دفعها لآخر وصول إليها. أيضا ، يتم التخلص من الصور القديمة من ذاكرة التخزين المؤقت إذا تجاوزت ذاكرة التخزين المؤقت حجمها الأقصى (في مشروع تجريبي 100 ميغا بايت).

لقد أعددنا صفحة من خلال الذهاب إلى أي شخص يمكن تجربة الروبيان:



في هذه الصفحة يمكنك ضبط حجم الصورة والنقر على "تغيير الحجم". سيتم تقديم طلبين إلى خادم الروبيان بنفس المعلمات. على الأرجح ، سيكون الطلب الأول فريدًا (على سبيل المثال ، لن تكون هناك ذاكرة تخزين مؤقت بمعلمات تغيير الحجم في ذاكرة التخزين المؤقت حتى الآن) ، لذا فإن الطلب الأول سيستغرق بعض الوقت لتغيير حجم الصورة بالفعل. والطلب الثاني ، على الأرجح ، سيجد الصورة التي تم قياسها بالفعل في ذاكرة التخزين المؤقت وسيعطيها على الفور.

من الممكن الحكم على ما إذا كانت الصورة قد تم الحصول عليها من ذاكرة التخزين المؤقت أو ما إذا تم قياسها بالفعل بواسطة النص الموجود أسفل الصورة. على سبيل المثال ، يشير النص "محول (114.0 مللي ثانية)" إلى أن الصورة تم تحجيمها واستغرقت عملية التكبير 114 مللي ثانية.

كيف يفعل الروبيان ذلك؟


الجمبري هو تطبيق متعدد الخيوط يقوم بتشغيل ثلاث مجموعات من خيوط العمل:

  1. مجموعة خيوط العمل التي تقوم بتشغيل وحدة خدمة HTTP. في هذا التجمع ، يتم عرض الاتصالات الجديدة ، ويتم تلقي الطلبات الواردة وتحليلها ، ويتم إنشاء الردود وإرسالها. يتم تنفيذ خادم HTTP من خلال مكتبة RESTinio.
  2. مؤشر ترابط عمل منفصل يعمل عليه عامل SObjectizer transform_manager. يعالج هذا الوكيل الطلبات الواردة من خادم HTTP ويحتفظ بذاكرة تخزين مؤقت للصور التي تم قياسها.
  3. تجمع مؤشر الترابط الذي يعمل عليه وكلاء SObjectizer. يقومون بإجراء التحجيم الفعلي للصور باستخدام ImageMagic ++.

اتضح مخطط العمل التالي:



يقبل خادم HTTP الطلب الوارد ويحللها ويتحقق من صحتها. إذا كان هذا الطلب لا يتطلب عملية تغيير الحجم ، فإن خادم HTTP نفسه يقوم بمعالجة الطلب من خلال عملية إرسال الملف. إذا كان الطلب يتطلب عملية تغيير الحجم ، يتم إرسال الطلب بشكل غير متزامن إلى وكيل transform_manager.

يتلقى وكيل transform_manager الطلبات من خادم HTTP ، ويتحقق من وجود صور تم قياسها بالفعل في ذاكرة التخزين المؤقت. إذا كانت هناك صورة في ذاكرة التخزين المؤقت ، فسيقوم convert_manager على الفور بإنشاء استجابة لخادم HTTP. في حالة عدم وجود صورة ، يرسل convert_manager طلبًا لتغيير حجم الصورة إلى أحد عوامل المحولات. عندما تأتي نتيجة القياس من المحول ، يتم تخزين النتيجة في ذاكرة التخزين المؤقت ويتم إنشاء إجابة لخادم HTTP.

يتلقى عامل المحول الطلبات من transform_manager ، ويعالجها ، ويعيد نتيجة التحويل إلى وكيل convert_manager.

ماذا يحتوي الروبيان تحت غطاء المحرك؟


يمكن العثور على الكود المصدري لأصغر إصدار من الجمبري الموصوف في هذا المقال في هذا المستودع: الجمبري التجريبي على BitBucket أو على GitHub .

هناك الكثير من الكود ، على الرغم من أنه في معظم الأحيان ، في هذا الإصدار من الجمبري ، الكود تافه للغاية. ومع ذلك ، فمن المنطقي التركيز على بعض جوانب التنفيذ.

باستخدام C ++ 17 وأحدث إصدارات المترجم


في تنفيذ الجمبري ، قررنا استخدام C ++ 17 وأحدث إصدارات المترجمات ، ولا سيما دول مجلس التعاون الخليجي 7.3 و 8.1. المشروع بحثي بشكل كبير. لذلك ، التعارف العملي لـ C ++ 17 في إطار مثل هذا المشروع طبيعي ومسموح به. بينما في التطورات الأكثر دنيوية التي تركز على التطبيقات الصناعية العملية هنا والآن ، نحن مضطرون للنظر إلى الوراء في المترجمات القديمة نوعًا ما وربما استخدام C ++ 14 ، أو حتى مجرد مجموعة فرعية من C ++ 11.

يجب أن أقول أن C ++ 17 يعطي انطباعًا جيدًا. يبدو أننا لم نستخدم الكثير من الابتكارات من المعيار السابع عشر في كود الجمبري ، ولكن كان لها تأثير إيجابي: السمة [[nodiscard]] ، std :: Optional / std :: variant / std :: filesystem مباشرة " خارج الصندوق "، وليس من التبعيات الخارجية ، ملزم منظم ، إذا كونستكسبر ، القدرة على تجميع الزائر لامداس ل std :: visit ... بشكل فردي ، هذه كلها تافهة ، ولكن معًا ينتجون تأثيرًا تراكميًا قويًا.

لذا فإن أول نتيجة مفيدة حصلنا عليها أثناء تطوير الجمبري: يستحق C ++ 17 التحول إليه.

خادم HTTP باستخدام أدوات RESTinio


ربما يكون الجزء الأسهل من الروبيان هو خادم HTTP ومعالج طلب HTTP GET ( http_server.hpp و http_server.cpp ).

استقبال وإرسال الطلبات الواردة


بشكل أساسي ، كل المنطق الأساسي لخادم HTTP للروبيان يتركز في هذه الوظيفة:

 void add_transform_op_handler( const app_params_t & app_params, http_req_router_t & router, so_5::mbox_t req_handler_mbox ) { router.http_get( R"(/:path(.*)\.:ext(.{3,4}))", restinio::path2regex::options_t{}.strict( true ), [req_handler_mbox, &app_params]( auto req, auto params ) { if( has_illegal_path_components( req->header().path() ) ) { return do_400_response( std::move( req ) ); } const auto opt_image_format = image_format_from_extension( params[ "ext" ] ); if( !opt_image_format ) { return do_400_response( std::move( req ) ); } if( req->header().query().empty() ) { return serve_as_regular_file( app_params.m_storage.m_root_dir, std::move( req ), *opt_image_format ); } const auto qp = restinio::parse_query( req->header().query() ); if( "resize" != restinio::value_or( qp, "op"sv, ""sv ) ) { return do_400_response( std::move( req ) ); } handle_resize_op_request( req_handler_mbox, *opt_image_format, qp, std::move( req ) ); return restinio::request_accepted(); } ); } 

تعد هذه الوظيفة معالج طلب HTTP GET باستخدام جهاز التوجيه RESTinio ExpressJS . عندما يتلقى خادم HTTP طلب GET ، يقع عنوان URL الخاص به تحت التعبير العادي المحدد ، يتم استدعاء وظيفة lambda المحددة.

تقوم وظيفة لامدا هذه بإجراء بعض الفحوصات البسيطة على صحة الطلب ، ولكن بشكل عام ، يعود عملها إلى خيار بسيط: إذا لم يتم تعيين تغيير الحجم ، سيتم إرجاع الصورة المطلوبة في شكلها الأصلي باستخدام ملف نظام فعال. إذا تم تعيين وضع تغيير الحجم ، فسيتم إنشاء رسالة وإرسالها إلى وكيل transform_manager:

 void handle_resize_op_request( const so_5::mbox_t & req_handler_mbox, image_format_t image_format, const restinio::query_string_params_t & qp, restinio::request_handle_t req ) { try_to_handle_request( [&]{ auto op_params = transform::resize_params_t::make( restinio::opt_value< std::uint32_t >( qp, "width" ), restinio::opt_value< std::uint32_t >( qp, "height" ), restinio::opt_value< std::uint32_t >( qp, "max" ) ); transform::resize_params_constraints_t{}.check( op_params ); std::string image_path{ req->header().path() }; so_5::send< so_5::mutable_msg<a_transform_manager_t::resize_request_t>>( req_handler_mbox, std::move(req), std::move(image_path), image_format, op_params ); }, req ); } 

اتضح أن خادم HTTP ، بعد قبول طلب تغيير الحجم ، يعطيه إلى وكيل transform_manager عبر رسالة غير متزامنة ، ويستمر في خدمة الطلبات الأخرى.

مشاركة الملفات مع سيند فايل


إذا اكتشف خادم HTTP طلبًا للصورة الأصلية ، بدون عملية تغيير الحجم ، يرسل الخادم هذه الصورة على الفور من خلال عملية إرسال الملف. الرمز الرئيسي المرتبط بهذا هو كما يلي (يمكن العثور على الرمز الكامل لهذه الوظيفة في المستودع ):

 [[nodiscard]] restinio::request_handling_status_t serve_as_regular_file( const std::string & root_dir, restinio::request_handle_t req, image_format_t image_format ) { const auto full_path = make_full_path( root_dir, req->header().path() ); try { auto sf = restinio::sendfile( full_path ); ... return set_common_header_fields_for_image_resp( file_stat.st_mtim.tv_sec, resp ) .append_header( restinio::http_field::content_type, image_content_type_from_img_format( image_format ) ) .append_header( http_header::shrimp_image_src, image_src_to_str( http_header::image_src_t::sendfile ) ) .set_body( std::move( sf ) ) .done(); } catch(...) {} return do_404_response( std::move( req ) ); } 

النقطة الرئيسية هنا هي استدعاء restinio :: sendfile () ، ثم تمرير القيمة التي أرجعتها هذه الدالة إلى set_body ().

تنشئ الدالة restinio :: sendfile () عملية تحميل ملف باستخدام واجهة برمجة تطبيقات النظام. عندما يتم تمرير هذه العملية إلى set_body () ، يفهم RESTinio أن محتويات الملف المحدد في restinio :: sendfile () سيتم استخدامها لنص استجابة HTTP. ثم يستخدم واجهة برمجة التطبيقات للنظام لكتابة محتويات هذا الملف إلى مقبس TCP.

تنفيذ ذاكرة التخزين المؤقت للصور


يخزن وكيل transform_manager ذاكرة التخزين المؤقت للصور المحولة ، حيث يتم وضع الصور بعد القياس. ذاكرة التخزين المؤقت هذه عبارة عن حاوية بسيطة ذاتية الصنع توفر الوصول إلى محتوياتها بطريقتين:

  1. من خلال البحث عن عنصر بمفتاح (مشابه لكيفية حدوث ذلك في الحاويات القياسية std :: map و std :: unordered_map).
  2. عن طريق الوصول إلى أقدم عنصر ذاكرة التخزين المؤقت.

يتم استخدام طريقة الوصول الأولى عندما نحتاج إلى التحقق من توفر الصورة في ذاكرة التخزين المؤقت. والثاني هو عندما نحذف أقدم الصور من ذاكرة التخزين المؤقت.

لم نبدأ في البحث عن شيء جاهز لهذه الأغراض على الإنترنت. ربما يكون Boost.MultiIndex مناسبًا تمامًا هنا. لكنني لم أكن أرغب في سحب Boost لمجرد MultiIndex ، لذلك قمنا بتنفيذنا التافه حرفياً على ركبتي. يبدو أنه يعمل ؛)

قائمة انتظار الطلبات المعلقة في transform_manager


يبدو أن عامل transform_manager ، على الرغم من حجمه اللائق إلى حد ما ( ملف hpp من حوالي 250 خطًا وملف cpp من حوالي 270 سطرًا) ، في أبسط تنفيذ لدينا للروبيان ، تافهًا إلى حد ما ، في رأينا.

أحد الأشياء التي تساهم بشكل كبير في تعقيد ومقدار رمز الوكيل هو عدم وجود ذاكرة تخزين مؤقت للصور المحولة في convert_manager فحسب ، بل أيضًا قوائم انتظار الطلبات المعلقة.

لدينا عدد محدود من عوامل المحولات (من حيث المبدأ ، يجب أن يتوافق عددهم تقريبًا مع عدد نوى المعالجة المتاحة). إذا كان هناك المزيد من الطلبات في وقت واحد أكثر من المحولات المجانية ، فيمكننا إما الاستجابة على الفور بشكل سلبي للطلب أو وضع الطلب في قائمة الانتظار. ثم أخذه من قائمة الانتظار عندما يظهر محول مجاني.

في الروبيان ، نستخدم قائمة انتظار الطلبات التي يتم تحديدها على النحو التالي:

 struct pending_request_t { transform::resize_request_key_t m_key; sobj_shptr_t<resize_request_t> m_cmd; std::chrono::steady_clock::time_point m_stored_at; pending_request_t( transform::resize_request_key_t key, sobj_shptr_t<resize_request_t> cmd, std::chrono::steady_clock::time_point stored_at ) : m_key{ std::move(key) } , m_cmd{ std::move(cmd) } , m_stored_at{ stored_at } {} }; using pending_request_queue_t = std::queue<pending_request_t>; pending_request_queue_t m_pending_requests; static constexpr std::size_t max_pending_requests{ 64u }; 

عند استلام الطلب ، نضعه في قائمة الانتظار مع تحديد وقت تلقي الطلب. ثم نتحقق بشكل دوري لمعرفة ما إذا انتهت مهلة هذا الطلب. في الواقع ، قد يحدث ، من حيث المبدأ ، وصول مجموعة من الطلبات "الثقيلة" في وقت سابق ، والتي تستغرق معالجتها وقتًا طويلاً. من الخطأ الانتظار إلى ما لا نهاية حتى يظهر محول مجاني ، فمن الأفضل إرسال رد سلبي إلى العميل بعد مرور بعض الوقت ، مما يعني أن الخدمة محملة بشكل زائد.

هناك أيضًا حد حجم لقائمة انتظار الطلبات المعلقة. إذا وصلت قائمة الانتظار بالفعل إلى أقصى حجم لها ، فإننا نرفض على الفور معالجة الطلب ونخبر العميل بأننا مثقلون.

هناك نقطة مهمة تتعلق بقائمة الطلبات المعلقة ، والتي سنركز عليها في ختام المقالة.

اكتب sobj_shptr_t وأعد استخدام مثيلات الرسالة


في تحديد نوع قائمة انتظار الطلبات ، وكذلك في توقيعات بعض طرق convert_manager ، يمكنك رؤية استخدام نوع sobj_shptr_t. من المنطقي الخوض في مزيد من التفاصيل حول نوعه ولماذا يتم استخدامه.

خلاصة القول هي أن convert_manager يتلقى طلبًا من خادم HTTP كرسالة resize_request_t:

 struct resize_request_t final : public so_5::message_t { restinio::request_handle_t m_http_req; std::string m_image; image_format_t m_image_format; transform::resize_params_t m_params; resize_request_t( restinio::request_handle_t http_req, std::string image, image_format_t image_format, transform::resize_params_t params ) : m_http_req{ std::move(http_req) } , m_image{ std::move(image) } , m_image_format{ image_format } , m_params{ params } {} }; 

وعلينا أن نفعل شيئًا لتخزين هذه المعلومات في قائمة انتظار الطلبات. على سبيل المثال ، يمكنك إنشاء نسخة جديدة من resize_request_t ونقل القيم من الرسالة المستلمة إليها.

ويمكنك أن تتذكر أن الرسالة نفسها في SObjectizer عبارة عن كائن تم إنشاؤه ديناميكيًا. وليس كائنًا بسيطًا ، ولكن مع عداد ارتباط بالداخل. وأنه في SObjectizer هناك نوع خاص من المؤشر الذكي لمثل هذه الأشياء - intrusive_ptr_t.

على سبيل المثال لا يمكننا عمل نسخة من resize_request_t لقائمة انتظار الطلبات ، ولكن يمكننا ببساطة وضع مؤشر ذكي في قائمة الانتظار هذه إلى مثيل موجود من resize_request_t. ماذا نفعل. ولكي لا نكتب في كل مكان الاسم الغريب إلى حد ما so_5 :: intrusive_ptr_t ، نقوم بإدخال الاسم المستعار الخاص بنا:

 template<typename T> using sobj_shptr_t = so_5::intrusive_ptr_t<T>; 

استجابات غير متزامنة للعملاء


قلنا أنه تتم معالجة طلبات HTTP بشكل غير متزامن. وأظهرنا أعلاه كيف يرسل خادم HTTP استفسارًا إلى وكيل transform_manager برسالة غير متزامنة. ولكن ماذا يحدث للاستجابات لطلبات HTTP؟

يتم تقديم الردود أيضًا بشكل غير متزامن. على سبيل المثال ، في كود convert_manager ، يمكنك رؤية ما يلي:

 void a_transform_manager_t::on_failed_resize( failed_resize_t & /*result*/, sobj_shptr_t<resize_request_t> cmd ) { do_404_response( std::move(cmd->m_http_req) ); } 

يولد هذا الرمز استجابة سلبية لطلب HTTP في الحالة التي يتعذر فيها تغيير حجم الصورة لسبب ما. يتم إنشاء الاستجابة في وظيفة المساعد do_404_response ، والتي يمكن تمثيل رمزها على النحو التالي:

 auto do_404_response( restinio::request_handle_t req ) { auto resp = req->create_response( 404, "Not Found" ); resp.append_header( restinio::http_field_t::server, "Shrimp draft server" ); resp.append_header_date_field(); if( req->header().should_keep_alive() ) resp.connection_keep_alive(); else resp.connection_close(); return resp.done(); } 

النقطة الرئيسية الأولى مع do_404_response () هي أن هذه الوظيفة تُدعى في سياق العمل الخاص بوكيل transform_manager وليس في سياق العمل لخادم HTTP.

النقطة الرئيسية الثانية هي استدعاء الأسلوب () المنجز على كائن الاستجابة المشكل بالكامل. كل السحر غير المتزامن مع استجابة HTTP يحدث هنا. تأخذ الطريقة () التي تم إجراؤها جميع المعلومات التي تم إعدادها في الاستجابة وترسلها بشكل غير متزامن إلى خادم HTTP. على سبيل المثال سيحدث الإرجاع من do_404_response () فورًا بعد وضع خادم HTTP في قائمة انتظار محتويات كائن الاستجابة.

سيكتشف خادم HTTP في سياق عمله وجود استجابة HTTP جديدة وسيبدأ في تنفيذ الإجراءات اللازمة لإرسال الاستجابة إلى العميل المناسب.

اكتب datasizable_blob_t


نقطة صغيرة أخرى من المنطقي توضيحها ، لأنها ربما تكون غير مفهومة دون فهم تعقيدات RESTinio. نتحدث عن وجود نوع غريب من datasizeable_blob_t ، للوهلة الأولى ، محدد على النحو التالي:

 struct datasizable_blob_t : public std::enable_shared_from_this< datasizable_blob_t > { const void * data() const noexcept { return m_blob.data(); } std::size_t size() const noexcept { return m_blob.length(); } Magick::Blob m_blob; //! Value for `Last-Modified` http header field. const std::time_t m_last_modified_at{ std::time( nullptr ) }; }; 

لتوضيح سبب الحاجة إلى هذا النوع ، تحتاج إلى إظهار كيفية تكوين استجابة HTTP مع صورة محولة:

 void serve_transformed_image( restinio::request_handle_t req, datasizable_blob_shared_ptr_t blob, image_format_t img_format, http_header::image_src_t image_src, header_fields_list_t header_fields ) { auto resp = req->create_response(); set_common_header_fields_for_image_resp( blob->m_last_modified_at, resp ) .append_header( restinio::http_field::content_type, image_content_type_from_img_format( img_format ) ) .append_header( http_header::shrimp_image_src, image_src_to_str( image_src ) ) .set_body( std::move( blob ) ); for( auto & hf : header_fields ) { resp.append_header( std::move( hf.m_name ), std::move( hf.m_value ) ); } resp.done(); } 

ننتبه إلى استدعاء set_body (): يتم إرسال مؤشر ذكي لمثيل datasizable_blob_t مباشرةً. لماذا؟

والحقيقة هي أن RESTinio يدعم العديد من الخيارات لتشكيل نص استجابة HTTP . أبسطها هو تمرير مثيل من نوع std :: string إلى set_body () وسيحفظ RESTinio قيمة هذه السلسلة داخل كائن resp.

ولكن هناك أوقات يجب فيها إعادة استخدام قيمة set_body () في عدة إجابات في وقت واحد. على سبيل المثال ، يحدث هذا في الجمبري عندما يتلقى الجمبري عدة طلبات متطابقة لتحويل نفس الصورة.في هذه الحالة ، من غير المربح نسخ نفس القيمة في كل إجابة. لذلك ، في RESTinio يوجد متغير set_body () من النموذج:
 template<typename T> auto set_body(std::shared_ptr<T> body); 

ولكن في هذه الحالة ، يتم فرض قيود مهمة على النوع T: يجب أن تحتوي على البيانات العامة () وطرق الحجم () ، وهي ضرورية حتى يتمكن RESTinio من الوصول إلى محتويات الاستجابة.

يتم تخزين الصورة المقاسة في الروبيان ككائن Magick :: Blob. توجد طريقة بيانات في نوع Magic :: Blob ، ولكن لا توجد طريقة size () ، ولكن هناك طريقة length (). لذلك ، كنا بحاجة إلى فئة الغلاف datasizable_blob_t ، والتي توفر لـ RESTinio الواجهة اللازمة للوصول إلى قيمة Magick :: Blob.

الرسائل الدورية في transform_manager


يحتاج وكيل transform_manager للقيام بعدة أشياء من وقت لآخر:

  • اسحب الصور الموجودة في ذاكرة التخزين المؤقت لفترة طويلة جدًا من ذاكرة التخزين المؤقت.
  • التحكم في الوقت الذي يقضيه في قائمة الانتظار انتظار المحولات المجانية.

يقوم وكيل transform_manager بتنفيذ هذه الإجراءات من خلال رسائل دورية. يبدو على النحو التالي.

أولاً ، يتم تحديد أنواع الإشارات التي سيتم استخدامها كرسائل دورية:

 struct clear_cache_t final : public so_5::signal_t {}; struct check_pending_requests_t final : public so_5::signal_t {}; 

ثم يتم الاشتراك في الوكيل ، بما في ذلك هذه الإشارات:

 void a_transform_manager_t::so_define_agent() { so_subscribe_self() .event( &a_transform_manager_t::on_resize_request ) .event( &a_transform_manager_t::on_resize_result ) .event( &a_transform_manager_t::on_clear_cache ) .event( &a_transform_manager_t::on_check_pending_requests ); } void a_transform_manager_t::on_clear_cache( mhood_t<clear_cache_t> ) {...} void a_transform_manager_t::on_check_pending_requests( mhood_t<check_pending_requests_t> ) {...} 

بفضل الاشتراك ، سوف يستدعي SObjectizer المعالج المطلوب عندما يتلقى الوكيل الإشارة المقابلة.

ويبقى فقط لتشغيل الرسائل الدورية عندما يبدأ الوكيل:

 void a_transform_manager_t::so_evt_start() { m_clear_cache_timer = so_5::send_periodic<clear_cache_t>( *this, clear_cache_period, clear_cache_period ); m_check_pending_timer = so_5::send_periodic<check_pending_requests_t>( *this, check_pending_period, check_pending_period ); } 

النقطة الأساسية هنا هي حفظ timer_id ، والتي يتم إرجاعها بواسطة دوال send_periodic (). بعد كل شيء ، ستأتي الإشارة الدورية فقط طالما أن معرّف المؤقت حي. لذلك ، إذا لم يتم حفظ القيمة المرجعة لـ send_periodic () ، فسيتم إلغاء إرسال رسالة دورية على الفور. لذلك ، تتميز الفئة a_transform_manager_t بالسمات التالية:

 so_5::timer_id_t m_clear_cache_timer; so_5::timer_id_t m_check_pending_timer; 

نهاية الجزء الأول


قدمنا ​​اليوم القارئ إلى أبسط وأبسط طريقة لتنفيذ الجمبري. هذا التطبيق يكفي لإظهار كيف يمكن استخدام RESTinio و SObjectizer معًا لشيء يشبه إلى حد ما مهمة حقيقية ، بدلاً من HelloWorld البسيط. ولكن لديها عدد من العيوب الخطيرة.

على سبيل المثال ، في وكيل convert_manager ، هناك تحقق معين من تفرد الطلب. ولكنه يعمل فقط إذا كانت الصورة المحولة موجودة بالفعل في ذاكرة التخزين المؤقت. إذا لم تكن هناك صورة في ذاكرة التخزين المؤقت حتى الآن وفي نفس الوقت يأتي طلبان متطابقان للصورة نفسها ، فسيتم إرسال كل من هذين الطلبين للمعالجة. ما هو غير جيد. سيكون من الصحيح معالجة واحد منهم فقط ، وتأجيل الثانية حتى تكتمل معالجة الأولى.

وسيؤدي مثل هذا التحكم الأكثر تقدمًا في تفرد الطلبات إلى رمز convert_manager أكثر تعقيدًا وكثافة. لذلك ، لم نبدأ في تنفيذه على الفور ، لكننا قررنا السير في المسار التطوري - من البسيط إلى المعقد.

أيضا ، أبسط نسخة من الروبيان هو "الصندوق الأسود" الذي لا يظهر أي علامات على عمله. وهو ليس مناسبًا جدًا أثناء الاختبار وأثناء التشغيل. لذلك ، بطريقة جيدة ، يجب أن يضيف الجمبري أيضًا قطع الأشجار.

سنحاول إزالة هذه العيوب وبعض أوجه القصور الأخرى في النسخة الأولى من الروبيان في الإصدارات المستقبلية ووصفها في المقالات المستقبلية. لذا ترقبوا.

إذا كان لدى أحد الأشخاص أسئلة حول منطق الجمبري أو RESTinio أو SObjectizer ، فسيسعدنا الإجابة في التعليقات. بالإضافة إلى ذلك ، يعتبر الجمبري نفسه مشروعًا تجريبيًا ، ولكن إذا كان شخص ما مهتمًا بوظائفه ويرغب في رؤية شيء آخر في الجمبري إلى جانب عملية تغيير الحجم ، فأخبرنا أننا سنكون سعداء بالاستماع إلى أي أفكار بناءة .

يتبع ...

Source: https://habr.com/ru/post/ar416387/


All Articles