Our team specializes in C ++ projects. And from time to time we had to create HTTP entry points to C ++ components. What used different tools. There were good old CGIs and various embedded libraries, both third-party and self-written. It all worked, but there was always the feeling that such things should be made simpler, and faster, and more productive.
As a result, we decided that it was time to stop looking around and we had to try to do something of our own, with
preference and courtesans cross-platform, asynchronous, performance and human attitude towards the end user. As a result, we have a small C ++ 14
RESTinio library that allows you to run an HTTP server inside a C ++ application with just a few lines of code. Here, for example, the simplest server that answers “Hello, World” to all requests:
#include <restinio/all.hpp> int main() { restinio::run( restinio::on_this_thread() .port(8080) .address("localhost") .request_handler([](auto req) { return req->create_response().set_body("Hello, World!").done(); })); return 0; }
In the RESTinio implementation, C ++ templates are actively used, and I would like to talk a bit today.
Literally a couple of common words about RESTinio
RESTinio is a small OpenSource project that is distributed under the BSD-3-CLAUSE license. RESTinio has been actively developing since the spring of 2017. During this time, we have made several public releases, gradually filling RESTinio with functionality. The most recent release took place today. This is a release of version 0.4, in which we, perhaps, realized the minimum of functionality that we wanted to have.
')
RESTinio uses several third-party components. To work with the network, we use Asio (standalone version of Asio), for parsing the HTTP protocol, we use http-parser from Node.js. Also inside is used fmtlib, and for testing - the library Catch2.
Despite the fact that RESTinio has not yet reached version 1.0, we are very careful about the quality and stability of RESTinio. For example, our colleague participated
in the Mail.ru-shnom HighloadCup contest with a solution based on RESTinio. This decision reached the final from the 45th place and took 44th place in the final. I can be mistaken, but among the finalists there were only two or three solutions that were built on the basis of universal HTTP frameworks. One of them was exactly the
solution based on RESTinio .
In general, if we talk about performance, the speed of RESTinio was not the number one priority in development. Although we paid attention to performance, it was nonetheless more important for us to get a solution that is convenient to use. At the same time,
RESTinio doesn't look so bad in synthetic benchmarks .
However, in this article I would like to talk not so much about the RESTinio library itself and its capabilities (for more information, see this information
here ). How much about how such an important feature of the C ++ language is used in its implementation, as templates.
Why templates?
RESTinio code is built on templates. So, in the example shown above, templates are not visible, although they are everywhere there:
- restinio :: run () function;
- the restinio :: on_this_thread () function is template;
- the request_handler () method is also template;
- and even the create_response () method is template.
Why is RESTinio so actively using templates? Probably the most serious were the following two reasons:
First, we wanted RESTinio to customize widely. But so that customization has a minimal cost at run-time. It seems to us that the templates are simply out of competition.
Secondly, some of us, apparently, were bitten by Alexandrescu strongly. And it still affects, although a lot has passed since then.
Well, we also liked the consequence of the fact that a good part of RESTinio is sample code: the library turned out to be header-only. It just so happens that in the current C ++ it is much easier to connect the header-only library to your (or to someone else's) project than the one you need to compile. Taki zoo assembly systems and dependency management systems in C ++ delivers. And the header-only libraries in these zoos feel much better. Even if you have to pay for this increase in compile time, but this is a topic for a completely different conversation ...
Customization on templates in simple examples
Above, we said that templates allow customizing RESTinio. Let's show what is meant by this on a couple-three simple examples.
Give the answer in chunked encoding mode
As mentioned above, the create_response () method is a template. This method is parameterized by the method of forming the HTTP response. The default is restinio_controlled_output_t. This method independently calculates the value of the HTTP Content-Length header and initiates writing the response to the socket after the programmer has completely created the entire response and calls the done () method.
But RESTinio supports several more methods: user_controlled_output_t and chunked_output_t. For example, using chunked_output_t mode would look something like this:
auto handler = [&](auto req) { auto resp = req->create_response<restinio::chunked_output_t>(); resp .append_header(restinio::http_field::server, "MyApp Embedded Server") .append_header_date_field() .append_header(restinio::http_field::content_type, "text/plain; charset=utf-8"); resp.flush();
It is noteworthy that create_response () returns a response_builder_t <Output_Type> object, whose public API depends on Output_Type. So, response_builder_t <restinio_controlled_output_t> does not have a public flush () method, and only response_builder_t <user_controlled_output_t> has a public set_content_length () method.
Turn on logging
At the very beginning of the article, we showed the simplest single-threaded HTTP server. Which works as a “black box”, without any debug seals or diagnostic logging. Let's make it so that the HTTP server that is started logs all actions that occur to it to the standard output stream. To do this, we need a little trick with the templates:
#include <restinio/all.hpp> int main() { struct my_traits : public restinio::default_single_thread_traits_t { using logger_t = restinio::single_threaded_ostream_logger_t; }; restinio::run( restinio::on_this_thread<my_traits>() .port(8080) .address("localhost") .request_handler([](auto req) { return req->create_response().set_body("Hello, World!").done(); })); return 0; }
What did we do here?
We defined our own property class (traits) for the HTTP server, in which we set the type of logger we need. Then they forced RESTinio to use this property class when constructing an HTTP server inside restinio :: run (). As a result, an HTTP server is created inside restino :: run (), which logs all events through a logger, which is implemented by the single_threaded_ostream_logger_t type.
If we run a modified example and issue a simple request to our server (like wget localhost: 8080), we will see something like this:
[2017-12-24 12:04:29.612] TRACE: starting server on 127.0.0.1:8080 [2017-12-24 12:04:29.612] INFO: init accept #0 [2017-12-24 12:04:29.612] INFO: server started on 127.0.0.1:8080 [2017-12-24 12:05:00.423] TRACE: accept connection from 127.0.0.1:45930 on socket #0 [2017-12-24 12:05:00.423] TRACE: [connection:1] start connection with 127.0.0.1:45930 [2017-12-24 12:05:00.423] TRACE: [connection:1] start waiting for request [2017-12-24 12:05:00.423] TRACE: [connection:1] continue reading request [2017-12-24 12:05:00.423] TRACE: [connection:1] received 141 bytes [2017-12-24 12:05:00.423] TRACE: [connection:1] request received (#0): GET / [2017-12-24 12:05:00.423] TRACE: [connection:1] append response (#0), flags: { final_parts, connection_keepalive }, bufs count: 2 [2017-12-24 12:05:00.423] TRACE: [connection:1] sending resp data, buf count: 2 [2017-12-24 12:05:00.423] TRACE: [connection:1] start waiting for request [2017-12-24 12:05:00.423] TRACE: [connection:1] continue reading request [2017-12-24 12:05:00.423] TRACE: [connection:1] outgoing data was sent: 76 bytes [2017-12-24 12:05:00.423] TRACE: [connection:1] should keep alive [2017-12-24 12:05:00.423] TRACE: [connection:1] start waiting for request [2017-12-24 12:05:00.423] TRACE: [connection:1] continue reading request [2017-12-24 12:05:00.424] TRACE: [connection:1] EOF and no request, close connection [2017-12-24 12:05:00.424] TRACE: [connection:1] close [2017-12-24 12:05:00.424] TRACE: [connection:1] destructor called [2017-12-24 12:05:16.402] TRACE: closing server on 127.0.0.1:8080 [2017-12-24 12:05:16.402] INFO: server closed on 127.0.0.1:8080
What have we done? In fact, we corrected one parameter in the HTTP server properties and got additional functionality. Which was not at all in the first case, when we used the default properties for the HTTP server. Moreover, by “generally” we understand precisely “in general”. Let's explain with an example.
In the RESTinio code, logging of server operations is scattered. Here, say:
void close_impl() { const auto ep = m_acceptor.local_endpoint(); m_logger.trace( [&]{ return fmt::format( "closing server on {}", ep ); } ); m_acceptor.close(); m_logger.info( [&]{ return fmt::format( "server closed on {}", ep ); } ); }
There is a call to the logger with the transfer of the lambda function, which is responsible for the formation of a message for the log. But if restinio :: null_logger_t is used as a logger (and this is the default), then trace (), info () and the like do nothing in null_logger_t:
class null_logger_t { public: template< typename Message_Builder > constexpr void trace( Message_Builder && ) const {} template< typename Message_Builder > constexpr void info( Message_Builder && ) const {} template< typename Message_Builder > constexpr void warn( Message_Builder && ) const {} ...
Therefore, the normal compiler simply throws out all calls to the logger and does not generate any code for logging. "Do not use - do not pay" in its pure form.
Choose regex-engine for express-router
We will demonstrate another example of customization through templates using an express router, which is in RESTinio. Express router is made in RESTinio based on JavaScript framework
Express . Using an express router greatly simplifies working with URLs to select the appropriate handler. Especially when inside the URL, the handler needs the parameters.
Here is a small example that shows how, by means of an express router, to hang up handlers for GET requests of the form / measure /: id and / measures /: year /: month /: day:
#include <restinio/all.hpp> using my_router_t = restinio::router::express_router_t<>; auto make_request_handler() { auto router = std::make_unique<my_router_t>(); router->http_get(R"(/measure/:id(\d+))", [](auto req, auto params) { return req->create_response() .set_body( fmt::format("Measure with id={} requested", restinio::cast_to<unsigned long>(params["id"]))) .done(); }); router->http_get(R"(/measures/:year(\d{4})/:month(\d{2})/:day(\d{2}))", [](auto req, auto params) { return req->create_response() .set_body( fmt::format("Request measures for a date: {}.{}.{}", restinio::cast_to<int>(params["year"]), restinio::cast_to<short>(params["month"]), restinio::cast_to<short>(params["day"]))) .done(); }); router->non_matched_request_handler([](auto req) { return req->create_response(404, "Unknown request") .connection_close() .done(); }); return router; } int main() { struct my_traits : public restinio::default_single_thread_traits_t { using request_handler_t = my_router_t; }; restinio::run( restinio::on_this_thread<my_traits>() .port(8080) .address("localhost") .request_handler(make_request_handler())); return 0; }
In order to parse URLs from requests, the express router needs some kind of regular expression implementation. The default is std :: regex, but std :: regex, at the moment, unfortunately, cannot boast of excellent performance. For example, PCRE / PCRE2 is much faster than std :: regex.
Therefore, in RESTinio, you can specify a different regular expression implementation for express_router_t. Ask how? Correct: through the template parameter. For example, to use PCRE2 instead of std :: regex:
#include <restinio/all.hpp> #include <restinio/router/pcre2_regex_engine.hpp> using my_router_t = restinio::router::express_router_t< restinio::router::pcre2_regex_engine_t<>>;
And the attentive reader may notice that pcre2_regex_engine_t is also a template. This time pcre2_regex_engine_t is content with default parameters. But we can fix it easily ...
pcre2_regex_engine_t is parametrized by its own class of properties specific for PCRE2. At the moment, in the properties for pcre2_regex_engine_t, you can set parameters such as options for compiling a regular expression, options for pcre2_match, as well as an important parameter such as max_capture_groups. This parameter defines the maximum number of fragments extracted from a string. By default, max_capture_groups is 20, which means that pcre2_regex_engine_t will immediately allocate space for 20 fragments. In our case, this is too much, because The maximum number of elements in lines with a URL for our short example is three. Let's make the settings specific to our particular case:
#include <restinio/all.hpp> #include <restinio/router/pcre2_regex_engine.hpp> struct my_pcre2_traits : public restinio::router::pcre2_traits_t<> { static constexpr int max_capture_groups = 4; // +1 URL. }; using my_router_t = restinio::router::express_router_t< restinio::router::pcre2_regex_engine_t<my_pcre2_traits>>;
And about Traits
We have already shown examples of using property classes (i.e. traits) to control the behavior of certain entities. But in general, Traits define all the behavior of an HTTP server in RESTinio. For under the hood, the restinio :: run () functions shown above hide the creation of an instance of the restinio :: http_server_t template class. And the Traits template parameter just determines the parameters of the HTTP server.
If you look at the large top, then the following type names must be defined in Traits:
timer_manager_t . Defines the type that the HTTP server will use to count timeouts related to server connections. In RESTinio, the default is asio_timer_manager_t, which uses the standard Asio timers mechanism. There is also so_timer_manager_t, which uses the
SObjectizer timers mechanism. There is also null_timer_manager_t, which does nothing at all and which is useful for benchmarking.
logger_t . Defines the logging mechanism for the internal HTTP server activity. The default is null_logger_t, i.e. by default, the HTTP server does not log anything. There is a regular implementation of a very simple ostream_logger_t logger, useful for debugging.
request_handler_t . Specifies the type of HTTP request handler. The default is default_request_handler_t, which is just std :: function <request_handling_status_t (request_handle_t)>. But the user can specify another type if this type provides operator () with the required signature. For example, the express router discussed above defines its own type of request handler, which must be specified as request_handler_t in the HTTP Traits server.
strand_t . Specifies the type of so-called. strand to protect Asio shnyh offal when working in multithreaded mode. The default is asio :: strand <asio :: executor>, which allows you to safely start the HTTP server on several working threads at once. For example:
restinio::run( restinio::on_thread_pool(std::thread::hardware_concurrency()) .port(8080) .address("localhost") .request_handler(make_request_handler()));
If the HTTP server runs in single-threaded mode, you can avoid additional overhead by defining Traits :: strand_t as restinio :: noop_strand_t (which is done in restinio :: default_single_thread_traits_t).
stream_socket_t . Specifies the type of socket with which RESTinio will work. The default is asio :: ip :: tcp :: socket. But to work with HTTPS, this parameter must be specified as restinio :: tls_socket_t.
In general, even in its core, the central class of the http_server_t, RESTinio uses policy based design on C ++ templates. It is therefore not surprising that the echoes of this approach are also found in many other parts of RESTinio.
Well, what is the three-story without CRTP?
The title of the article mentions three-story patterns, but so far it has been a question of how widely patterns are used in RESTinio. Examples of the same three-story has not yet been. Need to eliminate this omission;)
There is such a tricky thing in C ++ as
CRTP (which stands for Curiously recurring template pattern) . Here with the help of this thing in RESTinio work with server parameters is implemented.
Before you start the HTTP server, it needs to set several required parameters (+ you can still set several optional parameters). For example, in this example, the port and the address that the HTTP server should listen to, the request handler, as well as timeouts for various operations are set:
restinio::run( restinio::on_this_thread() .port(8080) .address("localhost") .request_handler(server_handler()) .read_next_http_message_timelimit(10s) .write_http_response_timelimit(1s) .handle_request_timeout(1s));
In fact, there is nothing particularly complicated here: the on_this_thread function constructs and returns a server_settings object, which is then further modified by calling setter methods.
However, saying “there is nothing particularly difficult” we are a little cunning, since on_this_thread returns an instance of this type:
template<typename Traits> class run_on_this_thread_settings_t final : public basic_server_settings_t<run_on_this_thread_settings_t<Traits>, Traits> { using base_type_t = basic_server_settings_t< run_on_this_thread_settings_t<Traits>, Traits>; public: using base_type_t::base_type_t; };
Those. we are already seeing CRTP ears. But it is even more interesting to look into the definition of basic_server_settings_t:
template<typename Derived, typename Traits> class basic_server_settings_t : public socket_type_dependent_settings_t<Derived, typename Traits::stream_socket_t> { ... };
Here you can see another template that is used as the base type. By itself, it does not represent anything interesting:
template <typename Settings, typename Socket> class socket_type_dependent_settings_t { protected : ~socket_type_dependent_settings_t() = default; };
But it can be specialized for various combinations of Settings and Socket. For example, to support TLS:
template<typename Settings> class socket_type_dependent_settings_t<Settings, tls_socket_t> { protected: ~socket_type_dependent_settings_t() = default; public: socket_type_dependent_settings_t() = default; socket_type_dependent_settings_t(socket_type_dependent_settings_t && ) = default; Settings & tls_context(asio::ssl::context context ) & {...} Settings && tls_context(asio::ssl::context context ) && {...} asio::ssl::context tls_context() {...} ... };
And if all this is put together, for example, in this situation:
struct my_pcre2_traits : public restinio::router::pcre2_traits_t<> { static constexpr int max_capture_groups = 4; }; using my_router_t = restinio::router::express_router_t< restinio::router::pcre2_regex_engine_t<my_pcre2_traits>>; using my_traits_t = restinio::single_thread_tls_traits_t< restinio::asio_timer_manager_t, restinio::single_threaded_ostream_logger_t, my_router_t>; ... restinio::run( restinio::on_this_thread<my_traits_t>() .address("localhost") .request_handler(server_handler()) .read_next_http_message_timelimit(10s) .write_http_response_timelimit(1s) .handle_request_timeout(1s) .tls_context(std::move(tls_context)));
Then there is certainly a template sitting on a template and a template chasing. What becomes especially well noticeable in the compiler error messages, if you accidentally grieve somewhere ...
Conclusion
We can hardly be mistaken if we say that the attitude towards C ++ templates among practitioners of C ++ programmers is very different: someone uses templates everywhere, someone from time to time, someone categorically against. An even more ambiguous attitude towards C ++ templates from the regulars of relevant forums / resources, especially among those who do not develop C ++ professionally, but has an opinion. Therefore, surely many who read the article will have a question: “Was it worth it?”
In our opinion, yes. Although, for example, we are not greatly confused by the time for compiling C ++ code. By the way, compiling RESTinio + Asio has quite a normal speed. This is when Catch2 is added to this, then yes, the compile time increases significantly. And we are not afraid of error messages from the C ++ compiler, especially since from year to year these same messages become more and more sane.
In any case, C ++ is programmed very differently. And everyone can use the style that suits him best. Starting from wrappers over purely shared libraries (like
mongoose or
civetweb ) or C ++ libraries written in a Java-like “C with classes” (as it happens, say, in
POCO ). And ending actively using C ++ templates
CROW ,
Boost.Beast and
RESTinio .
We generally hold the opinion that in the modern world, if there are such competitors as Rust, Go, D and, not to mention C # and Java, C ++ doesn’t have too many serious and objective advantages. And C ++ templates are perhaps one of the few competitive advantages of C ++ that can justify the use of C ++ in a specific application. And if so, then what's the point of abandoning C ++ templates or limiting yourself to using them? We do not see such a meaning, therefore, we will use patterns in the implementation of RESTinio as actively as common sense allows us (well, or its absence, here which side to look at).