RESTinio is a relatively small project, which is an asynchronous HTTP server embedded in C ++ applications. Its characteristic feature is the wide, one might say, universal application of C ++ templates. Both in implementation and in public API.
C ++ templates in RESTinio are used so actively that the first article, which told about RESTinio on Habré, was called " Three-storey C ++ templates in the implementation of an embedded asynchronous HTTP server with a human face ".
Three-story patterns. And this, in general, was not a figure of speech.
And recently, we once again updated RESTinio, and in order to add new functionality to version 0.5.1, we had to make the number of storeys higher. So in some places C ++ templates in RESTinio are already four-story.
And if someone wonders why it took us and how we used the templates, then stay with us, under the cut there will be some details. Older C ++ gurus are unlikely to find anything new for themselves, but less advanced C ++ nicknames will be able to look at how templates are used to insert / remove pieces of functionality. Almost in the "wild".
The main feature for which version 0.5.1 was created is the ability to inform the user that the connection status to the HTTP server has changed. For example, the client "fell off" and this made it unnecessary to process requests from this client, who are still waiting for their turn.
Sometimes we were asked about this feature and now our hands have reached its realization. But since far from everything was asked about this feature, it was thought that it should be optional: if some user needs it, then let it explicitly include it, and everyone else should not pay anything for its existence in RESTinio.
And since the main characteristics of the HTTP server in RESTinio are set through the "properties" (traits), it was decided to enable / disable the listening of the connection status through the server properties.
In order to set your connection status listener, the user must perform three steps.
Step 1: define your own class, which should have a non-static state_changed method of the following form:
void state_changed( const restinio::connection_state::notice_t & notice) noexcept;
For example, it might be something like:
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_}; .... } ... };
Step 2: Inside the server properties, you need to define a typedef with the name connection_state_listener_t
, which should refer to the name of the type created in step # 1:
struct my_traits : public restinio::default_traits_t { using connection_state_listener_t = my_state_listener; };
Accordingly, these properties should be used when starting the HTTP server:
restinio::run(restinio::on_thread_pool<my_traits>(8)...);
Step 3: the user must create an instance of his listener and pass this pointer through shared_ptr in the server settings:
restinio::run( restinio::on_thread_pool<my_traits>(8) .port(8080) .address("localhost") .request_handler(...) .connection_state_listener(std::make_shared<my_state_listener>(...)) ) );
If the user does not make a call to the connection_state_listener
method, an exception will be thrown when the HTTP server is started: the north cannot work if the user wants to use the state listener, but this same listener was not asked.
If the user sets the name connection_state_listener_t
in the server properties, he must call the connection_state_listener
method when setting the server parameters. But if the user does not specify connection_state_listener_t
?
In this case, the server properties will still have the name connection_state_listener_t
, but this name will indicate the special type restinio::connection_state::noop_listener_t
.
In essence, the following happens: in RESTinio, when defining regular traits, the value of connection_state_listener_t
is set. Sort of:
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; }; } /* namespace restinio */
And when the user is inherited from restinio::default_traits_t
, then the staff definition of connection_state_listener_t
is also inherited. But if the new name connection_state_listener_t
defined in the derived class:
struct my_traits : public restinio::default_traits_t { using connection_state_listener_t = my_state_listener; ... };
then the new name hides the inherited definition for connection_state_listener_t
. And if there is no new definition, then the old definition remains visible.
So, if the user does not define his own value for connection_state_listener_t
, then RESTinio will use the default value, noop_listener_t
, which is handled by RESTinio in a special way. For example:
connection_state_listener_t
in this case. And, accordingly, a call to the connection_state_listener
method is prohibited (such a call will result in a compile-time error);And just about how all this is achieved speech and will go below.
So, in the RESTinio code, you need to check what the connection_state_listener_t
definition has in the server properties and, depending on this value:
connecton_state_listener_t
;connection_state_listener
methods to set HTTP server parameters;connection_state_listener_t
before starting the HTTP server;state_changed
method state_changed
when the client connection state changes.The fact that RESTinio is still being developed as a library for C ++ 14 is added to the boundary conditions, so the implementation cannot use the features of C ++ 17 (the same if constexpr).
All this is implemented through simple techniques: template classes and their specializations for the type restinio::connection_state::noop_listener_t
. For example, here’s how storage of shared_ptr is made for an object of type connection_state_listener_t
in server settings. Part one:
template< typename Listener > struct connection_state_listener_holder_t { ... // compile-time . std::shared_ptr< Listener > m_connection_state_listener; static constexpr bool has_actual_connection_state_listener = true; void check_valid_connection_state_listener_pointer() const { if( !m_connection_state_listener ) throw exception_t{ "connection state listener is not specified" }; } }; template<> struct connection_state_listener_holder_t< connection_state::noop_listener_t > { static constexpr bool has_actual_connection_state_listener = false; void check_valid_connection_state_listener_pointer() const { // Nothing to do. } };
A template structure is defined here that either has a useful content or not. Just for the type noop_listener_t
it has no useful content.
And part two:
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 > { ... };
The class that contains the parameters for the HTTP server is inherited from connection_state_listener_holder_t
. Thus, in the server settings, either it turns out to be shared_ptr for an object of the connection_state_listener_t
type, or not.
It must be said that storing or not storing shared_ptr in the parameters is flowers. And the berries went when trying to make the methods designed for working with the state listener in basic_server_settings_t
available only if connection_state_listener_t
is different from noop_listener_t
.
Ideally, I wanted to make sure that the compiler didn’t see them at all. But I was tortured to write down conditions for std::enable_if
order to hide these methods. Therefore, I just limited myself to adding 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(); }
There was just another moment when it was regrettable that in C ++ if constexpr is not the same as static if in D. And indeed in C ++ 14 there is nothing like :(
Here you can see the presence of the ensure_valid_connection_state_listener
method. This method is called in the http_server_t
constructor to check that the server parameters contain all the necessary values:
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() } { // Since v.0.5.1 the presence of custom connection state // listener should be checked before the start of HTTP server. settings.ensure_valid_connection_state_listener(); ...
At the same time inside ensure_valid_connection_state_listener
method inherited from connection_state_listener_holder_t
is check_valid_connection_state_listener_pointer
, which, due to the connection_state_listener_holder_t
specialization, either performs an actual check or does nothing.
Similar techniques are used to either call the current state_changed
, if the user wishes to use the state listener, or not to call anything otherwise.
First we need another version of 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 & ) { /* nothing to do */ } template< typename Lambda > void call_state_listener( Lambda && /*lambda*/ ) const noexcept { /* nothing to do */ } }; } /* namespace connection_settings_details */
Unlike connection_state_listener_holder_t
, which was shown earlier and used to store the connection state listener in the parameters of the entire server (that is, objects of the type basic_server_settings_t
), this state_listener_holder_t
will be used for similar purposes, but not in the parameters of the entire server but of Connection:
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 >; ...
There are two features here.
First, the initialization state_listener_holder_t
. It is either needed or not. But only state_listener_holder_t
knows about it. Therefore, the constructor connection_settings_t
simply "pulls" the constructor state_listener_holder_t
, which is called, just in case:
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() }
And the state_listener_holder_t
constructor state_listener_holder_t
either performs the necessary actions or does nothing at all (in the latter case, a more or less intelligent compiler will not generate any code at all to initialize state_listener_holder_t
).
Secondly, it is the state_listner_holder_t::call_state_listener
, which makes the state_changed
call to the state listener. Or does not, if there is no state listener. This call_state_listener
in places where RESTinio diagnoses a connection state change. For example, when it is detected that the connection has been closed:
void close() { m_logger.trace( [&]{ return fmt::format( "[connection:{}] close", connection_id() ); } ); ... // Inform state listener if it used. m_settings->call_state_listener( [this]() noexcept { return connection_state::notice_t{ this->connection_id(), this->m_remote_endpoint, connection_state::cause_t::closed }; } ); }
A call_state_listener
is sent to call_state_listener
, from which the notice_t
object is notice_t
with information about the connection status. If there is a current listener, then this lambda will indeed be called, and the value returned by it will be passed to state_changed
.
However, if there is no listener, then call_state_listener
will be empty and, accordingly, no lambda will be called. In fact, a normal compiler simply throws all calls to an empty call_state_listener
. And in this case, in the generated code there will be nothing at all related to the connection state being addressed to the listener.
In RESTinio-0.5.1, in addition to the connection status listener, such a thing as an IP blocker was added. Those. the user can specify an object that RESTinio will “pull” for each new incoming connection. If the IP blocker says that you can work with the connection, then RESTinio starts the normal maintenance of the new connection (reads and parses the request, calls the request-handler, controls the time-outs, etc.). But if the IP-blocker prohibits work with the connection, then RESTinio stupidly closes this connection and does nothing with it.
Like the state listener, an IP blocker is optional functionality. To use the IP blocker, you must explicitly enable it. Through the properties of the HTTP server. Just as with the listener connection status. And the implementation of IP blocker support in RESTinio uses the same techniques that were already described above. Therefore, we will not dwell on how the IP blocker is used inside RESTinio. Instead, consider an example in which both the IP blocker and the state listener are the same object.
In version 0.5.1, another example is included in the regular RESTinio examples: ip_blocker . This example demonstrates how you can limit the number of parallel connections to a server from a single IP address.
This will require not only an IP blocker that will allow or deny accepting connections. But also the listener of the connection state. The listener is needed to keep track of the moments of creating and closing connections.
At the same time, both the IP blocker and the listener will need the same data set. Therefore, the simplest solution is to make the IP blocker and the listener be the same object.
No problem, we can easily do it:
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: // IP-blocker-. restinio::ip_blocker::inspection_result_t inspect( const restinio::ip_blocker::incoming_info_t & info ) noexcept {...} // . void state_changed( const restinio::connection_state::notice_t & notice ) noexcept {...} };
Here we have no inheritance from any interfaces and redefinition of inherited virtual methods. The only requirement for the listener is the presence of the state_changed
method. This requirement is met.
Similarly, with the only requirement for IP blocker: is there an inspect
method with the required signature? There is! So everything is fine.
It remains to determine the correct properties for the HTTP server:
struct my_traits_t : public restinio::default_traits_t { using logger_t = restinio::shared_ostream_logger_t; // . using connection_state_listener_t = blocker_t; using ip_blocker_t = blocker_t; };
After that, it remains only to create an instance of blocker_t
and pass it in the parameters to the HTTP server:
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) ); } ) );
In my opinion, C ++ templates are what are called "too big gun". Those. so powerful feature that one involuntarily has to think about how well its use is justified. Therefore, the modern C ++ community is divided into several warring camps.
Representatives of one of them prefer to stay away from the templates. Since templates are complex, generating unimaginable length unreadable sheets of error messages increase compile time significantly. Not to mention the urban legends about bloating code and reduced performance.
Representatives of the other camp (like me), believe that the patterns are one of the most powerful sides of C ++. It is even possible that templates are one of the few most serious competitive advantages of C ++ in the modern world. Therefore, in my opinion, the future of C ++ is exactly the templates. And some of the current inconveniences associated with the extensive use of templates (such as lengthy and resource-intensive compilation or low-informative error messages) will in some way be eliminated over time.
Therefore, it seems to me personally that the approach chosen during the implementation of RESTinio, namely, the widespread use of templates and setting the characteristics of an HTTP server through properties, still justifies itself. Thanks to this, we get good customizability for specific needs. And, at the same time, we literally do not pay for what we do not use.
However, on the other hand, it seems that programming in C ++ templates is still unreasonably difficult. You especially feel this when programming is not always necessary, but switching between different types of activities. If you distract from coding for a couple of weeks, then you come back and start to bluntly and specifically stupid if necessary to hide some method using SFINAE or check the presence of a method with a specific signature on the object.
So it's good that there are templates in C ++. It would be even better if they were brought to such a state that even old-footed runners, like me, could use C ++ templates without pain, without having to study cppreference and stackoverflow every 10-15 minutes.
At the moment, RESTinio develops on the principle "when there is time and there is a Wishlist." For example, in the fall of 2018 and in the winter of 2019, we didn’t have much on the development of RESTinio. They answered questions from users, minor edits were made, but there was not enough of our resources for something greater.
But at the end of the spring of 2019, the time for RESTinio was found and we first did RESTinio 0.5.0 , and then 0.5.1 . In this case, the stock of our and others hotelok was exhausted. Those. what we ourselves wanted to see in RESTinio and what users were telling us about is already in RESTinio.
Obviously, RESTinio can be filled with many more. But what exactly?
And here the answer is very simple: RESTinio will only get what we are asked for. So if you want to see something you need in RESTinio, then take the time to inform us about it (for example, through the issues on GitHub or BitBucket , either through the Google group , or right in the comments here on Habré) . Do not say anything - and get nothing;)
Actually, the same situation with our other projects, in particular with SObjectizer . Their new versions will be released as they become intelligible hotelok.
Well, at the very end, I would like to offer to anyone who has not tried RESTinio: try it is free don't hurt. Suddenly like it. But do not like it, then share what exactly. This will help us to make RESTinio more convenient and functional.
Source: https://habr.com/ru/post/456632/
All Articles