RESTinio ist ein relativ kleines Projekt, bei dem es sich um einen asynchronen HTTP-Server handelt, der in C ++ - Anwendungen integriert ist. Sein charakteristisches Merkmal ist die weit verbreitete, man könnte sagen, weit verbreitete Verwendung von C ++ - Vorlagen. Sowohl in der Implementierung als auch in der öffentlichen API.
C ++ - Vorlagen in RESTinio werden so aktiv verwendet, dass der erste Artikel, der sich mit RESTinio auf Habr befasste, "Dreistöckige C ++ - Vorlagen bei der Implementierung eines eingebetteten asynchronen HTTP-Servers mit menschlichem Gesicht " hieß.
Dreistöckige Vorlagen. Und dies war im Allgemeinen keine Redewendung.
Und kürzlich haben wir RESTinio erneut aktualisiert. Um Version 0.5.1 um neue Funktionen zu erweitern, mussten wir die "Anzahl der Stockwerke" der Vorlagen noch erhöhen. Stellenweise sind C ++ - Vorlagen in RESTinio also bereits vierstöckig.

Und wenn sich jemand fragt, warum wir das gebraucht haben und wie wir die Vorlagen verwendet haben, dann bleiben Sie bei uns, es werden ein paar Details unter dem Schnitt sein. Es ist unwahrscheinlich, dass eingefleischte C ++ - Gurus etwas Neues für sich finden, aber weniger fortgeschrittene C ++ - Spitznamen können sehen, wie Vorlagen zum Einfügen / Entfernen von Funktionen verwendet werden. Fast in freier Wildbahn.
Verbindungsstatus-Listener
Die Hauptfunktion, für die Version 0.5.1 erstellt wurde, ist die Möglichkeit, den Benutzer darüber zu informieren, dass sich der Status der Verbindung zum HTTP-Server geändert hat. Beispielsweise ist der Client "abgefallen", und dies machte es unnötig, Anforderungen von diesem Client zu verarbeiten, die noch in der Schlange stehen.
Wir wurden manchmal nach dieser Funktion gefragt und jetzt haben unsere Hände ihre Implementierung erreicht. Aber seitdem Nicht jeder fragte nach dieser Funktion, es wurde angenommen, dass sie optional sein sollte: Wenn ein Benutzer sie benötigt, lassen Sie sie explizit einschließen, und der Rest sollte nichts für ihre Existenz in RESTinio bezahlen.
Und da die Hauptmerkmale des HTTP-Servers in RESTinio über "Merkmale" festgelegt werden, wurde beschlossen, das Abhören des Verbindungsstatus über die Servereigenschaften zu aktivieren / deaktivieren.
Wie legt ein Benutzer seinen eigenen Listener für den Verbindungsstatus fest?
Um Ihren Listener auf den Status von Verbindungen einzustellen , muss der Benutzer drei Schritte ausführen.
Schritt 1: Definieren Sie Ihre eigene Klasse, die eine nicht statische state_changed-Methode der folgenden Form haben sollte:
void state_changed( const restinio::connection_state::notice_t & notice) noexcept;
Zum Beispiel könnte es so etwas sein wie:
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_}; .... } ... };
Schritt 2: In den Eigenschaften des Servers müssen Sie ein typedef mit dem Namen connection_state_listener_t
, das sich auf den Namen des in Schritt 1 erstellten Typs beziehen soll:
struct my_traits : public restinio::default_traits_t { using connection_state_listener_t = my_state_listener; };
Dementsprechend sollten diese Eigenschaften beim Starten des HTTP-Servers verwendet werden:
restinio::run(restinio::on_thread_pool<my_traits>(8)...);
Schritt 3: Der Benutzer muss eine Instanz seines Listeners erstellen und diesen Zeiger über shared_ptr in den Serverparametern übergeben:
restinio::run( restinio::on_thread_pool<my_traits>(8) .port(8080) .address("localhost") .request_handler(...) .connection_state_listener(std::make_shared<my_state_listener>(...)) ) );
Wenn der Benutzer die Methode connection_state_listener
aufruft, wird beim Starten des HTTP-Servers eine Ausnahme ausgelöst: Der Norden kann nicht funktionieren, wenn der Benutzer den Statuslistener verwenden möchte, diesen Listener jedoch nicht angibt.
Und wenn Sie connection_state_listener_t nicht setzen?
Wenn der Benutzer den Namen connection_state_listener_t
in den Servereigenschaften festlegt, muss er die Methode connection_state_listener
aufrufen, um die Serverparameter festzulegen. Aber wenn der Benutzer connection_state_listener_t
nicht angibt?
In diesem Fall ist der Name connection_state_listener_t
weiterhin in den restinio::connection_state::noop_listener_t
vorhanden, dieser Name restinio::connection_state::noop_listener_t
auf den speziellen Typ restinio::connection_state::noop_listener_t
.
Tatsächlich passiert Folgendes: In RESTinio wird beim Definieren regulärer Merkmale der Wert connection_state_listener_t
festgelegt. So etwas wie:
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; }; }
Und wenn der Benutzer von restinio::default_traits_t
erbt, wird auch die Standarddefinition von connection_state_listener_t
geerbt. Wenn jedoch der neue Name connection_state_listener_t
in der Nachfolgeklasse definiert connection_state_listener_t
:
struct my_traits : public restinio::default_traits_t { using connection_state_listener_t = my_state_listener; ... };
dann verbirgt der neue Name die geerbte Definition für connection_state_listener_t
. Und wenn es keine neue Definition gibt, bleibt die alte Definition sichtbar.
Wenn der Benutzer keinen eigenen Wert für connection_state_listener_t
, verwendet RESTinio den Standardwert noop_listener_t
, der von RESTinio auf besondere Weise verarbeitet wird. Zum Beispiel:
- RESTinio speichert in diesem Fall shared_ptr überhaupt nicht für
connection_state_listener_t
. Dementsprechend ist ein Aufruf der Methode connection_state_listener
verboten (ein solcher Aufruf führt zu einem Fehler bei der Kompilierung). - RESTinio führt keine zusätzlichen Anrufe im Zusammenhang mit der Änderung des Verbindungsstatus durch.
Und wie all dies erreicht wird und weiter unten diskutiert wird.
Wie ist dies in RESTinio implementiert?
Im RESTinio-Code müssen Sie also überprüfen, welchen Wert die Definition von connection_state_listener_t
in den Servereigenschaften hat, und abhängig von diesem Wert:
- Speichern oder Nicht-Speichern der shared_ptr-Instanz für ein Objekt vom Typ
connecton_state_listener_t
; - Zulassen oder Verbieten von Aufrufen von
connection_state_listener
Methoden zum Festlegen von HTTP-Serverparametern; - Überprüfen Sie, ob ein aktueller Zeiger auf ein Objekt vom Typ
connection_state_listener_t
bevor Sie den Betrieb des HTTP-Servers starten. - Rufen Sie die Methode
state_changed
oder nicht, wenn sich der Status der Verbindung zum Client ändert.
Es wird auch zu den Randbedingungen hinzugefügt, die RESTinio noch als Bibliothek für C ++ 14 entwickelt. Daher können Sie die Funktionen von C ++ 17 in der Implementierung nicht verwenden (dasselbe gilt, wenn constexpr).
All dies wird durch einfache Tricks implementiert: Vorlagenklassen und ihre Spezialisierungen für den Typ restinio::connection_state::noop_listener_t
. Im Folgenden wird beispielsweise beschrieben, wie der Speicher von shared_ptr für ein Objekt vom Typ connection_state_listener_t
in Serverparametern ausgeführt wird. Erster Teil:
template< typename Listener > struct connection_state_listener_holder_t { ...
Hier wird eine Vorlagenstruktur definiert, die entweder einen nützlichen Inhalt hat oder nicht. Nur für den Typ noop_listener_t
hat es keinen nützlichen Inhalt.
Und Teil zwei:
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 > { ... };
Die Klasse, die die Parameter für den HTTP-Server enthält, wird von connection_state_listener_holder_t
geerbt. Daher zeigen die Serverparameter entweder shared_ptr für ein Objekt vom Typ connection_state_listener_t
an oder nicht.
Ich muss sagen, dass das Speichern oder Nicht-Speichern von shared_ptr in den Parametern Blumen sind. Die Beeren gingen jedoch verloren, als versucht wurde, die Methoden für die Arbeit mit dem basic_server_settings_t
in basic_server_settings_t
nur verfügbar zu machen, wenn connection_state_listener_t
sich von noop_listener_t
.
Idealerweise wollte ich den Compiler dazu bringen, sie überhaupt nicht zu sehen. Aber ich wurde gefoltert, um Bedingungen für std::enable_if
zu schreiben, um diese Methoden zu verbergen. Daher war es einfach auf das Hinzufügen von static_asser beschränkt:
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(); }
Es gab nur einen weiteren Moment, in dem ich das in C ++ bereute, wenn constexpr nicht dasselbe ist wie statisch, wenn in D. Und im Allgemeinen gibt es in C ++ 14 nichts Ähnliches :(
Hier können Sie auch die Verfügbarkeit der Methode ensure_valid_connection_state_listener
. Diese Methode wird im Konstruktor http_server_t
, um zu überprüfen, ob die http_server_t
alle erforderlichen Werte enthalten:
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() } {
Gleichzeitig wird innerhalb des ensure_valid_connection_state_listener
geerbte Methode ensure_valid_connection_state_listener
check_valid_connection_state_listener_pointer
, die aufgrund der Spezialisierung connection_state_listener_holder_t
entweder eine tatsächliche Prüfung durchführt oder nichts unternimmt.
Ähnliche Tricks wurden verwendet, um entweder den aktuellen state_changed
wenn der Benutzer den state_changed
wollte, oder nichts anderes aufzurufen.
Zuerst benötigen wir eine weitere Option 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 { } }; }
Im Gegensatz zu connection_state_listener_holder_t
, das zuvor gezeigt wurde und zum Speichern des Verbindungsstatus-Listeners in den Parametern des gesamten Servers (d. H. In Objekten vom Typ basic_server_settings_t
) verwendet wurde, wird dieser state_listener_holder_t
für ähnliche Zwecke verwendet, jedoch nicht in den Parametern des gesamten Servers, sondern eines separaten Verbindung:
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 >; ...
Hier gibt es zwei Funktionen.
Initialisieren Sie zunächst state_listener_holder_t
. Es wird entweder benötigt oder nicht. Aber nur state_listener_holder_t
weiß davon. Daher "zieht" der Konstruktor " state_listener_holder_t
" einfach den Konstruktor " state_listener_holder_t
, wie sie sagen, nur für den Fall:
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() }
Und der Konstruktor state_listener_holder_t
führt entweder die erforderlichen Aktionen aus oder führt überhaupt nichts aus (im letzteren Fall generiert der mehr oder weniger sinnvolle Compiler keinen Code, um state_listener_holder_t
zu initialisieren).
Zweitens ist es die state_listner_holder_t::call_state_listener
, die den state_changed
Aufruf an den state Listener state_changed
. Oder nicht, wenn es keinen Statuslistener gibt. Dieser call_state_listener
an Stellen call_state_listener
an denen RESTinio eine Änderung des Verbindungsstatus diagnostiziert. Wenn beispielsweise festgestellt wird, dass die Verbindung geschlossen wurde:
void close() { m_logger.trace( [&]{ return fmt::format( "[connection:{}] close", connection_id() ); } ); ...
Ein call_state_listener
wird an call_state_listener
, von dem ein notice_t
Objekt mit Verbindungsstatusinformationen zurückgegeben wird. Wenn es einen tatsächlichen Listener gibt, wird dieses Lambda tatsächlich aufgerufen und der von ihm zurückgegebene Wert wird an state_changed
.
Wenn jedoch kein Listener vorhanden ist, ist call_state_listener
leer und dementsprechend wird kein Lambda aufgerufen. Tatsächlich wirft der normale Compiler einfach alle Aufrufe an einen leeren call_state_listener
. In diesem Fall gibt es im generierten Code überhaupt nichts, was mit dem Verbindungsstatus zusammenhängt, auf den der Listener zugreift.
Auch IP-Blocker
In RESTinio-0.5.1 wurde zusätzlich zum Verbindungsstatus-Listener ein IP-Blocker hinzugefügt. Das heißt, Der Benutzer kann ein Objekt angeben, das RESTinio für jede neue eingehende Verbindung "zieht". Wenn der IP-Blocker angibt, dass Sie mit der Verbindung arbeiten können, startet RESTinio die übliche Wartung der neuen Verbindung (es liest und analysiert die Anforderung, ruft den Anforderungshandler auf, steuert Zeitüberschreitungen usw.). Aber wenn der IP-Blocker das Arbeiten mit der Verbindung verbietet, schließt RESTinio diese Verbindung dumm und macht nichts mehr damit.
Wie der Status-Listener ist der IP-Blocker eine optionale Funktion. Um den IP-Blocker verwenden zu können, müssen Sie ihn explizit aktivieren. Über die Eigenschaften des HTTP-Servers. Genau wie beim Verbindungsstatus-Listener. Die Implementierung der IP-Blocker-Unterstützung in RESTinio verwendet dieselben Techniken, die oben bereits beschrieben wurden. Daher werden wir uns nicht mit der Verwendung des IP-Blockers in RESTinio befassen. Stellen Sie sich stattdessen ein Beispiel vor, in dem sowohl der IP-Blocker als auch der Status-Listener dasselbe Objekt sind.
Analyse des Standardbeispiels ip_blocker
In Version 0.5.1 ist ein weiteres Beispiel in den Standard-RESTinio-Beispielen enthalten: ip_blocker . Dieses Beispiel zeigt, wie Sie die Anzahl gleichzeitiger Verbindungen zum Server von einer einzelnen IP-Adresse aus begrenzen können.
Dies erfordert nicht nur einen IP-Blocker, der das Akzeptieren von Verbindungen zulässt oder verbietet. Aber auch ein Listener für den Verbindungsstatus. Ein Listener wird benötigt, um die Momente des Erstellens und Schließens von Verbindungen zu verfolgen.
Gleichzeitig benötigen sowohl der IP-Blocker als auch der Listener denselben Datensatz. Daher besteht die einfachste Lösung darin, den IP-Blocker und den Listener zum selben Objekt zu machen.
Kein Problem, wir können dies leicht tun:
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:
Hier haben wir keine Vererbung von Schnittstellen oder Überschreibungen von geerbten virtuellen Methoden. Die einzige Voraussetzung für den Listener ist das Vorhandensein der Methode state_changed
. Diese Anforderung ist erfüllt.
Ebenso mit der einzigen Anforderung für einen IP-Blocker: Gibt es eine inspect
mit der erforderlichen Signatur? Da ist! Also ist alles in Ordnung.
Dann müssen noch die richtigen Eigenschaften für den HTTP-Server ermittelt werden:
struct my_traits_t : public restinio::default_traits_t { using logger_t = restinio::shared_ostream_logger_t;
Danach muss nur noch eine Instanz von blocker_t
und in den Parametern an den HTTP-Server übergeben werden:
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) ); } ) );
Fazit
Informationen zu C ++ - Vorlagen
Meiner Meinung nach sind C ++ - Vorlagen sogenannte Big Guns. Das heißt, Eine so leistungsstarke Funktion, dass Sie unwillkürlich darüber nachdenken müssen, wie und wie ihre Verwendung gerechtfertigt ist. Daher ist die moderne C ++ - Community wie in mehrere kriegführende Lager unterteilt.
Vertreter eines von ihnen halten sich lieber von Vorlagen fern. Da die Vorlagen komplex sind, erzeugen sie unlesbare Längen unlesbarer Fehlermeldungen, wodurch die Kompilierungszeit erheblich verlängert wird. Ganz zu schweigen von urbanen Legenden über das Aufblähen von Code und das Reduzieren der Leistung.
Vertreter eines anderen Lagers (wie ich) glauben, dass Vorlagen einer der mächtigsten Aspekte von C ++ sind. Es ist sogar möglich, dass Vorlagen einer der wenigen schwerwiegendsten Wettbewerbsvorteile von C ++ in der modernen Welt sind. Daher sind meiner Meinung nach die Zukunft von C ++ genau die Vorlagen. Einige der aktuellen Unannehmlichkeiten, die mit der weit verbreiteten Verwendung von Vorlagen verbunden sind (z. B. langwierige und ressourcenintensive Kompilierung oder nicht informative Fehlermeldungen), werden im Laufe der Zeit auf die eine oder andere Weise beseitigt.
Daher scheint es mir persönlich, dass sich der bei der Implementierung von RESTinio gewählte Ansatz, nämlich die weit verbreitete Verwendung von Vorlagen und die Angabe der Eigenschaften eines HTTP-Servers durch Eigenschaften, immer noch auszahlt. Dank dessen erhalten wir eine gute Anpassung an spezifische Bedürfnisse. Gleichzeitig zahlen wir im wahrsten Sinne des Wortes nicht für das, was wir nicht verwenden.
Andererseits scheint die Programmierung in C ++ - Vorlagen immer noch unangemessen kompliziert zu sein. Sie spüren es besonders, wenn Sie nicht ständig programmieren müssen, sondern zwischen verschiedenen Aktivitäten wechseln. Sie werden für ein paar Wochen vom Codieren abgelenkt, dann kehren Sie zurück und beginnen offen und spezifisch dumm zu sein, wenn nötig, eine Methode mit SFINAE auszublenden oder zu prüfen, ob eine Methode mit einer bestimmten Signatur auf dem Objekt vorhanden ist.
Es ist also gut, dass es in C ++ Vorlagen gibt. Es wäre sogar noch besser, wenn sie in einen Zustand versetzt würden, in dem selbst Starter wie ich problemlos C ++ - Vorlagen verwenden könnten, ohne alle 10 bis 15 Minuten die Referenz und den Stapelüberlauf untersuchen zu müssen.
Informationen zum aktuellen Status von RESTinio und zur zukünftigen Funktionalität von RESTinio. Und nicht nur RESTinio
Im Moment entwickelt sich RESTinio nach dem Prinzip "Wenn es Zeit gibt und es einen Wunsch gibt". Zum Beispiel hatten wir im Herbst 2018 und im Winter 2019 nicht viel Zeit für die Entwicklung von RESTinio. Sie beantworteten die Fragen der Benutzer, nahmen geringfügige Änderungen vor, aber für etwas mehr reichten unsere Ressourcen nicht aus.
Im späten Frühjahr 2019 war jedoch Zeit für RESTinio, und wir haben zuerst RESTinio 0.5.0 und dann 0.5.1 erstellt . Gleichzeitig war der Vorrat an Wunschliste für uns und andere erschöpft. Das heißt, Was wir selbst in RESTinio sehen wollten und was uns Benutzer erzählt haben, ist bereits in RESTinio.
Natürlich kann RESTinio mit viel mehr gefüllt werden. Aber was genau?
Und hier ist die Antwort sehr einfach: Nur das, was wir von RESTinio erwarten. Wenn Sie also etwas sehen möchten, das Sie in RESTinio benötigen, nehmen Sie sich Zeit, um uns davon zu erzählen (z. B. durch Probleme mit GitHub oder BitBucket , entweder über die Google-Gruppe oder direkt in den Kommentaren hier auf Habré). . Du wirst nichts sagen - du wirst nichts erhalten;)
Tatsächlich ist die gleiche Situation bei unseren anderen Projekten, insbesondere bei SObjectizer . Ihre neuen Versionen werden nach Erhalt der verständlichen Wunschliste veröffentlicht.
Nun, und zum Schluss möchte ich allen anbieten, die RESTinio noch nicht ausprobiert haben: Probieren Sie es aus kostenlos nicht verletzt. Plötzlich mag es. Und wenn es Ihnen nicht gefällt, teilen Sie was genau mit. Dies wird uns helfen, RESTinio noch komfortabler und funktionaler zu gestalten.