
Seit Anfang 2017 entwickelt unser kleines Team die
RESTinio OpenSource-Bibliothek zum Einbetten eines HTTP-Servers in C ++ - Anwendungen. Zu unserer großen Überraschung erhalten wir von Zeit zu Zeit Fragen aus der Kategorie "Und warum ist möglicherweise ein eingebetteter C ++ - HTTP-Server erforderlich?" Leider sind einfache Fragen am schwierigsten zu beantworten. Manchmal ist die beste Antwort Beispielcode.
Vor ein paar Monaten haben wir mit
Shrimp ein kleines
Demo-Projekt gestartet, das ein typisches Szenario deutlich macht, in dem unsere Bibliothek „geschärft“ wird. Das Demo-Projekt ist ein einfacher Webdienst, der Anforderungen zum Skalieren von auf dem Server gespeicherten Bildern empfängt und ein Bild mit der vom Benutzer benötigten Größe zurückgibt.
Dieses Demo-Projekt ist insofern gut, als es zum einen die Integration mit vor langer Zeit geschriebenem und korrekt funktionierendem Code in C oder C ++ (in diesem Fall ImageMagick) erfordert. Daher sollte klar sein, warum es sinnvoll ist, den HTTP-Server in eine C ++ - Anwendung einzubetten.
Und zweitens ist in diesem Fall eine asynchrone Anforderungsverarbeitung erforderlich, damit der HTTP-Server nicht blockiert, während das Image skaliert wird (und dies kann Hunderte von Millisekunden oder sogar Sekunden dauern). Und wir haben mit der Entwicklung von RESTinio begonnen, gerade weil wir keinen vernünftigen eingebetteten C ++ - Server finden konnten, der speziell auf die asynchrone Anforderungsverarbeitung ausgerichtet ist.
Wir haben die Arbeit iterativ auf Shrimp aufgebaut: Zuerst wurde die einfachste Version erstellt und
beschrieben , die nur die Bilder skalierte. Dann haben wir eine Reihe von Mängeln der ersten Version behoben und
dies im zweiten Artikel beschrieben . Schließlich haben wir die Funktionalität von Shrimp noch einmal erweitert: Die Konvertierung von Bildern von einem Format in ein anderes wurde hinzugefügt. Wie dies gemacht wurde und wird in diesem Artikel besprochen.
Unterstützung des Zielformats
In der nächsten Version von Shrimp haben wir die Möglichkeit hinzugefügt, ein skaliertes Bild in einem anderen Format zu erstellen. Wenn Sie also eine Shrimp-Anfrage des Formulars stellen:
curl "http://localhost:8080/my_picture.jpg?op=resize&max=1920"
Dann rendert Shrimp das Bild im gleichen JPG-Format wie das Originalbild.
Wenn Sie der URL jedoch den Parameter für das Zielformat hinzufügen, konvertiert Shrimp das Bild in das angegebene Zielformat. Zum Beispiel:
curl "http://localhost:8080/my_picture.jpg?op=resize&max=1920&target-format=webp"
In diesem Fall rendert Shrimp das Bild im Webp-Format.
Das aktualisierte Shrimp unterstützt fünf Bildformate: JPG, PNG, GIF, WebP und Heic (auch als HEIF bekannt). Sie können mit verschiedenen Formaten
auf einer speziellen Webseite experimentieren:

(Auf dieser Seite gibt es keine Möglichkeit, das heic-Format auszuwählen, da normale Desktop-Browser dieses Format standardmäßig nicht unterstützen.)
Um das Zielformat in Shrimp zu unterstützen, musste der Shrimp-Code leicht geändert werden (was uns selbst überraschte, da es wirklich nur wenige Änderungen gab). Andererseits musste ich mit der Assembly von ImageMagick spielen, von der wir noch mehr überrascht waren Früher mussten wir uns durch einen glücklichen Zufall mit dieser Küche befassen. Aber reden wir über alles in Ordnung.
ImageMagick muss verschiedene Formate verstehen
ImageMagick verwendet externe Bibliotheken zum Codieren / Decodieren von Bildern: libjpeg, libpng, libgif usw. Diese Bibliotheken müssen auf dem System installiert sein, bevor ImageMagick konfiguriert und erstellt wird.
Dasselbe sollte passieren, damit ImageMagick die Formate webp und heic unterstützt: Zuerst müssen Sie libwebp und libheif erstellen und installieren, dann ImageMagick konfigurieren und installieren. Und wenn mit libwebp alles einfach ist, dann musste ich um libheif mit einem Tamburin tanzen. Obwohl nach einiger Zeit, nachdem sich alles endlich versammelt und funktioniert hatte, bereits nicht klar war: Warum mussten Sie auf ein Tamburin zurückgreifen, scheint alles trivial zu sein? ;)
Wenn sich jemand mit heic und ImageMagick anfreunden möchte, müssen Sie im Allgemeinen Folgendes installieren:
Es ist in dieser Reihenfolge (möglicherweise müssen Sie
nasm installieren, damit
x265 mit maximaler Geschwindigkeit funktioniert). Wenn Sie dann den Befehl
./configure ausgeben, kann ImageMagick alles finden, was zur Unterstützung von .heic-Dateien erforderlich ist.
Unterstützung für das Zielformat in der Abfragezeichenfolge eingehender Anforderungen
Nachdem wir ImageMagick mit den Formaten webp und heic befreundet haben, ist es Zeit, den Shrimp-Code zu ändern. Zunächst müssen wir lernen, wie das Zielformatargument in eingehenden HTTP-Anforderungen erkannt wird.
Aus Sicht von RESTinio ist dies überhaupt kein Problem. Nun, ein weiteres Argument erschien in der Abfragezeichenfolge, na und? Aus Sicht von Shrimp stellte sich die Situation jedoch als etwas komplizierter heraus, sodass der Code der Funktion, die für das Parsen der HTTP-Anforderung verantwortlich war, komplizierter wurde.
Tatsache ist, dass es zuvor notwendig war, nur zwei Situationen zu unterscheiden:
- Es kam eine Anfrage der Form "/filename.ext" ohne weitere Parameter. Sie müssen also nur die Datei "filename.ext" so wie sie ist angeben.
- Eine Anfrage kam in der Form "/filename.ext?op=resize & ...". In diesem Fall müssen Sie das Bild aus der Datei "filename.ext" skalieren.
Nach dem Hinzufügen des Zielformats müssen wir jedoch zwischen vier Situationen unterscheiden:
- Es kam eine Anfrage der Form "/filename.ext" ohne weitere Parameter. Sie müssen also nur die Datei "filename.ext" so wie sie ist angeben, ohne Skalierung und ohne Transcodierung in ein anderes Format.
- Es kam eine Anfrage der Form "/filename.ext?target-format=fmt" ohne weitere Parameter. Es bedeutet, ein Bild aus der Datei "filename.ext" zu nehmen und es in das Format "fmt" zu transkodieren, wobei die Originalgrößen beibehalten werden.
- Eine Anfrage kam in der Form "/filename.ext?op=resize & ...", jedoch ohne Zielformat. In diesem Fall müssen Sie das Bild aus der Datei „filename.ext“ skalieren und im Originalformat angeben.
- Eine Anfrage kam von der Form "/filename.ext?op=resize&...&target-format=fmt". In diesem Fall müssen Sie eine Skalierung durchführen und das Ergebnis dann in das Format „fmt“ umcodieren.
Infolgedessen hatte die Funktion zum Bestimmen der Abfrageparameter die
folgende Form :
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 qp = restinio::parse_query( req->header().query() ); const auto target_format = qp.get_param( "target-format"sv ); // // . target-format, // . target-format // , , // . const auto image_format = try_detect_target_image_format( params[ "ext" ], target_format ); if( !image_format ) { // . . return do_400_response( std::move( req ) ); } if( !qp.size() ) { // , . return serve_as_regular_file( app_params.m_storage.m_root_dir, std::move( req ), *image_format ); } const auto operation = qp.get_param( "op"sv ); if( operation && "resize"sv != *operation ) { // , resize. return do_400_response( std::move( req ) ); } if( !operation && !target_format ) { // op=resize, // target-format=something. return do_400_response( std::move( req ) ); } handle_resize_op_request( req_handler_mbox, *image_format, qp, std::move( req ) ); return restinio::request_accepted(); } ); }
In der vorherigen Version von Shrimp, in der Sie das Bild nicht transkodieren mussten, sah das Arbeiten mit den Anforderungsparametern
etwas einfacher aus .
Anforderungswarteschlange und Bildcache auf das Zielformat zugeschnitten
Der nächste Punkt bei der Implementierung der Unterstützung für das Zielformat war die Arbeit an der Warteschlange für wartende Anforderungen und einem Cache mit vorgefertigten Bildern im Agenten a_transform_manager. Wir haben
im vorherigen Artikel ausführlicher über diese Dinge gesprochen, aber wir möchten Sie ein wenig daran erinnern, worum es ging.
Wenn eine Anforderung zur Bildkonvertierung eintrifft, kann sich herausstellen, dass sich das fertige Bild mit solchen Parametern bereits im Cache befindet. In diesem Fall müssen Sie nichts tun, sondern nur das Bild als Antwort aus dem Cache senden. Wenn das Bild transformiert werden muss, stellt sich möglicherweise heraus, dass derzeit keine freien Mitarbeiter vorhanden sind und Sie warten müssen, bis es angezeigt wird. Dazu müssen Anforderungsinformationen in die Warteschlange gestellt werden. Gleichzeitig ist es jedoch erforderlich, die Eindeutigkeit der Anforderungen zu überprüfen. Wenn drei identische Anforderungen auf die Verarbeitung warten (d. H. Dass dasselbe Bild auf dieselbe Weise konvertiert werden muss), sollten wir das Bild nur einmal verarbeiten und das Ergebnis der Verarbeitung als Antwort angeben zu diesen drei Anfragen. Das heißt, In der Warteschlange müssen identische Anforderungen gruppiert werden.
Früher in Shrimp haben wir einen einfachen zusammengesetzten Schlüssel verwendet, um den Bildcache und die Warteschlange zu durchsuchen: eine
Kombination aus dem ursprünglichen Dateinamen und den
Optionen zur Größenänderung von Bildern . Nun mussten zwei neue Faktoren berücksichtigt werden:
- erstens das Zielbildformat (d. h. das Originalbild kann in jpg sein und das resultierende Bild kann in png sein);
- zweitens die Tatsache, dass eine Skalierung des Bildes möglicherweise nicht erforderlich ist. Dies geschieht in einer Situation, in der der Client nur die Konvertierung des Bildes von einem Format in ein anderes anordnet, wobei jedoch die Originalgröße des Bildes erhalten bleibt.
Ich muss sagen, dass wir hier den einfachsten Weg gegangen sind, ohne irgendwie etwas zu optimieren. Beispielsweise könnte man versuchen, zwei Caches zu erstellen: Einer würde Bilder im Originalformat speichern, aber auf die gewünschte Größe skalieren, und im zweiten Fall würden die skalierten Bilder in das Zielformat konvertiert.
Warum sollte ein solches doppeltes Caching erforderlich sein? Tatsache ist, dass beim Transformieren von Bildern die zwei teuersten Vorgänge in der Zeit die Größenänderung und Serialisierung des Bilds in das Zielformat sind. Wenn wir daher die Anforderung erhalten, das Bild example.jpg auf eine Größe von 1920 in der Breite zu skalieren und in das Webp-Format umzuwandeln, können wir zwei Bilder in unserem Speicher speichern: example_1920px_width.jpg und example_1920px_width.webp. Wir würden ein Beispiel_1920px_width.webp Bild geben, wenn wir eine zweite Anfrage erhalten. Das Bild example_1920px_width.jpg kann jedoch verwendet werden, wenn Anforderungen zum Skalieren von example.jpg auf eine Größe von 1920 in der Breite empfangen und in ein heic-Format umgewandelt werden. Wir könnten den Größenänderungsvorgang überspringen und nur die Formatkonvertierung durchführen (d. H. Das fertige Bild example_1920px_width.jpg würde in das heic-Format transkodiert).
Eine weitere potenzielle Möglichkeit: Wenn eine Anforderung zum Umcodieren eines Bilds in ein anderes Format ohne Größenänderung eingeht, können Sie die tatsächliche Größe des Bildes bestimmen und diese Größe im zusammengesetzten Schlüssel verwenden. Beispiel: Beispiel.jpg hat eine Größe von 3000 x 2000 Pixel. Wenn wir als nächstes eine Anfrage zur Skalierung von example.jpg auf 2000px Höhe erhalten, können wir sofort feststellen, dass wir bereits ein Bild in dieser Größe haben.
Theoretisch verdienen all diese Überlegungen Beachtung. Aus praktischer Sicht ist jedoch nicht klar, wie hoch die Wahrscheinlichkeit einer solchen Entwicklung von Ereignissen ist. Das heißt, Wie oft erhalten wir eine Anfrage zur Skalierung von example.jpg auf 1920px mit Konvertierung in webp und dann eine Anfrage nach derselben Skalierung desselben Bildes, jedoch mit Konvertierung in png? Es ist schwer zu sagen, keine wirklichen Statistiken zu haben. Aus diesem Grund haben wir uns entschlossen, unser Leben in unserem Demo-Projekt nicht zu verkomplizieren, sondern zunächst den einfachsten Weg zu gehen. Mit der Erwartung, dass, wenn jemand fortgeschrittenere Caching-Schemata benötigt, diese später hinzugefügt werden können, ausgehend von realen, nicht fiktiven Szenarien für die Verwendung von Shrimp.
Infolgedessen haben wir in der aktualisierten Version von Shrimp den Schlüssel leicht erweitert und ihm auch einen Parameter wie das Zielformat hinzugefügt:
class resize_request_key_t { std::string m_path; image_format_t m_format; resize_params_t m_params; public: resize_request_key_t( std::string path, image_format_t format, resize_params_t params ) : m_path{ std::move(path) } , m_format{ format } , m_params{ params } {} [[nodiscard]] bool operator<(const resize_request_key_t & o ) const noexcept { return std::tie( m_path, m_format, m_params ) < std::tie( o.m_path, o.m_format, o.m_params ); } [[nodiscard]] const std::string & path() const noexcept { return m_path; } [[nodiscard]] image_format_t format() const noexcept { return m_format; } [[nodiscard]] resize_params_t params() const noexcept { return m_params; } };
Das heißt, Die Anforderung zur Größenänderung von example.jpg bis 1920px mit Konvertierung in PNG unterscheidet sich von der gleichen Größenänderung, jedoch mit der Konvertierung in WebP oder Heic.
Das Hauptaugenmerk liegt jedoch
auf der neuen Implementierung der Klasse resize_params_t , die die neuen Größen des skalierten Bildes bestimmt.
Bisher unterstützte diese Klasse drei Optionen: Nur die Breite wurde festgelegt, nur die Höhe wurde festgelegt oder die lange Seite wurde festgelegt (die Höhe oder Breite wird durch die aktuelle Bildgröße bestimmt). Dementsprechend hat die Methode
resize_params_t :: value () immer einen realen Wert zurückgegeben (welcher Wert wurde durch die Methode
resize_params_t :: mode () bestimmt).
Im neuen Shrimp wurde jedoch ein weiterer Modus hinzugefügt - keep_original. Dies bedeutet, dass keine Skalierung durchgeführt wird und das Bild in seiner ursprünglichen Größe gerendert wird. Um diesen Modus zu unterstützen, musste resize_params_t einige Änderungen vornehmen. Zunächst bestimmt jetzt die Methode
resize_params_t :: make () , ob der keep_original-Modus verwendet wird (es wird davon ausgegangen, dass dieser Modus verwendet wird, wenn keiner der Parameter width, height und max in der Abfragezeichenfolge der eingehenden Anforderung angegeben ist). Dadurch konnten wir die Funktion
handle_resize_op_request () nicht neu
schreiben , wodurch die Anforderung zum Skalieren des
auszuführenden Bildes weitergeleitet wird.
Zweitens kann die Methode
resize_params_t :: value () jetzt nicht immer aufgerufen werden, sondern nur, wenn sich der Skalierungsmodus von keep_original unterscheidet.
Das Wichtigste ist jedoch, dass
resize_params_t :: operator <() weiterhin wie beabsichtigt funktioniert.
Dank all dieser Änderungen im a_transform_manager sind sowohl der skalierte Bildcache als auch die Warteschlange der wartenden Anforderungen gleich geblieben. In diesen Datenstrukturen werden nun Informationen zu verschiedenen Abfragen gespeichert. Der Schlüssel {"example.jpg", "jpg", keep_original} unterscheidet sich also sowohl vom Schlüssel {"example.jpg", "png", keep_original} als auch vom Schlüssel {"example.jpg", "jpg", width = 1920px}.
Es stellte sich heraus, dass wir, nachdem wir die Definition so einfacher Datenstrukturen wie resize_params_t und resize_params_key_t ein wenig verdorben hatten, es vermieden haben, komplexere Strukturen wie den Cache der resultierenden Bilder und die Warteschlange wartender Anforderungen zu ändern.
Unterstützung für das Zielformat in a_transformer
Nun, der letzte Schritt zur Unterstützung des Zielformats besteht darin, die Logik des Agenten a_transformer zu erweitern, sodass das möglicherweise bereits skalierte Bild dann in das Zielformat konvertiert wird.
Es stellte sich heraus, dass dies am einfachsten war. Sie mussten
lediglich den Code der Methode
a_transform_t :: handle_resize_request () erweitern :
[[nodiscard]] a_transform_manager_t::resize_result_t::result_t a_transformer_t::handle_resize_request( const transform::resize_request_key_t & key ) { try { m_logger->trace( "transformation started; request_key={}", key ); auto image = load_image( key.path() ); const auto resize_duration = measure_duration( [&]{ // // keep_original. if( transform::resize_params_t::mode_t::keep_original != key.params().mode() ) { transform::resize( key.params(), total_pixel_count, image ); } } ); m_logger->debug( "resize finished; request_key={}, time={}ms", key, std::chrono::duration_cast<std::chrono::milliseconds>( resize_duration).count() ); image.magick( magick_from_image_format( key.format() ) ); datasizable_blob_shared_ptr_t blob; const auto serialize_duration = measure_duration( [&] { blob = make_blob( image ); } ); m_logger->debug( "serialization finished; request_key={}, time={}ms", key, std::chrono::duration_cast<std::chrono::milliseconds>( serialize_duration).count() ); return a_transform_manager_t::successful_resize_t{ std::move(blob), std::chrono::duration_cast<std::chrono::microseconds>( resize_duration), std::chrono::duration_cast<std::chrono::microseconds>( serialize_duration) }; } catch( const std::exception & x ) { return a_transform_manager_t::failed_resize_t{ x.what() }; } }
Im Vergleich
zur Vorgängerversion gibt es zwei grundlegende Ergänzungen.
Rufen Sie zunächst die wirklich magische image.magick () -Methode nach der Größenänderung auf. Diese Methode teilt ImageMagick das resultierende Bildformat mit. Gleichzeitig ändert sich die Darstellung des Bildes im Speicher nicht - ImageMagick speichert es weiterhin so, wie es ihm passt. Der von der magick () -Methode festgelegte Wert wird dann beim nachfolgenden Aufruf von Image :: write () berücksichtigt.
Zweitens zeichnet die aktualisierte Version die Zeit auf, die zum Serialisieren des Bildes in das angegebene Format benötigt wird. Die neue Version von Shrimp legt jetzt die für die Skalierung aufgewendete Zeit und die für die Konvertierung in das Zielformat aufgewendete Zeit separat fest.
Der Rest des Agenten a_transformer_t hat keine Änderungen erfahren.
ImageMagick-Parallelisierung
Standardmäßig wird ImageMagic mit OpenMP-Unterstützung erstellt. Das heißt, Es ist möglich, Operationen an Bildern, die ImageMagick ausführt, zu parallelisieren. Sie können die Anzahl der Workflows, die ImageMagick in diesem Fall verwendet, mithilfe der Umgebungsvariablen MAGICK_THREAD_LIMIT steuern.
Auf meinem Testgerät mit dem Wert MAGICK_THREAD_LIMIT = 1 (d. H. Ohne echte Parallelisierung) erhalte ich beispielsweise die folgenden Ergebnisse:
curl "http://localhost:8080/DSC08084.jpg?op=resize&max=2400" -v > /dev/null > GET /DSC08084.jpg?op=resize&max=2400 HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.47.0 > Accept: */* > < HTTP/1.1 200 OK < Connection: keep-alive < Content-Length: 2043917 < Server: Shrimp draft server < Date: Wed, 15 Aug 2018 11:51:24 GMT < Last-Modified: Wed, 15 Aug 2018 11:51:24 GMT < Access-Control-Allow-Origin: * < Access-Control-Expose-Headers: Shrimp-Processing-Time, Shrimp-Resize-Time, Shrimp-Encoding-Time, Shrimp-Image-Src < Content-Type: image/jpeg < Shrimp-Image-Src: transform < Shrimp-Processing-Time: 1323 < Shrimp-Resize-Time: 1086.72 < Shrimp-Encoding-Time: 236.276
Die für die Größenänderung aufgewendete Zeit wird im Header Shrimp-Resize-Time angegeben. In diesem Fall sind es 1086,72 ms.
Wenn Sie jedoch MAGICK_THREAD_LIMIT = 3 auf demselben Computer festlegen und Shrimp ausführen, erhalten wir unterschiedliche Werte:
curl "http://localhost:8080/DSC08084.jpg?op=resize&max=2400" -v > /dev/null > GET /DSC08084.jpg?op=resize&max=2400 HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.47.0 > Accept: */* > < HTTP/1.1 200 OK < Connection: keep-alive < Content-Length: 2043917 < Server: Shrimp draft server < Date: Wed, 15 Aug 2018 11:53:49 GMT < Last-Modified: Wed, 15 Aug 2018 11:53:49 GMT < Access-Control-Allow-Origin: * < Access-Control-Expose-Headers: Shrimp-Processing-Time, Shrimp-Resize-Time, Shrimp-Encoding-Time, Shrimp-Image-Src < Content-Type: image/jpeg < Shrimp-Image-Src: transform < Shrimp-Processing-Time: 779.901 < Shrimp-Resize-Time: 558.246 < Shrimp-Encoding-Time: 221.655
Das heißt, Die Größenänderungszeit wurde auf 558,25 ms reduziert.
Da ImageMagick die Möglichkeit bietet, Berechnungen zu parallelisieren, können Sie diese Möglichkeit nutzen. Gleichzeitig ist es jedoch wünschenswert zu steuern, wie viele Arbeitsfäden Shrimp für sich selbst benötigt. In früheren Versionen von Shrimp konnte nicht beeinflusst werden, wie viele Workflows Shrimp erstellt. In der aktualisierten Version von Shrimp ist dies möglich. Oder über Umgebungsvariablen, zum Beispiel:
SHRIMP_IO_THREADS=1 \ SHRIMP_WORKER_THREADS=3 \ MAGICK_THREAD_LIMIT=4 \ shrimp.app -p 8080 -i ...
Oder über Befehlszeilenargumente, zum Beispiel:
MAGICK_THREAD_LIMIT=4 \ shrimp.app -p 8080 -i ... --io-threads 1 --worker-threads 4
Über die Befehlszeile angegebene Werte haben eine höhere Priorität.
Es sollte betont werden, dass MAGICK_THREAD_LIMIT nur die Operationen betrifft, die ImageMagick selbst ausführt. Die Größenänderung erfolgt beispielsweise über ImageMagick. Die Konvertierung von einem Format in ein anderes ImageMagick wird jedoch an externe Bibliotheken delegiert. Und wie Operationen in diesen externen Bibliotheken parallelisiert werden, ist ein separates Problem, das wir nicht verstanden haben.
Fazit
Vielleicht haben wir in dieser Version von Shrimp unser Demo-Projekt in einen akzeptablen Zustand gebracht. Wer sehen und experimentieren möchte, findet die Ausgangstexte von Shrimp auf
BitBucket oder
GitHub . Dort finden Sie auch die Docker-Datei, mit der Sie Shrimp für Ihre Experimente erstellen können.
Im Allgemeinen haben wir unsere Ziele erreicht, die wir uns mit dem Start dieses Demo-Projekts gesetzt haben. Eine Reihe von Ideen für die Weiterentwicklung von RESTinio und SObjectizer sind aufgetaucht, und einige von ihnen haben bereits ihre Verkörperung gefunden. Ob sich Shrimp irgendwo weiterentwickeln wird, hängt daher vollständig von Fragen und Wünschen ab. Wenn ja, kann sich Shrimp ausdehnen. Wenn nicht, bleibt Shrimp ein Demo-Projekt und ein Übungsfeld für das Experimentieren mit neuen Versionen von RESTinio und SObjectizer.
Abschließend möchte ich
Aensidhe meinen besonderen Dank für ihre Hilfe und ihren Rat
aussprechen , ohne die unsere Tänze mit einem Tamburin viel länger und trauriger wären.