RESTinio es un proyecto relativamente pequeño, que es un servidor HTTP asíncrono integrado en aplicaciones C ++. Su rasgo característico es el uso generalizado, podría decirse, el uso generalizado de plantillas C ++. Tanto en implementación como en API pública.
Las plantillas C ++ en RESTinio se usan de manera tan activa que el primer artículo que habló sobre RESTinio en Habr se llamó " Plantillas C ++ de tres pisos en la implementación de un servidor HTTP asíncrono incrustado con rostro humano ".
Plantillas de tres pisos. Y esto, en general, no era una forma de hablar.
Y recientemente, una vez más actualizamos RESTinio, y para agregar una nueva funcionalidad a la versión 0.5.1, tuvimos que aumentar aún más el "número de pisos" de plantillas. Entonces, en algunos lugares, las plantillas C ++ en RESTinio ya son de cuatro pisos.

Y si alguien se pregunta por qué necesitábamos esto y cómo usamos las plantillas, quédese con nosotros, habrá algunos detalles debajo del corte. Es poco probable que los gurús inverterados de C ++ encuentren algo nuevo por sí mismos, pero los apodos de C ++ menos avanzados podrán ver cómo se usan las plantillas para insertar / eliminar piezas de funcionalidad. Casi en la naturaleza.
Escucha del estado de la conexión
La característica principal para la que se creó la versión 0.5.1 es la capacidad de informar al usuario que el estado de la conexión al servidor HTTP ha cambiado. Por ejemplo, el cliente "se cayó" y esto hizo innecesario procesar solicitudes de este cliente que todavía están esperando en la cola.
Algunas veces nos preguntaron sobre esta característica y ahora nuestras manos llegaron a su implementación. Pero desde Si no todos preguntaron acerca de esta característica, se pensó que debería ser opcional: si algún usuario la necesita, deje que la incluya explícitamente, y el resto no debería pagar nada por su existencia en RESTinio.
Y dado que las características principales del servidor HTTP en RESTinio se establecen a través de "rasgos", se decidió habilitar / deshabilitar la escucha en el estado de las conexiones a través de las propiedades del servidor.
¿Cómo configura un usuario su propio oyente para el estado de conexión?
Para configurar su escucha para el estado de las conexiones, el usuario debe realizar tres pasos.
Paso 1: defina su propia clase, que debe tener un método de cambio de estado no estático de la siguiente forma:
void state_changed( const restinio::connection_state::notice_t & notice) noexcept;
Por ejemplo, podría ser algo como:
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_}; .... } ... };
Paso 2: dentro de las propiedades del servidor, debe definir un typedef llamado connection_state_listener_t
, que debe hacer referencia al nombre del tipo creado en el paso 1:
struct my_traits : public restinio::default_traits_t { using connection_state_listener_t = my_state_listener; };
En consecuencia, estas propiedades deben usarse al iniciar el servidor HTTP:
restinio::run(restinio::on_thread_pool<my_traits>(8)...);
Paso # 3: el usuario debe crear una instancia de su escucha y pasar este puntero a través de shared_ptr en los parámetros del servidor:
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 el usuario no realiza una llamada al método connection_state_listener
, se lanzará una excepción al iniciar el servidor HTTP: el norte no puede funcionar si el usuario desea utilizar el escucha de estado pero no especifica este escucha.
¿Y si no establece connection_state_listener_t?
Si el usuario establece el nombre connection_state_listener_t
en las propiedades del servidor, debe llamar al método connection_state_listener
para establecer los parámetros del servidor. Pero si el usuario no especifica connection_state_listener_t
?
En este caso, el nombre connection_state_listener_t
seguirá estando presente en las propiedades del servidor, pero este nombre apuntará al tipo especial restinio::connection_state::noop_listener_t
.
De hecho, sucede lo siguiente: en RESTinio, al definir rasgos regulares, se establece connection_state_listener_t
. Algo como:
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; }; }
Y cuando el usuario hereda de restinio::default_traits_t
, la definición estándar de connection_state_listener_t
también se hereda. Pero si el nuevo nombre connection_state_listener_t
define en la clase sucesora:
struct my_traits : public restinio::default_traits_t { using connection_state_listener_t = my_state_listener; ... };
entonces el nuevo nombre oculta la definición heredada de connection_state_listener_t
. Y si no hay una nueva definición, entonces la vieja definición permanece visible.
Entonces, si el usuario no define su propio valor para connection_state_listener_t
, RESTinio usará el valor predeterminado, noop_listener_t
, que RESTinio procesa de manera especial. Por ejemplo:
- RESTinio no almacena shared_ptr en absoluto en este caso para
connection_state_listener_t
. Y, en consecuencia, está prohibida una llamada al método connection_state_listener
(dicha llamada dará lugar a un error en tiempo de compilación); - RESTinio no realiza ninguna llamada adicional relacionada con el cambio del estado de la conexión.
Y solo acerca de cómo se logra todo esto y se discutirá a continuación.
¿Cómo se implementa esto en RESTinio?
Entonces, en el código RESTinio, debe verificar qué valor tiene la definición de connection_state_listener_t
en las propiedades del servidor y, dependiendo de este valor:
- para almacenar o no almacenar la instancia shared_ptr para un objeto de tipo
connecton_state_listener_t
; - permitir o prohibir llamadas a métodos
connection_state_listener
para establecer los parámetros del servidor HTTP; - verificar o no verificar la presencia de un puntero actual a un objeto de tipo
connection_state_listener_t
antes de iniciar la operación del servidor HTTP; - realizar o no realizar llamadas al método
state_changed
cuando state_changed
el estado de la conexión con el cliente.
También se agrega a las condiciones de contorno que RESTinio todavía está desarrollando como una biblioteca para C ++ 14, por lo tanto, no puede usar las capacidades de C ++ 17 en la implementación (lo mismo si constexpr).
Todo esto se implementa a través de trucos simples: clases de plantillas y sus especializaciones para el tipo restinio::connection_state::noop_listener_t
. Por ejemplo, así es como se realiza el almacenamiento shared_ptr para un objeto de tipo connection_state_listener_t
en los parámetros del servidor. Primera parte:
template< typename Listener > struct connection_state_listener_holder_t { ...
Aquí se define una estructura de plantilla que tiene un contenido útil o no. Solo para el tipo noop_listener_t
, no tiene contenido útil.
Y la segunda parte:
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 clase que contiene los parámetros para el servidor HTTP se hereda de connection_state_listener_holder_t
. Por lo tanto, los parámetros del servidor muestran shared_ptr para un objeto de tipo connection_state_listener_t
, o no lo hace.
Debo decir que almacenar o no almacenar shared_ptr en los parámetros son flores. Pero las bayas desaparecieron al intentar hacer que los métodos destinados a trabajar con el escucha de estado en basic_server_settings_t
estén disponibles solo si connection_state_listener_t
es diferente de noop_listener_t
.
Idealmente, quería hacer que el compilador "no los vea" en absoluto. Pero fui torturado para escribir condiciones para std::enable_if
para ocultar estos métodos. Por lo tanto, simplemente se limitó a agregar 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(); }
Hubo otro momento en el que lamenté que en C ++ si constexpr no es lo mismo que static en D. Y en general en C ++ 14 no hay nada similar :(
Aquí también puede ver la disponibilidad del método ensure_valid_connection_state_listener
. Se llama a este método en el constructor http_server_t
para verificar que los parámetros del servidor contienen todos los valores necesarios:
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() } {
Al mismo tiempo, dentro del ensure_valid_connection_state_listener
método ensure_valid_connection_state_listener
heredado de connection_state_listener_holder_t
, que, debido a la especialización connection_state_listener_holder_t
, hace una comprobación real o no hace nada.
Se usaron trucos similares para llamar al estado actual state_changed
si el usuario quería usar el escucha de estado o no llamar a nada de otra manera.
Primero, necesitamos otra opción 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 { } }; }
A diferencia de connection_state_listener_holder_t
, que se mostró anteriormente y que se usó para almacenar el escucha de estado de conexión en los parámetros de todo el servidor (es decir, en objetos del tipo basic_server_settings_t
), este state_listener_holder_t
se usará para fines similares, pero no en los parámetros de todo el servidor, sino de un servidor separado conexión:
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 >; ...
Hay dos características aquí.
Primero, inicializando state_listener_holder_t
. Es necesario o no. Pero solo state_listener_holder_t
sabe. Por lo tanto, el constructor connection_settings_t
simplemente "tira" del constructor state_listener_holder_t
, como dicen, por si acaso:
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() }
Y el state_listener_holder_t
constructor state_listener_holder_t
realiza las acciones necesarias o no hace nada (en el último caso, el compilador más o menos sensible no generará ningún código para inicializar state_listener_holder_t
).
En segundo lugar, es el state_listner_holder_t::call_state_listener
, que hace que la llamada state_changed
al escucha de estado. O no, si no hay escucha de estado. Este call_state_listener
en lugares donde RESTinio diagnostica un cambio en el estado de la conexión. Por ejemplo, cuando se detecta que la conexión se ha cerrado:
void close() { m_logger.trace( [&]{ return fmt::format( "[connection:{}] close", connection_id() ); } ); ...
Se pasa un call_state_listener
a call_state_listener
, desde el cual se notice_t
un objeto notice_t
con información de estado de conexión. Si hay un oyente real, entonces se llamará a esta lambda, y el valor devuelto se pasará a state_changed
.
Sin embargo, si no hay escucha, call_state_listener
estará vacío y, en consecuencia, no se llamará a lambda. De hecho, el compilador normal simplemente lanza todas las llamadas a un call_state_listener
vacío. Y en este caso, en el código generado no habrá nada relacionado con el acceso al estado de conexión al oyente.
También bloqueador de IP
En RESTinio-0.5.1, además de la escucha del estado de la conexión, se agregó un bloqueador de IP . Es decir el usuario puede especificar un objeto que RESTinio "extraerá" para cada nueva conexión entrante. Si el bloqueador de IP dice que puede trabajar con la conexión, RESTinio inicia el mantenimiento habitual de la nueva conexión (lee y analiza la solicitud, llama al manejador de solicitudes, controla los tiempos de espera, etc.). Pero si el bloqueador de IP prohíbe trabajar con la conexión, RESTinio cierra estúpidamente esta conexión y no hace nada más con ella.
Al igual que la escucha de estado, el bloqueador de IP es una característica opcional. Para usar el bloqueador de IP, debe habilitarlo explícitamente. A través de las propiedades del servidor HTTP. Al igual que con el oyente del estado de la conexión. Y la implementación del soporte del bloqueador de IP en RESTinio utiliza las mismas técnicas que ya se han descrito anteriormente. Por lo tanto, no nos detendremos en cómo se usa el bloqueador de IP dentro de RESTinio. En su lugar, considere un ejemplo en el que tanto el bloqueador de IP como el oyente de estado son el mismo objeto.
Análisis del ejemplo estándar ip_blocker
En la versión 0.5.1, se incluye otro ejemplo en los ejemplos estándar de RESTinio: ip_blocker . Este ejemplo demuestra cómo puede limitar el número de conexiones simultáneas al servidor desde una sola dirección IP.
Esto requerirá no solo un bloqueador de IP, que permitirá o prohibirá la aceptación de conexiones. Pero también un oyente para el estado de la conexión. Se necesita un oyente para rastrear los momentos de creación y cierre de conexiones.
Al mismo tiempo, tanto el bloqueador de IP como el oyente necesitarán el mismo conjunto de datos. Por lo tanto, la solución más simple es hacer que el bloqueador de IP y el oyente sean el mismo objeto.
No hay problema, podemos hacer esto fácilmente:
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:
Aquí no tenemos herencia de ninguna interfaz o anulación de métodos virtuales heredados. El único requisito para el oyente es la presencia del método state_changed
. Este requisito se cumple.
Del mismo modo, con el único requisito para un bloqueador de IP: ¿hay inspect
método de inspect
con la firma requerida? Hay! Entonces todo está bien.
Luego queda determinar las propiedades correctas para el servidor HTTP:
struct my_traits_t : public restinio::default_traits_t { using logger_t = restinio::shared_ostream_logger_t;
Después de eso, solo queda crear una instancia de blocker_t
y pasarla en los parámetros al servidor 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) ); } ) );
Conclusión
Acerca de las plantillas de C ++
En mi opinión, las plantillas de C ++ son lo que se llaman armas demasiado grandes. Es decir característica tan poderosa que involuntariamente tiene que pensar cómo y cómo se justifica su uso. Por lo tanto, la comunidad moderna de C ++ está dividida en varios campos de guerra.
Los representantes de uno de ellos prefieren mantenerse alejados de las plantillas. Como las plantillas son complejas, generan longitudes ilegibles de hojas de mensajes de error ilegibles, lo que aumenta significativamente el tiempo de compilación. Sin mencionar las leyendas urbanas sobre el código hinchado y la reducción del rendimiento.
Los representantes de otro campo (como yo) creen que las plantillas son uno de los aspectos más poderosos de C ++. Incluso es posible que las plantillas sean una de las pocas ventajas competitivas más serias de C ++ en el mundo moderno. Por lo tanto, en mi opinión, el futuro de C ++ es precisamente las plantillas. Y algunos de los inconvenientes actuales asociados con el uso generalizado de plantillas (como la compilación larga y de uso intensivo de recursos o los mensajes de error no informativos) se eliminarán de una forma u otra con el tiempo.
Por lo tanto, me parece personalmente que el enfoque elegido durante la implementación de RESTinio, a saber, el uso generalizado de plantillas y la configuración de las características de un servidor HTTP a través de las propiedades, todavía vale la pena. Gracias a esto, obtenemos una buena personalización para necesidades específicas. Y al mismo tiempo, en sentido literal, no pagamos por lo que no usamos.
Sin embargo, por otro lado, parece que la programación en plantillas C ++ sigue siendo irrazonablemente complicada. Lo sientes especialmente cuando tienes que programar no constantemente, sino cuando cambias entre diferentes actividades. Se distraerá durante un par de semanas de la codificación, luego volverá y comenzará a ser abierta y específicamente estúpido si es necesario, ocultará algún método usando SFINAE o comprobará la existencia de un método con una cierta firma en el objeto.
Por lo tanto, es bueno que haya plantillas en C ++. Sería aún mejor si se llevaran a tal estado que incluso los principiantes como yo pudieran usar fácilmente las plantillas de C ++ sin tener que estudiar cppreference y stackoverflow cada 10-15 minutos.
Acerca del estado actual de RESTinio y la funcionalidad futura de RESTinio. Y no solo RESTinio
En este momento, RESTinio se está desarrollando según el principio de "cuando hay tiempo y hay un deseo". Por ejemplo, en el otoño de 2018 y en el invierno de 2019, no tuvimos mucho tiempo para el desarrollo de RESTinio. Respondieron las preguntas de los usuarios, hicieron cambios menores, pero para algo más nuestros recursos no fueron suficientes.
Pero a fines de la primavera de 2019, hubo tiempo para RESTinio, y primero hicimos RESTinio 0.5.0 , y luego 0.5.1 . Al mismo tiempo, el suministro de nuestra lista de deseos y la de otros se agotó. Es decir lo que nosotros mismos queríamos ver en RESTinio y lo que los usuarios nos contaron, ya está en RESTinio.
Obviamente, RESTinio se puede llenar con mucho más. ¿Pero qué exactamente?
Y aquí la respuesta es muy simple: solo lo que se nos pide que ingresemos a RESTinio. Por lo tanto, si desea ver algo que necesita en RESTinio, tómese el tiempo para contarnos al respecto (por ejemplo, a través de problemas en GitHub o BitBucket , ya sea a través del grupo de Google o directamente en los comentarios aquí en Habré) . No dirás nada, no recibirás nada;)
En realidad, la misma situación es con nuestros otros proyectos, en particular con SObjectizer . Sus nuevas versiones se lanzarán al recibir la lista de deseos inteligible.
Bueno, y finalmente, me gustaría ofrecer a todos los que aún no han probado RESTinio: pruébelo gratis No duele. De repente me gusta. Y si no te gusta, comparte qué exactamente. Esto nos ayudará a hacer que RESTinio sea aún más conveniente y funcional.