Three-story C ++ templates in the implementation of an embedded asynchronous HTTP server with a human face

    Our team specializes in C ++ projects. And from time to time we had to create HTTP entry points into C ++ components. What were the different tools used for? There were good old CGI, and various built-in libraries, both third-party and self-written. All this worked, but there was always the feeling that such things should be done easier, faster, and more productively.

    In the end, we decided that it was time to stop looking around and we should try to do something our own, with preference and courtesans cross-platform, asynchronous, productive and human attitude to the end user. As a result, we got a small C ++ 14 RESTinio library, which allows you to start an HTTP server inside a C ++ application with just a few lines of code. Here, for example, is the simplest server that responds to all requests with "Hello, World":

    #include 
    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 implementation of RESTinio, C ++ templates are actively used and I would like to talk about this a little today.

    Just a couple of general 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 made several public releases, gradually filling RESTinio with functionality. The most recent release took place today. This is the release of version 0.4, in which we, perhaps, did realize 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. It also uses fmtlib internally, and the Catch2 library for testing.

    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 contest HighloadCup with a solution based on RESTinio. This decision reached the final from the 45th place and took the 44th place in the final. I could be wrong, but among the finalists there were only two or three solutions that were built on the basis of universal HTTP frameworks. One of them turned out to be a solution based on RESTinio .

    In general, when it comes to performance, the speed of RESTinio was not the No. 1 priority in development. Although we paid attention to performance, nevertheless, it was more important for us to obtain a solution that is convenient to use. WhereinRESTinio 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 (more details on this information can be found here ). How much about how its implementation uses such an important feature of the C ++ language as templates.

    Why patterns?


    RESTinio code is built on templates. So, in the above example, the templates are not visible, although they are everywhere there:

    • function restinio :: run () templated;
    • function restinio :: on_this_thread () templated;
    • The request_handler () method is also boilerplate;
    • and even the create_response () method is template.

    Why is RESTinio using templates so much? Probably the most serious were the following two reasons:

    First, we wanted RESTinio to be able to customize widely. But so that customization has a minimum cost in run-time. It seems to us that the templates here are simply out of competition.

    Secondly, some of us, apparently, were bitten by Alexandrescu. And this still affects, although a lot has passed since then.

    Well, we also liked the consequence of the fact that a fair part of RESTinio is a template code: the library turned out to be header-only. It just so happens that in current C ++, connecting the header-only library to your (or to someone else's) project is much easier than the one you need to compile. This zoo delivers build systems and dependency management systems in C ++. And the header-only libraries in these zoos feel much better. Even if you have to pay for this by increasing the compilation time, this is already a topic for a completely different conversation ...

    Customization on templates in simple examples


    We said above that templates allow you to customize RESTinio. Let's show what is meant by a couple of simple examples.

    We give the answer in the mode of chunked encoding


    It has already been said above that the create_response () method is boilerplate. This method is parameterized by the method of generating an HTTP response. The default is restinio_controlled_output_t. This method independently calculates the value of the Content-Length HTTP header and initiates writing the response to the socket after the programmer completely creates the entire response and calls the done () method.

    But RESTinio supports a few more methods: user_controlled_output_t and chunked_output_t. For example, using the chunked_output_t mode will look something like this:

    auto handler = [&](auto req) {
       auto resp = req->create_response();
       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(); // Запись подготовленных заголовков.
       for(const auto & part : fragments) {
          resp.append_chunk(make_chunk_from(part));
          resp.flush(); // Запись очередной части ответа.
       }
       return resp.done(); // Завершение обработки.
    };

    Remarkably, create_response () returns a response_builder_t objectwhose public API depends on Output_Type. So, in response_builder_t there is no public flush () method, and only response_builder_t has the 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 debugging seals or diagnostic logging. Let's make the starting HTTP server log all the actions that happen to it to the standard output stream. To do this, we need a little trick with templates:

    #include 
    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()
             .port(8080)
             .address("localhost")
             .request_handler([](auto req) {
                return req->create_response().set_body("Hello, World!").done();
             }));
       return 0;
    }

    What have we done here?

    We defined our own class of traits for the HTTP server, in which we set the type of logger we need. Then they forced RESTinio to use this class of properties 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 implemented by the type single_threaded_ostream_logger_t.

    If we run a modified example and issue a simple request to our server (like wget localhost: 8080), then 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 properties of the HTTP server and received additional functionality. Which was not at all in the first case, when we used the default properties for the HTTP server. Moreover, by “general” we mean precisely “general”. Let us illustrate with an example.

    In the RESTinio code, the logging of server operations is scattered. Here, let's 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 responsible for generating a message for the log. But if restinio :: null_logger_t is used as the logger (and this happens by default), then the trace (), info () and the like methods simply 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 purest form.

    Choosing a regex-engine for an express router


    We will demonstrate another example of customization through templates using the express router, which is in RESTinio. Express router made in RESTinio based on the Express JavaScript framework . Using an express router greatly simplifies working with URLs to select the appropriate handler. Especially when the parameters necessary for the handler are “protected” inside the URL.

    Here is a small example that shows how to set up handlers for GET requests of the form / measure /: id and / measures /: year /: month /: day using an express router:

    #include 
    using my_router_t = restinio::router::express_router_t<>;
    auto make_request_handler()
    {
       auto router = std::make_unique();
       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(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(params["year"]),
                         restinio::cast_to(params["month"]),
                         restinio::cast_to(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()
             .port(8080)
             .address("localhost")
             .request_handler(make_request_handler()));
       return 0;
    }

    In order to parse URLs from requests, an express router needs some kind of regular expression implementation. By default, std :: regex is used, 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 regex implementation for express_router_t. Ask how? Correct: through the template parameter. For example, in order to use PCRE2 instead of std :: regex:

    #include 
    #include 
    using my_router_t = restinio::router::express_router_t<
          restinio::router::pcre2_regex_engine_t<>>;

    Moreover, an attentive reader may notice that pcre2_regex_engine_t is also a template. This time pcre2_regex_engine_t is content with the default parameters. But we can easily fix it ...

    pcre2_regex_engine_t is parameterized by its own class of properties specific to PCRE2. Currently, properties for pcre2_regex_engine_t can be used to set parameters such as options for compiling a regular expression, options for pcre2_match, as well as such an important parameter as max_capture_groups. This parameter determines the maximum number of fragments extracted from the 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 URL strings for our short example is three. Let's make the settings specific to our specific case:

    #include 
    #include 
    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>;


    And about Traits


    Above, examples of using property classes (i.e., traits) to control the behavior of certain entities have already been shown. But in general, it is Traits that determine the entire behavior of the HTTP server in RESTinio. For under the hood of the above restinio :: run () functions hides the creation of an instance of the template class restinio :: http_server_t. And the Traits template parameter just defines the parameters of the HTTP server.

    If you look at the big 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 the timeouts associated with server connections. RESTinio uses asio_timer_manager_t by default, using the standard Asio timer mechanism. There is also so_timer_manager_t, which uses the SObjectizer timer mechanism . There is also null_timer_manager_t, which does nothing at all and which is useful for benchmarks.

    logger_t . Defines the mechanism for logging the internal activity of an HTTP server. The default is null_logger_t, i.e. By default, the HTTP server does not log anything. There is a full-time implementation of the very simple ostream_logger_t logger, useful for debugging.

    request_handler_t. Defines the type of HTTP request handler. The default is default_request_handler_t, which is just a std :: function. But the user can specify another type if this type provides operator () with the desired signature. For example, the express router discussed above defines its type of request handler, which must be set as request_handler_t in the Traits of the HTTP server.

    strand_t . Determines the type of so-called strand to protect Asio giblets when working in multi-threaded mode. By default it is asio :: strand, which allows you to safely run the HTTP server at once on several working threads. For instance:

    restinio::run(
       restinio::on_thread_pool(std::thread::hardware_concurrency())
          .port(8080)
          .address("localhost")
          .request_handler(make_request_handler()));

    If the HTTP server operates in single-threaded mode, then you can avoid the 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 . Defines the type of socket RESTinio will work with. By default, this is asio :: ip :: tcp :: socket. But to work with HTTPS, this parameter must be set as restinio :: tls_socket_t.

    In general, even in its core - the central class http_server_t - RESTinio applies policy based design on C ++ templates. It is therefore not surprising that echoes of this approach are also found in many other parts of RESTinio.

    Well, what is the three-story building without CRTP?


    Three-story templates are mentioned in the title of the article, but so far it has only been about how widely the templates are used in RESTinio. There were no examples of the three-story building itself. It is necessary 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 implemented work with server parameters.

    Before starting the HTTP server, it needs to set some required parameters (+ you can also set some optional parameters). For example, this example sets the port and address that the HTTP server should listen to, the handler for requests, as well as timeouts for various operations:

    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 further modified by calling setter methods.

    However, saying “there is nothing particularly complicated” we are a little cunning, since on_this_thread returns an instance of this type:

    template
    class run_on_this_thread_settings_t final
       : public basic_server_settings_t, Traits>
    {
       using base_type_t = basic_server_settings_t<
             run_on_this_thread_settings_t, Traits>;
    public:
          using base_type_t::base_type_t;
    };

    Those. we already see CRTP ears. But it’s even more interesting to look at the definition of basic_server_settings_t:

    template
    class basic_server_settings_t
       : public socket_type_dependent_settings_t
    {
    ...
    };

    Here you can see another template that is used as the base type. In itself, it does not represent anything interesting:

    template 
    class socket_type_dependent_settings_t
    {
    protected :
       ~socket_type_dependent_settings_t() = default;
    };

    But then it can be specialized for various combinations of Settings and Socket. For example, to support TLS:

    template
    class socket_type_dependent_settings_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>;
    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()
          .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, certainly, the template sits on the template and drives the template. What is especially noticeable in compiler error messages is if you accidentally get it printed somewhere ...

    Conclusion


    We are unlikely to make a mistake if we say that the attitude to C ++ templates among practicing C ++ programmers is very different: someone uses templates everywhere, someone from time to time, someone is categorically against it. Regular forum / resource attendees have an even more ambiguous attitude to C ++ templates, especially among those who are not professionally involved in C ++ development, but who have an opinion. Therefore, for sure, many who read the article will have the question: “Was it worth it?”

    In our opinion, yes. Although, for example, we are not very confused by the compilation time of C ++ code. By the way, the compilation of RESTinio + Asio has quite normal speed. This is when Catch2 is also added to this, then yes, the compilation time increases significantly. And we are not afraid of error messages from the C ++ compiler, especially since from year to year these same messages are becoming more and more sane.

    In any case, C ++ is programmed in very different ways. And everyone can use the style that suits him best. Starting from wrappers over purely cached libraries (like mongoose or civetweb ) or C ++ libraries written in a Java-like "C with classes" (as it happens, say, in POCO) And ending with actively using C ++ templates CROW , Boost.Beast and RESTinio .

    We generally adhere to the opinion that in the modern world, with competitors such as Rust, Go, D and, not to mention C # and Java, C ++ does not have many serious and objective advantages. And C ++ templates, perhaps, is one of the few competitive advantages of C ++ that can justify the use of C ++ in a specific application. And if so, then what is the point of abandoning C ++ templates or limiting oneself in their use? We don’t see such a sense, therefore, we use the templates in the RESTinio implementation as actively as common sense allows us (well, or its absence, here to look from which side).

    Also popular now: