Shrimp: Skalieren und teilen Sie HTTP-Bilder in modernem C ++ mit ImageMagic ++, SObjectizer und RESTinio



Vorwort


Unser kleines Team entwickelt zwei OpenSource-Tools für C ++ - Entwickler - das SObjectizer- Actor-Framework und den eingebetteten RESTinio-HTTP-Server. Wir stoßen jedoch regelmäßig auf einige nicht triviale Fragen:

  • Welche Funktionen müssen der Bibliothek hinzugefügt werden und welche müssen über Bord gelassen werden?
  • Wie kann man die "ideologisch korrekten" Arten der Nutzung der Bibliothek klar demonstrieren?

Es ist gut, wenn die Antworten auf solche Fragen während der Verwendung unserer Entwicklungen in realen Projekten erscheinen, wenn Entwickler mit ihren Beschwerden oder ihrer Wunschliste zu uns kommen. Aufgrund der Befriedigung der Benutzerwünsche füllen wir unsere Tools mit Funktionen, die vom Leben selbst vorgegeben werden und nicht „aus dem Finger gesaugt“ werden.

Die Informationen erreichen uns jedoch weit entfernt von allen Problemen und Schwierigkeiten, mit denen Benutzer konfrontiert sind. Und wir können die erhaltenen Informationen und insbesondere die Codebeispiele nicht immer in unseren öffentlichen Materialien verwenden.

Daher denken wir uns manchmal kleine Probleme aus und lösen diese, die wir von Entwicklern von Tools zu Benutzern machen müssen. Dies ermöglicht es uns, unsere eigenen Werkzeuge mit anderen Augen zu betrachten und selbst zu verstehen, was gut ist, was nicht gut ist, was fehlt und was zu viel ist.

Heute wollen wir nur eine solche "kleine" Aufgabe erzählen, bei der SObjectizer und RESTinio natürlich zusammengekommen sind.

Skalierung und Verteilung von Bildern. Warum genau das?


Als kleine Demo-Aufgabe haben wir uns für einen HTTP-Server entschieden, der auf Anfrage skalierte Bilder verteilt. Sie legen die Bilder in einem Verzeichnis ab, starten den HTTP-Server und fordern das Formular an:

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

und im Gegenzug erhalten Sie ein Bild, das auf der langen Seite auf 1920 Pixel skaliert ist.

Die Wahl fiel auf diese Aufgabe, da sie die Szenarien perfekt demonstriert, für die wir einmal mit der Entwicklung von RESTinio begonnen haben: In C oder C ++ gibt es einen lang laufenden und debuggten Code, an den Sie eine HTTP-Eingabe anhängen und auf eingehende Anforderungen reagieren müssen. Gleichzeitig, was wichtig ist, kann die Anwendungsverarbeitung der Anforderung beträchtliche Zeit in Anspruch nehmen, und daher ist es unrentabel, den Anwendungscode direkt im E / A-Kontext abzurufen. Der HTTP-Server sollte asynchron sein: Akzeptieren und analysieren Sie die HTTP-Anforderung, geben Sie die analysierte Anforderung irgendwo für die weitere Anwendungsverarbeitung, fahren Sie mit der Bearbeitung der nächsten HTTP-Anforderung fort und kehren Sie zur Rückgabe der Antwort auf die HTTP-Anforderung zurück, wenn diese Antwort von jemandem vorbereitet wurde.

Genau dies geschieht bei der Verarbeitung von Anforderungen zum Skalieren von Bildern. Ein HTTP-Server kann seine direkte Arbeit (d. H. Lesen von Daten, Analysieren einer HTTP-Anforderung) in einem Bruchteil einer Millisekunde ausführen. Das Skalieren eines Bildes kann jedoch zehn, Hunderte oder sogar Tausende von Millisekunden dauern.

Und da das Skalieren eines Bildes viel Zeit in Anspruch nehmen kann, müssen Sie sicherstellen, dass der HTTP-Server während der Skalierung des Bildes weiterarbeiten kann. Dazu müssen wir die Arbeit des HTTP-Servers verteilen und Bilder auf verschiedene Arbeitskontexte skalieren. Im einfachen Fall handelt es sich dabei um unterschiedliche Arbeitsthreads. Nun, da wir in Multi-Core-Prozessoren leben, werden wir mehrere Arbeitsthreads haben. Einige von ihnen bedienen HTTP-Anfragen, andere arbeiten mit Bildern.

Es stellt sich heraus, dass wir zum Verteilen skalierbarer Bilder über HTTP den lang geschriebenen, funktionierenden C / C ++ - Code (in diesem Fall ImageMagic ++) wiederverwenden, HTTP-Anforderungen asynchron bedienen und die Anwendungsverarbeitung von Anforderungen in mehreren Workflows durchführen müssen. Eine hervorragende Aufgabe für RESTinio und SObjectizer, wie es uns schien.

Und wir haben beschlossen, unser Demo-Projekt Shrimp zu nennen.

Shrimps wie es ist


Was macht Shrimp?


Shrimp wird als Konsolenanwendung ausgeführt, öffnet und überwacht den angegebenen Port, empfängt und verarbeitet HTTP-GET-Anforderungen des Formulars:

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

Wo:

  • Bild ist der Name der zu skalierenden Bilddatei. Zum Beispiel my_picture oder DSCF0069;
  • ext ist eine der von Shrimps unterstützten Erweiterungen (jpg, jpeg, png oder gif);
  • Seite ist eine Angabe der Seite, für die die Größe eingestellt ist. Es kann entweder einen Breitenwert haben. In diesem Fall wird das Bild so skaliert, dass die resultierende Breite dem angegebenen Wert entspricht. Die Höhe des Bildes wird automatisch ausgewählt, während das Seitenverhältnis beibehalten wird. Oder der Wert der Höhe, in diesem Fall erfolgt die Skalierung in der Höhe. Entweder max, in diesem Fall ist die lange Seite begrenzt, und Garnelen selbst bestimmen, ob die lange Seite Höhe oder Breite hat;
  • value ist die Größe, bei der die Skalierung erfolgt.

Wenn in der URL nur der Dateiname angegeben ist, ohne dass die Größe geändert werden muss, gibt shrimp in der Antwort einfach das Originalbild zurück. Wenn der Größenänderungsvorgang angegeben ist, ändert Shrimp die Größe des angeforderten Bildes und gibt die skalierte Version an.

Gleichzeitig speichert Shrimps einen Cache mit skalierten Bildern im Speicher. Wenn ein Bild wiederholt mit denselben Größenänderungsparametern angefordert wird, die sich bereits im Cache befinden, wird der Wert aus dem Cache zurückgegeben. Befindet sich kein Bild im Cache, wird das Bild von der Festplatte gelesen, skaliert, im Cache gespeichert und als Antwort zurückgegeben.

Der Cache wird regelmäßig geleert. Bilder, die seit dem letzten Zugriff mehr als eine Stunde im Cache gelebt haben, werden aus dem Cache verschoben. Außerdem werden die ältesten Bilder aus dem Cache geworfen, wenn der Cache seine maximale Größe überschreitet (in einem Demo-Projekt sind es 100 MB).

Wir haben eine Seite vorbereitet, auf der jeder mit Garnelen experimentieren kann:



Auf dieser Seite können Sie die Bildgröße einstellen und auf "Größe ändern" klicken. Zwei Anfragen werden mit denselben Parametern an den Shrimp-Server gesendet. Höchstwahrscheinlich ist die erste Anforderung eindeutig (d. H. Es gibt noch keinen Cache mit solchen Größenänderungsparametern im Cache), sodass die erste Anforderung einige Zeit benötigt, um das Bild tatsächlich zu skalieren. Und die zweite Anforderung findet höchstwahrscheinlich das bereits skalierte Bild im Cache und gibt es sofort weiter.

Es kann beurteilt werden, ob ein Bild aus dem Cache stammt oder ob es wirklich durch den Text unter dem Bild skaliert wurde. Der Text „Transformiert (114,0 ms)“ zeigt beispielsweise an, dass das Bild skaliert wurde und der Zoomvorgang 114 Millisekunden dauerte.

Wie macht Shrimp das?


Shrimp ist eine Multithread-Anwendung, die drei Gruppen von Arbeitsthreads ausführt:

  1. Der Pool von Arbeitsthreads, auf denen der HTTP-Server ausgeführt wird. In diesem Pool werden neue Verbindungen bereitgestellt, eingehende Anforderungen empfangen und analysiert, Antworten generiert und gesendet. Der HTTP-Server wird über die RESTinio-Bibliothek implementiert.
  2. Ein separater Arbeitsthread, auf dem der SObjectizer-Agent transform_manager ausgeführt wird. Dieser Agent verarbeitet vom HTTP-Server empfangene Anforderungen und verwaltet einen Cache mit skalierten Bildern.
  3. Der Thread-Pool, in dem SObjectizer-Agenten Transformatoren arbeiten. Sie führen die eigentliche Skalierung von Bildern mit ImageMagic ++ durch.

Es stellt sich folgendes Arbeitsschema heraus:



Der HTTP-Server akzeptiert die eingehende Anforderung, analysiert sie und überprüft die Richtigkeit. Wenn für diese Anforderung kein Größenänderungsvorgang erforderlich ist, verarbeitet der HTTP-Server selbst die Anforderung über den Sendedateivorgang . Wenn für die Anforderung eine Größenänderungsoperation erforderlich ist, wird die Anforderung asynchron an den transform_manager-Agenten gesendet.

Der transform_manager-Agent empfängt Anforderungen vom HTTP-Server und prüft, ob bereits skalierte Bilder im Cache vorhanden sind. Befindet sich ein Bild im Cache, generiert transform_manager sofort eine Antwort für den HTTP-Server. Wenn kein Bild vorhanden ist, sendet transform_manager eine Anforderung zum Skalieren des Bildes an einen der Transformatoragenten. Wenn das Skalierungsergebnis vom Transformator stammt, wird das Ergebnis im Cache gespeichert und eine Antwort für den HTTP-Server generiert.

Der Transformator-Agent empfängt Anforderungen von transform_manager, verarbeitet sie und gibt das Ergebnis der Transformation an den transform_manager-Agenten zurück.

Was hat Shrimp unter der Haube?


Der in diesem Artikel beschriebene Quellcode für die minimalste Version von Shrimps befindet sich in diesem Repository: Shrimp-Demo auf BitBucket oder auf GitHub .

Es gibt viel Code, obwohl der Code in dieser Version von Shrimps größtenteils ziemlich trivial ist. Es ist jedoch sinnvoll, sich auf einige Aspekte der Implementierung zu konzentrieren.

Verwenden von C ++ 17 und den neuesten Compilerversionen


Bei der Implementierung von Shrimp haben wir uns für C ++ 17 und die neuesten Versionen von Compilern entschieden, insbesondere GCC 7.3 und 8.1. Das Projekt ist stark forschend. Daher ist die praktische Bekanntschaft von C ++ 17 im Rahmen eines solchen Projekts natürlich und zulässig. Während wir uns in alltäglicheren Entwicklungen hier und jetzt auf praktische industrielle Anwendungen konzentrieren, müssen wir auf ziemlich alte Compiler zurückblicken und vielleicht C ++ 14 oder sogar nur eine Teilmenge von C ++ 11 verwenden.

Ich muss sagen, dass C ++ 17 einen guten Eindruck macht. Es scheint, dass wir nicht so viele Innovationen aus dem siebzehnten Standard im Shrimp-Code verwendet haben, aber sie hatten einen positiven Effekt: das Attribut [[nodiscard]], std :: optional / std :: variante / std :: filesystem direkt “ out of the box “und nicht aufgrund externer Abhängigkeiten, strukturierte Bindung, wenn constexpr, die Fähigkeit, Besucher für Lambdas für std :: visit zusammenzustellen ... Individuell sind dies alles Kleinigkeiten, aber zusammen erzeugen sie einen starken kumulativen Effekt.

Das erste nützliche Ergebnis, das wir bei der Entwicklung von Shrimps erhalten haben: C ++ 17 ist es wert, darauf umzusteigen.

HTTP-Server mit RESTinio-Tools


Der vielleicht einfachste Teil von Shrimps war der HTTP-Server und der HTTP-GET-Anforderungshandler ( http_server.hpp und http_server.cpp ).

Eingehende Anfragen empfangen und versenden


Im Wesentlichen konzentriert sich die gesamte Grundlogik des Shrimp-HTTP-Servers auf diese Funktion:

 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(); } ); } 

Diese Funktion bereitet den HTTP-GET-Anforderungshandler mithilfe des RESTinio ExpressJS-Routers vor . Wenn der HTTP-Server eine GET-Anforderung empfängt, deren URL unter den angegebenen regulären Ausdruck fällt, wird die angegebene Lambda-Funktion aufgerufen.

Diese Lambda-Funktion führt einige einfache Überprüfungen der Richtigkeit der Anforderung durch. Im Wesentlichen handelt es sich jedoch um eine einfache Auswahl: Wenn die Größenänderung nicht festgelegt ist, wird das angeforderte Bild in seiner ursprünglichen Form mithilfe einer effektiven System-Sendedatei zurückgegeben. Wenn der Größenänderungsmodus festgelegt ist, wird eine Nachricht generiert und an den transform_manager-Agenten gesendet:

 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 ); } 

Es stellt sich heraus, dass der HTTP-Server, nachdem er die Größenänderungsanforderung akzeptiert hat, sie über eine asynchrone Nachricht an den transform_manager-Agenten weiterleitet und weiterhin andere Anforderungen bearbeitet.

Dateifreigabe mit sendfile


Wenn der HTTP-Server eine Anforderung für das Originalbild ohne den Größenänderungsvorgang erkennt, sendet der Server dieses Bild sofort über den Sendedateivorgang. Der damit verbundene Hauptcode lautet wie folgt (der vollständige Code für diese Funktion befindet sich im Repository ):

 [[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 ) ); } 

Der entscheidende Punkt hierbei ist, restinio :: sendfile () aufzurufen und dann den von dieser Funktion zurückgegebenen Wert an set_body () zu übergeben.

Die Funktion restinio :: sendfile () erstellt einen Datei-Upload-Vorgang mithilfe der System-API. Wenn diese Operation an set_body () übergeben wird, versteht RESTinio, dass der Inhalt der in restinio :: sendfile () angegebenen Datei für den Hauptteil der HTTP-Antwort verwendet wird. Anschließend wird der Inhalt dieser Datei mithilfe der System-API in den TCP-Socket geschrieben.

Implementieren des Image-Cache


Der transform_manager-Agent speichert den Cache konvertierter Bilder, in dem die Bilder nach der Skalierung platziert werden. Dieser Cache ist ein einfacher selbst erstellter Container, der auf zwei Arten Zugriff auf seinen Inhalt bietet:

  1. Durch Suchen nach einem Element per Schlüssel (ähnlich wie dies in den Standardcontainern std :: map und std :: unordered_map geschieht).
  2. Durch Zugriff auf das älteste Cache-Element.

Die erste Zugriffsmethode wird verwendet, wenn die Verfügbarkeit des Bildes im Cache überprüft werden muss. Das zweite ist, wenn wir die ältesten Bilder aus dem Cache löschen.

Wir haben nicht begonnen, im Internet nach etwas zu suchen, das für diese Zwecke bereit ist. Wahrscheinlich wäre Boost.MultiIndex hier durchaus geeignet. Aber ich wollte Boost nicht nur wegen MultiIndex ziehen, also haben wir unsere triviale Implementierung buchstäblich auf meinen Knien gemacht. Es scheint zu funktionieren;)

Warteschlange ausstehender Anforderungen in transform_manager


Der transform_manager-Agent erwies sich trotz seiner recht anständigen Größe (eine HPP-Datei mit ca. 250 Zeilen und eine CPP-Datei mit ca. 270 Zeilen) in unserer einfachsten Implementierung von Shrimp unserer Meinung nach als eher trivial.

Eines der Dinge, die einen wesentlichen Beitrag zur Komplexität und Menge des Agentencodes leisten, ist das Vorhandensein nicht nur eines Caches transformierter Bilder in transform_manager, sondern auch von Warteschlangen ausstehender Anforderungen.

Wir haben eine begrenzte Anzahl von Transformatoragenten (im Prinzip sollte ihre Anzahl ungefähr der Anzahl der verfügbaren Prozessorkerne entsprechen). Wenn mehr Anfragen gleichzeitig eingehen als freie Transformatoren, können wir entweder sofort negativ auf die Anfrage reagieren oder die Anfrage in die Warteschlange stellen. Und nehmen Sie es dann aus der Warteschlange, wenn ein freier Transformator erscheint.

In Shrimps verwenden wir eine Warteschlange mit Warteanfragen, die wie folgt definiert ist:

 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 }; 

Nach Eingang der Anfrage stellen wir sie in die Warteschlange, um den Zeitpunkt des Eingangs der Anfrage festzulegen. Anschließend überprüfen wir regelmäßig, ob das Zeitlimit für diese Anforderung abgelaufen ist. Grundsätzlich kann es vorkommen, dass ein Bündel „schwerer“ Anfragen früher eingetroffen ist, deren Bearbeitung zu lange gedauert hat. Es ist falsch, endlos auf das Erscheinen eines freien Transformators zu warten. Es ist besser, nach einiger Zeit eine negative Antwort an den Client zu senden, was bedeutet, dass der Dienst jetzt überlastet ist.

Es gibt auch eine Größenbeschränkung für die Warteschlange ausstehender Anforderungen. Wenn die Warteschlange bereits ihre maximale Größe erreicht hat, lehnen wir die Verarbeitung der Anforderung sofort ab und teilen dem Client mit, dass wir überlastet sind.

Es gibt einen wichtigen Punkt im Zusammenhang mit der Warteschlange ausstehender Anfragen, auf den wir uns im Abschluss des Artikels konzentrieren werden.

Geben Sie sobj_shptr_t ein und verwenden Sie Nachrichteninstanzen erneut


Beim Bestimmen des Typs der Warteschlange wartender Anforderungen sowie bei den Signaturen einiger Methoden von transform_manager können Sie die Verwendung des Typs sobj_shptr_t sehen. Es ist sinnvoll, genauer darauf einzugehen, um welchen Typ es sich handelt und warum er verwendet wird.

Unter dem Strich empfängt transform_manager eine Anfrage vom HTTP-Server als resize_request_t-Nachricht:

 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 } {} }; 

und wir müssen etwas tun, um diese Informationen in der Warteschlange wartender Anfragen zu speichern. Sie können beispielsweise eine neue Instanz von resize_request_t erstellen und die Werte aus der empfangenen Nachricht in diese verschieben.

Und Sie können sich daran erinnern, dass die Nachricht selbst in SObjectizer ein dynamisch erstelltes Objekt ist. Und kein einfaches Objekt, sondern mit einem Linkzähler im Inneren. Und dass es in SObjectizer einen speziellen Typ eines intelligenten Zeigers für solche Objekte gibt - intrusive_ptr_t.

Das heißt, Wir können keine Kopie von resize_request_t für die Warteschlange wartender Anforderungen erstellen, aber wir können einfach einen intelligenten Zeiger auf eine vorhandene Instanz von resize_request_t in diese Warteschlange stellen. Was wir tun Und um nicht überall den eher exotischen Namen so_5 :: intrusive_ptr_t zu schreiben, geben wir unseren Alias ​​ein:

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

Asynchrone Antworten auf Clients


Wir haben gesagt, dass HTTP-Anfragen asynchron verarbeitet werden. Und wir haben oben gezeigt, wie der HTTP-Server eine Anfrage mit einer asynchronen Nachricht an den transform_manager-Agenten sendet. Aber was passiert mit Antworten auf HTTP-Anfragen?

Antworten werden auch asynchron bereitgestellt. Im Code transform_manager sehen Sie beispielsweise Folgendes:

 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) ); } 

Dieser Code generiert eine negative Antwort auf die HTTP-Anforderung, wenn das Bild aus irgendeinem Grund nicht skaliert werden konnte. Die Antwort wird in der Hilfsfunktion do_404_response generiert, deren Code wie folgt dargestellt werden kann:

 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(); } 

Der erste wichtige Punkt bei do_404_response () ist, dass diese Funktion im Arbeitskontext des transform_manager-Agenten und nicht im Arbeitskontext des HTTP-Servers aufgerufen wird.

Der zweite wichtige Punkt ist der Aufruf der Methode done () für das vollständig geformte bzw. Objekt. Alle asynchrone Magie mit einer HTTP-Antwort geschieht hier. Die Methode done () verwendet alle in bzw. vorbereiteten Informationen und sendet sie asynchron an den HTTP-Server. Das heißt, Eine Rückgabe von do_404_response () erfolgt unmittelbar nachdem der Inhalt des resp-Objekts vom HTTP-Server in die Warteschlange gestellt wurde.

Der HTTP-Server in seinem Arbeitskontext erkennt das Vorhandensein einer neuen HTTP-Antwort und führt die erforderlichen Aktionen aus, um die Antwort an den entsprechenden Client zu senden.

Geben Sie datasizable_blob_t ein


Ein weiterer kleiner Punkt, der zur Klärung sinnvoll ist, da er wahrscheinlich unverständlich ist, ohne die Feinheiten von RESTinio zu verstehen. Wir sprechen über das Vorhandensein eines seltsamen Typs von datizizeable_blob_t, der auf den ersten Blick wie folgt definiert ist:

 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 ) }; }; 

Um zu erklären, warum dieser Typ benötigt wird, müssen Sie zeigen, wie eine HTTP-Antwort mit einem transformierten Bild gebildet wird:

 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(); } 

Wir achten auf den Aufruf von set_body (): Ein intelligenter Zeiger auf die Instanz datasizable_blob_t wird direkt dorthin gesendet. Warum?

Tatsache ist, dass RESTinio verschiedene Optionen zum Bilden des Körpers einer HTTP-Antwort unterstützt . Am einfachsten ist es, eine Instanz vom Typ std :: string an set_body () zu übergeben, und RESTinio speichert den Wert dieser Zeichenfolge im resp-Objekt.

Es gibt jedoch Situationen, in denen der Wert für set_body () in mehreren Antworten gleichzeitig wiederverwendet werden sollte. Bei Garnelen geschieht dies beispielsweise, wenn Garnelen mehrere identische Anforderungen für die Transformation desselben Bildes erhalten.In diesem Fall ist es unrentabel, in jede Antwort denselben Wert zu kopieren. Daher gibt es in RESTinio eine set_body () -Variante des Formulars:
 template<typename T> auto set_body(std::shared_ptr<T> body); 

In diesem Fall wird dem Typ T jedoch eine wichtige Einschränkung auferlegt: Er muss die Methoden public data () und size () enthalten, die erforderlich sind, damit RESTinio auf den Inhalt der Antwort zugreifen kann.

Das skalierte Bild in Shrimps wird als Magick :: Blob-Objekt gespeichert. Es gibt eine Datenmethode im Magic :: Blob-Typ, aber keine size () -Methode, sondern eine length () -Methode. Daher brauchten wir die Wrapper-Klasse datasizable_blob_t, die RESTinio die notwendige Schnittstelle für den Zugriff auf den Wert von Magick :: Blob bietet.

Periodische Nachrichten in transform_manager


Der transform_manager-Agent muss von Zeit zu Zeit verschiedene Dinge tun:

  • Ziehen Sie Bilder, die sich zu lange im Cache befinden, aus dem Cache.
  • Steuern Sie die Zeit, die Anforderungen in der Warteschlange der freien Transformatoren verbringen.

Der transform_manager-Agent führt diese Aktionen über regelmäßige Nachrichten aus. Es sieht wie folgt aus.

Zunächst werden die Arten von Signalen bestimmt, die als periodische Nachrichten verwendet werden:

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

Dann wird der Agent abonniert, einschließlich dieser Signale:

 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> ) {...} 

Dank des Abonnements ruft SObjectizer den gewünschten Handler auf, wenn der Agent das entsprechende Signal empfängt.

Und es bleibt nur, um regelmäßige Nachrichten auszuführen, wenn der Agent startet:

 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 ); } 

Der entscheidende Punkt hierbei ist das Speichern von timer_id, die von den Funktionen send_periodic () zurückgegeben werden. Schließlich wird ein periodisches Signal nur kommen, solange seine timer_id aktiv ist. Wenn der Rückgabewert von send_periodic () nicht gespeichert wird, wird das Senden einer periodischen Nachricht sofort abgebrochen. Daher weist die Klasse a_transform_manager_t die folgenden Attribute auf:

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

Ende des ersten Teils


Heute haben wir dem Leser die einfachste und minimalistischste Implementierung von Garnelen vorgestellt. Diese Implementierung reicht aus, um zu zeigen, wie RESTinio und SObjectizer zusammen für etwas verwendet werden können, das mehr oder weniger einer echten Aufgabe ähnelt, als einer einfachen HelloWorld. Aber es hat eine Reihe schwerwiegender Mängel.

Beispielsweise wird im transform_manager-Agenten eine bestimmte Überprüfung der Eindeutigkeit der Anforderung durchgeführt. Dies funktioniert jedoch nur, wenn sich das transformierte Bild bereits im Cache befindet. Befindet sich noch kein Bild im Cache und gleichzeitig kommen zwei identische Anforderungen für dasselbe Bild, werden beide Anforderungen zur Verarbeitung gesendet. Was ist nicht gut Es wäre richtig, nur einen von ihnen zu verarbeiten und den zweiten zu verschieben, bis die Verarbeitung des ersten abgeschlossen ist.

Eine derart erweiterte Kontrolle über die Eindeutigkeit von Anforderungen würde zu einem viel komplexeren und umfangreicheren transform_manager-Code führen. Daher haben wir nicht sofort damit begonnen, sondern beschlossen, den evolutionären Weg zu gehen - von einfach bis komplex.

Die einfachste Version von Garnelen ist eine „Black Box“, die keine Anzeichen ihrer Arbeit zeigt. Was sowohl beim Testen als auch während des Betriebs nicht sehr praktisch ist. Daher sollten Garnelen in guter Weise auch die Protokollierung hinzufügen.

Wir werden versuchen, diese und einige andere Mängel der allerersten Version von Shrimps in zukünftigen Versionen zu beseitigen und sie in zukünftigen Artikeln zu beschreiben. Also bleibt dran.

Wenn jemand Fragen zur Logik von Shrimps, RESTinio oder SObjectizer hat, beantworten wir diese gerne in den Kommentaren. Darüber hinaus ist Shrimp selbst ein Demo-Projekt. Wenn jedoch jemand an seiner Funktionalität interessiert ist und neben dem Größenänderungsvorgang noch etwas anderes in Shrimp sehen möchte, lassen Sie es uns wissen. Gerne hören wir uns konstruktive Ideen an .

Fortsetzung folgt ...

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


All Articles