RESTinio est un projet relativement petit, qui est un serveur HTTP asynchrone intégré aux applications C ++. Sa caractéristique est l'utilisation répandue, pourrait-on dire, des modèles C ++. Tant dans l'implémentation que dans l'API publique.
Les modèles C ++ dans RESTinio sont utilisés si activement que le premier article qui parlait de RESTinio sur Habr s'appelait " Modèles C ++ à trois étages dans la mise en œuvre d'un serveur HTTP asynchrone intégré à visage humain ".
Modèles à trois étages. Et cela, en général, n'était pas une figure de style.
Et récemment, nous avons à nouveau mis à jour RESTinio, et pour ajouter de nouvelles fonctionnalités à la version 0.5.1, nous avons dû augmenter encore le "nombre d'étages" des modèles. Donc, par endroits, les modèles C ++ dans RESTinio sont déjà à quatre étages.

Et si quelqu'un se demande pourquoi nous en avions besoin et comment nous avons utilisé les modèles, alors restez avec nous, il y aura quelques détails sous la coupe. Les gourous invétérés du C ++ ne trouveront probablement rien de nouveau pour eux-mêmes, mais les surnoms C ++ moins avancés pourront voir comment les modèles sont utilisés pour insérer / supprimer des fonctionnalités. Presque à l'état sauvage.
Écouteur d'état de connexion
La principale fonctionnalité pour laquelle la version 0.5.1 a été créée est la possibilité d'informer l'utilisateur que l'état de la connexion au serveur HTTP a changé. Par exemple, le client est "tombé" et il n'a donc pas été nécessaire de traiter les demandes de ce client qui sont toujours en attente.
On nous a parfois posé des questions sur cette fonctionnalité et maintenant nos mains ont atteint sa mise en œuvre. Mais depuis tout le monde n'a pas posé de questions sur cette fonctionnalité, on a pensé qu'elle devrait être facultative: si un utilisateur en a besoin, alors laissez-la l'inclure explicitement, et tout le reste ne devrait rien payer pour son existence dans RESTinio.
Et comme les principales caractéristiques du serveur HTTP dans RESTinio sont définies via des "traits", il a été décidé d'activer / désactiver l'écoute sur l'état des connexions via les propriétés du serveur.
Comment un utilisateur définit-il son propre écouteur pour l'état de la connexion?
Afin de définir votre écouteur pour l'état des connexions, l' utilisateur doit effectuer trois étapes.
Étape # 1: définissez votre propre classe, qui devrait avoir une méthode state_changed non statique de la forme suivante:
void state_changed( const restinio::connection_state::notice_t & notice) noexcept;
Par exemple, cela pourrait être quelque chose comme:
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_}; .... } ... };
Étape # 2: à l'intérieur des propriétés du serveur, vous devez définir un typedef appelé connection_state_listener_t
, qui devrait faire référence au nom du type créé à l'étape # 1:
struct my_traits : public restinio::default_traits_t { using connection_state_listener_t = my_state_listener; };
Par conséquent, ces propriétés doivent être utilisées lors du démarrage du serveur HTTP:
restinio::run(restinio::on_thread_pool<my_traits>(8)...);
Étape # 3: l'utilisateur doit créer une instance de son écouteur et passer ce pointeur via shared_ptr dans les paramètres du serveur:
restinio::run( restinio::on_thread_pool<my_traits>(8) .port(8080) .address("localhost") .request_handler(...) .connection_state_listener(std::make_shared<my_state_listener>(...)) ) );
Si l'utilisateur n'appelle pas la méthode connection_state_listener
, une exception sera levée lors du démarrage du serveur HTTP: le nord ne peut pas fonctionner si l'utilisateur veut utiliser l'écouteur d'état mais ne spécifie pas cet écouteur.
Et si vous ne définissez pas connection_state_listener_t?
Si l'utilisateur définit le nom connection_state_listener_t
dans les propriétés du serveur, il doit alors appeler la méthode connection_state_listener
pour définir les paramètres du serveur. Mais si l'utilisateur ne spécifie pas connection_state_listener_t
?
Dans ce cas, le nom connection_state_listener_t
sera toujours présent dans les propriétés du serveur, mais ce nom pointera vers le type spécial restinio::connection_state::noop_listener_t
.
En fait, ce qui suit se produit: dans RESTinio, lors de la définition de traits réguliers, la valeur connection_state_listener_t
est définie. Quelque chose comme:
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; }; }
Et lorsque l'utilisateur hérite de restinio::default_traits_t
, la définition standard de connection_state_listener_t
est également héritée. Mais si le nouveau nom connection_state_listener_t
défini dans la classe successeur:
struct my_traits : public restinio::default_traits_t { using connection_state_listener_t = my_state_listener; ... };
puis le nouveau nom masque la définition héritée de connection_state_listener_t
. Et s'il n'y a pas de nouvelle définition, l'ancienne définition reste visible.
Donc, si l'utilisateur ne définit pas sa propre valeur pour connection_state_listener_t
, alors RESTinio utilisera la valeur par défaut, noop_listener_t
, qui est traitée par RESTinio d'une manière spéciale. Par exemple:
- RESTinio ne stocke pas du tout shared_ptr dans ce cas pour
connection_state_listener_t
. Et, en conséquence, un appel à la méthode connection_state_listener
est interdit (un tel appel entraînera une erreur de compilation); - RESTinio ne fait aucun appel supplémentaire lié à la modification de l'état de la connexion.
Et à peu près comment tout cela est réalisé et sera discuté ci-dessous.
Comment est-ce implémenté dans RESTinio?
Ainsi, dans le code RESTinio, vous devez vérifier la valeur de la définition de connection_state_listener_t
dans les propriétés du serveur et, en fonction de cette valeur:
- stocker ou ne pas stocker l'instance shared_ptr pour un objet de type
connecton_state_listener_t
; - autoriser ou interdire les appels aux méthodes
connection_state_listener
pour définir les paramètres du serveur HTTP; - vérifier ou non la présence d'un pointeur en cours sur un objet de type
connection_state_listener_t
avant de démarrer le fonctionnement du serveur HTTP; - effectuer ou non des appels à la méthode
state_changed
lorsque l'état de la connexion au client change.
Il est également ajouté aux conditions aux limites que RESTinio développe toujours en tant que bibliothèque pour C ++ 14, par conséquent, vous ne pouvez pas utiliser les capacités de C ++ 17 dans l'implémentation (la même chose si constexpr).
Tout cela est implémenté à travers des astuces simples: les classes de modèles et leurs spécialisations pour le type restinio::connection_state::noop_listener_t
. Par exemple, voici comment le stockage shared_ptr est effectué pour un objet de type connection_state_listener_t
dans les paramètres du serveur. Première partie:
template< typename Listener > struct connection_state_listener_holder_t { ...
Une structure de modèle est définie ici qui a un contenu utile ou non. Juste pour le type noop_listener_t
, il n'a pas de contenu utile.
Et la deuxième partie:
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 > { ... };
La classe qui contient les paramètres du serveur HTTP est héritée de connection_state_listener_holder_t
. Ainsi, les paramètres du serveur affichent shared_ptr pour un objet de type connection_state_listener_t
, ou ce n'est pas le cas.
Je dois dire que stocker ou ne pas stocker shared_ptr dans les paramètres sont des fleurs. Mais les baies sont allées en essayant de rendre les méthodes destinées à travailler avec l'écouteur d'état dans basic_server_settings_t
disponibles uniquement si connection_state_listener_t
est différent de noop_listener_t
.
Idéalement, je voulais que le compilateur «ne les voie pas» du tout. Mais j'ai été torturé d'écrire des conditions pour std::enable_if
afin de cacher ces méthodes. Par conséquent, il était simplement limité à l'ajout de static_asser:
Derived & connection_state_listener( std::shared_ptr< typename Traits::connection_state_listener_t > listener ) & { static_assert( has_actual_connection_state_listener, "connection_state_listener(listener) can't be used " "for the default connection_state::noop_listener_t" ); this->m_connection_state_listener = std::move(listener); return reference_to_derived(); } Derived && connection_state_listener( std::shared_ptr< typename Traits::connection_state_listener_t > listener ) && { return std::move(this->connection_state_listener(std::move(listener))); } const std::shared_ptr< typename Traits::connection_state_listener_t > & connection_state_listener() const noexcept { static_assert( has_actual_connection_state_listener, "connection_state_listener() can't be used " "for the default connection_state::noop_listener_t" ); return this->m_connection_state_listener; } void ensure_valid_connection_state_listener() { this->check_valid_connection_state_listener_pointer(); }
Il y a eu juste un autre moment où j'ai regretté qu'en C ++ si constexpr n'est pas la même chose que statique si en D. Et en général en C ++ 14 il n'y a rien de similaire :(
Ici, vous pouvez également voir la disponibilité de la méthode ensure_valid_connection_state_listener
. Cette méthode est appelée dans le constructeur http_server_t
pour vérifier que les paramètres du serveur contiennent toutes les valeurs nécessaires:
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() } {
Dans le même temps, à l'intérieur de la ensure_valid_connection_state_listener
méthode ensure_valid_connection_state_listener
héritée de connection_state_listener_holder_t
est check_valid_connection_state_listener_pointer
, ce qui, en raison de la spécialisation connection_state_listener_holder_t
, effectue une vérification réelle ou ne fait rien.
Des astuces similaires ont été utilisées pour appeler le state_changed
actuel si l'utilisateur voulait utiliser l'écouteur d'état, ou pour ne rien appeler autrement.
Tout d'abord, nous avons besoin d'une autre 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 { } }; }
Contrairement à connection_state_listener_holder_t
, qui a été montré précédemment et qui a été utilisé pour stocker l'écouteur d'état de connexion dans les paramètres de l'ensemble du serveur (c'est-à-dire, dans des objets de type basic_server_settings_t
), cet state_listener_holder_t
sera utilisé à des fins similaires, mais pas dans les paramètres de l'ensemble du serveur, mais d'un serveur séparé connexion:
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 >; ...
Il y a deux fonctionnalités ici.
Tout d'abord, initialiser state_listener_holder_t
. Il est nécessaire ou non. Mais seul state_listener_holder_t
sait. Par conséquent, le constructeur connection_settings_t
«tire» simplement le constructeur state_listener_holder_t
, comme on dit, juste au cas où:
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() }
Et le constructeur state_listener_holder_t
effectue les actions nécessaires ou ne fait rien du tout (dans ce dernier cas, le compilateur plus ou moins sensible ne générera aucun code pour initialiser state_listener_holder_t
).
Deuxièmement, c'est la state_listner_holder_t::call_state_listener
, qui effectue l'appel state_changed
à l'écouteur d'état. Ou pas, s'il n'y a pas d'auditeur d'état. Ce call_state_listener
aux endroits où RESTinio diagnostique un changement d'état de connexion. Par exemple, lorsqu'il est détecté que la connexion a été fermée:
void close() { m_logger.trace( [&]{ return fmt::format( "[connection:{}] close", connection_id() ); } ); ...
Un call_state_listener
est transmis à call_state_listener
, à partir duquel un objet notice_t
avec des informations sur l'état de la connexion est renvoyé. S'il y a un écouteur réel, alors ce lambda sera en effet appelé, et la valeur retournée par lui sera passée à state_changed
.
Cependant, s'il n'y a pas d'écouteur, alors call_state_listener
sera vide et, par conséquent, aucun lambda ne sera appelé. En fait, le compilateur normal envoie simplement tous les appels à un call_state_listener
vide. Et dans ce cas, dans le code généré, il n'y aura rien du tout lié à l'état de connexion auquel l'auditeur accède.
Également bloqueur IP
Dans RESTinio-0.5.1, en plus de l'écouteur d'état de connexion, une chose telle qu'un bloqueur IP a été ajoutée. C'est-à-dire l'utilisateur peut spécifier un objet que RESTinio «tirera» pour chaque nouvelle connexion entrante. Si le bloqueur IP dit que vous pouvez travailler avec la connexion, alors RESTinio démarre la maintenance habituelle de la nouvelle connexion (il lit et analyse la demande, appelle le gestionnaire de demandes, contrôle les délais, etc.). Mais si le bloqueur IP interdit de travailler avec la connexion, alors RESTinio ferme bêtement cette connexion et ne fait plus rien avec elle.
Comme l'écouteur d'état, le bloqueur IP est une fonctionnalité facultative. Pour utiliser le bloqueur IP, vous devez l'activer explicitement. Grâce aux propriétés du serveur HTTP. Tout comme avec l'écouteur d'état de connexion. Et l'implémentation de la prise en charge du bloqueur IP dans RESTinio utilise les mêmes techniques qui ont déjà été décrites ci-dessus. Par conséquent, nous ne nous attarderons pas sur la façon dont le bloqueur IP est utilisé dans RESTinio. Prenons plutôt un exemple dans lequel le bloqueur IP et l'écouteur d'état sont le même objet.
Analyse de l'exemple standard ip_blocker
Dans la version 0.5.1, un autre exemple est inclus dans les exemples RESTinio standard: ip_blocker . Cet exemple montre comment vous pouvez limiter le nombre de connexions simultanées au serveur à partir d'une seule adresse IP.
Cela nécessitera non seulement un bloqueur IP, qui permettra ou interdira l'acceptation des connexions. Mais aussi un écouteur pour l'état de la connexion. Un écouteur est nécessaire pour suivre les moments de création et de fermeture de connexions.
En même temps, le bloqueur IP et l'auditeur auront besoin du même ensemble de données. Par conséquent, la solution la plus simple consiste à faire du bloqueur IP et de l'écouteur le même objet.
Pas de problème, nous pouvons facilement le faire:
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:
Ici, nous n'avons aucun héritage d'aucune interface ni substitution de méthodes virtuelles héritées. La seule condition requise pour l'écouteur est la présence de la méthode state_changed
. Cette exigence est satisfaite.
De même, avec la seule exigence pour un bloqueur IP: existe-t-il une méthode d' inspect
avec la signature requise? Voilà! Donc tout va bien.
Il reste ensuite à déterminer les propriétés correctes pour le serveur HTTP:
struct my_traits_t : public restinio::default_traits_t { using logger_t = restinio::shared_ostream_logger_t;
Après cela, il ne reste plus qu'à créer une instance de blocker_t
et à la passer dans les paramètres au serveur HTTP:
auto blocker = std::make_shared<blocker_t>(); restinio::run( ioctx, restinio::on_thread_pool<my_traits_t>( std::thread::hardware_concurrency() ) .port( 8080 ) .address( "localhost" ) .connection_state_listener( blocker ) .ip_blocker( blocker ) .max_pipelined_requests( 4 ) .handle_request_timeout( std::chrono::seconds{20} ) .request_handler( [&ioctx](auto req) { return handler( ioctx, std::move(req) ); } ) );
Conclusion
À propos des modèles C ++
À mon avis, les modèles C ++ sont ce qu'on appelle des pistolets trop gros. C'est-à-dire fonctionnalité si puissante que vous devez involontairement réfléchir à la manière et à la manière dont son utilisation est justifiée. Par conséquent, la communauté C ++ moderne est comme divisée en plusieurs camps en guerre.
Les représentants de l'un d'entre eux préfèrent rester à l'écart des modèles. Les modèles étant complexes, ils génèrent des longueurs illisibles de feuilles de messages d'erreur illisibles, ce qui augmente considérablement le temps de compilation. Sans parler des légendes urbaines sur le ballonnement du code et la réduction des performances.
Les représentants d'un autre camp (comme moi) pensent que les modèles sont l'un des aspects les plus puissants du C ++. Il est même possible que les modèles soient l'un des rares avantages compétitifs les plus sérieux du C ++ dans le monde moderne. Par conséquent, à mon avis, l'avenir du C ++ est précisément les modèles. Et certains des inconvénients actuels associés à l'utilisation généralisée des modèles (comme la compilation longue et gourmande en ressources ou les messages d'erreur non informatifs) seront éliminés d'une manière ou d'une autre au fil du temps.
Par conséquent, il me semble personnellement que l'approche choisie lors de la mise en œuvre de RESTinio, à savoir l'utilisation généralisée de modèles et la spécification des caractéristiques d'un serveur HTTP via les propriétés, porte toujours ses fruits. Grâce à cela, nous obtenons une bonne personnalisation pour des besoins spécifiques. Et en même temps, au sens littéral, nous ne payons pas ce que nous n'utilisons pas.
Cependant, d'un autre côté, il semble que la programmation dans les modèles C ++ soit encore excessivement compliquée. Vous le ressentez particulièrement lorsque vous devez programmer non pas constamment, mais lorsque vous basculez entre différentes activités. Vous serez distrait pendant quelques semaines du codage, puis vous reviendrez et commencerez ouvertement et spécifiquement stupide si nécessaire, masquerez une méthode en utilisant SFINAE ou vérifiez si un objet avec une certaine signature a une méthode.
C'est donc bien qu'il y ait des modèles en C ++. Ce serait encore mieux s'ils étaient amenés à un état tel que même des démarreurs comme moi pourraient facilement utiliser des modèles C ++ sans avoir à étudier cppreference et stackoverflow toutes les 10-15 minutes.
À propos de l'état actuel de RESTinio et des fonctionnalités futures de RESTinio. Et pas seulement RESTinio
En ce moment, RESTinio se développe sur le principe "quand il y a du temps et qu'il y a une liste de souhaits". Par exemple, à l'automne 2018 et à l'hiver 2019, nous n'avons pas eu beaucoup de temps pour le développement de RESTinio. Ils ont répondu aux questions des utilisateurs, ont apporté des modifications mineures, mais pour quelque chose de plus, nos ressources n'étaient pas suffisantes.
Mais à la fin du printemps 2019, il était temps pour RESTinio, et nous avons d'abord créé RESTinio 0.5.0 , puis 0.5.1 . Dans le même temps, l'offre de notre liste de souhaits et de celle des autres a été épuisée. C'est-à-dire ce que nous voulions nous-mêmes voir dans RESTinio et ce que les utilisateurs nous ont dit, est déjà dans RESTinio.
De toute évidence, RESTinio peut être rempli de beaucoup plus. Mais quoi exactement?
Et ici la réponse est très simple: seulement ce qu'on nous demande d'entrer dans RESTinio. Par conséquent, si vous voulez voir quelque chose dont vous avez besoin dans RESTinio, prenez le temps de nous en parler (par exemple, via des problèmes sur GitHub ou BitBucket , soit via le groupe Google , soit directement dans les commentaires ici sur Habré) . Vous ne direz rien - vous ne recevrez rien;)
En fait, la même situation est avec nos autres projets, en particulier avec SObjectizer . Leurs nouvelles versions seront publiées à la réception d'une liste de souhaits intelligible.
Et enfin, je voudrais offrir à tous ceux qui n'ont pas encore essayé RESTinio: essayez-le gratuitement pas mal. Tout à coup, j'aime ça. Et si vous ne l'aimez pas, partagez quoi exactement. Cela nous aidera à rendre RESTinio encore plus pratique et fonctionnel.