Трехэтажные C++ные шаблоны в реализации встраиваемого асинхронного HTTP-сервера с человеческим лицом

    Наша команда специализируется на C++ проектах. И нам время от времени приходилось создавать HTTP-точки входа в C++ компоненты. Для чего использовались разные инструменты. Тут были и старые-добрые CGI, и различные встраиваемые библиотеки, как сторонние, так и самописные. Все это работало, но всегда оставалось ощущение, что следовало бы делать такие вещи и проще, и быстрее, и производительнее.

    В итоге мы решили, что пора прекращать смотреть по сторонам и нужно попробовать сделать что-то свое, с преферансом и куртизанками кроссплатформенностью, асинхронностью, производительностью и человеческим отношением к конечному пользователю. В результате у нас получилась небольшая C++14 библиотека RESTinio, которая позволяет запустить HTTP-сервер внутри C++ приложения всего несколькими строчками кода. Вот, например, простейший сервер, который на все запросы отвечает «Hello, World»:

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

    В реализации RESTinio активно используются C++ные шаблоны и об этом хотелось бы сегодня немного поговорить.

    Буквально пара общих слов о RESTinio


    RESTinio — это небольшой OpenSource проект, который распространяется под BSD-3-CLAUSE лицензией. RESTinio активно развивается с весны 2017-го года. За это время мы сделали несколько публичных релизов, постепенно наполняя RESTinio функциональностью. Самый свежий релиз состоялся сегодня. Это релиз версии 0.4, в которой мы, пожалуй, таки реализовали тот минимум функциональности, который мы хотели иметь.

    RESTinio использует несколько сторонних компонентов. Для работы с сетью мы используем Asio (standalone версию Asio), для парсинга HTTP-протокола у нас используется http-parser из Node.js. Также внутри используется fmtlib, а для тестирования — библиотека Catch2.

    Не смотря на то, что RESTinio пока еще не достиг версии 1.0, мы очень тщательно относимся к качеству и стабильности работы RESTinio. Например, наш коллега участвовал в Mail.ru-шном конкурсе HighloadCup с решением на базе RESTinio. Это решение вышло в финал с 45-го места и заняло в финале 44-е место. Могу ошибаться, но среди финалистов было всего два или три решения, которые строились на базе универсальных HTTP-фреймворков. Вот одним из них как раз и оказалось решение на базе RESTinio.

    Вообще, если говорить о производительности, то скорость работы RESTinio не была приоритетом №1 при разработке. И хотя производительности мы уделяли внимание, тем не менее более важным для нас было получение решения, которым удобно пользоваться. При этом RESTinio не так уж плохо выглядит в синтетических бенчмарках.

    Однако, в данной статье хотелось бы поговорить не столько о самой библиотеке RESTinio и ее возможностях (подробнее с этой информацией можно ознакомиться здесь). Сколько о том, как в ее реализации используется такая важная фича языка C++, как шаблоны.

    Почему шаблоны?


    Код RESTinio построен на шаблонах. Так, в показанном выше примере шаблонов не видно, хотя они там повсюду:

    • функция restinio::run() шаблонная;
    • функция restinio::on_this_thread() шаблонная;
    • метод request_handler() так же шаблонный;
    • и даже метод create_response() шаблонный.

    Почему же RESTinio так активно использует шаблоны? Наверное, самыми серьезными были две следующих причины:

    Во-первых, мы хотели, чтобы RESTinio могла кастомизироваться в широких пределах. Но чтобы кастомизация имела минимальную стоимость в run-time. Как нам кажется, шаблоны здесь просто вне конкуренции.

    Во-вторых, кое-кого из нас, видимо, сильно покусал Александреску. И это до сих пор сказывается, хотя времени с тех пор прошло уже немало.

    Ну и еще нам понравилось следствие из того, что изрядная часть RESTinio представляет из себя шаблонный код: библиотека получилась header-only. Так уж складывается, что в нынешнем C++ подключить header-only библиотеку к своему (или к чужому) проекту гораздо проще, чем ту, которую нужно компилировать. Таки зоопарк систем сборки и систем управления зависимостями в C++ доставляет. И header-only библиотеки в этих зоопарках чувствуют себя гораздо лучше. Пусть даже за это приходится платить увеличением времени компиляции, но это уже тема для совершенно другого разговора…

    Кастомизация на шаблонах в простых примерах


    Выше мы сказали, что шаблоны позволяют кастомизировать RESTinio. Давайте покажем, что под этим подразумевается на паре-тройке простых примеров.

    Отдаем ответ в режиме chunked encoding


    Выше уже было сказано, что метод create_response() является шаблонным. Этот метод параметризуется способом формирования HTTP-ответа. По умолчанию используется restinio_controlled_output_t. Этот метод самостоятельно вычисляет значение HTTP-заголовка Content-Length и инициирует запись ответа в сокет после того, как программист полностью создаст весь ответ и вызовет метод done().

    Но RESTinio поддерживает еще несколько методов: user_controlled_output_t и chunked_output_t. Например, использование режима chunked_output_t будет выглядеть как-то так:

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

    Примечательно то, что create_response() возвращает объект response_builder_t<Output_Type>, публичный API которого зависит от Output_Type. Так, у response_builder_t<restinio_controlled_output_t> нет публичного метода flush(), а публичный метод set_content_length() есть только у response_builder_t<user_controlled_output_t>.

    Включаем логирование


    В самом начале статьи мы показали простейший однопоточный HTTP-сервер. Который работает как «черный ящик», без каких-либо отладочных печатей или диагностического логирования. Давайте сделаем так, чтобы запускаемый HTTP-сервер логировал все происходящие с ним действия на стандартный поток вывода. Для этого нам потребуется небольшой трюк с шаблонами:

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

    Что мы здесь сделали?

    Мы определили собственный класс свойств (traits) для HTTP-сервера, в котором задали нужный нам тип логгера. Потом заставили RESTinio использовать этот класс свойств при конструировании HTTP-сервера внутри restinio::run(). В итоге внутри restino::run() создается HTTP-сервер, который логирует все события посредством логгера, который реализуется типом single_threaded_ostream_logger_t.

    Если мы запустим модифицированный пример и выдадим простейший запрос к нашему серверу (вроде wget localhost:8080), то мы увидим что-то такое:

    [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
    

    Что мы сделали? По сути мы поправили один параметр в свойствах HTTP-сервера и получили дополнительную функциональность. Которой вообще не было в первом случае, когда мы использовали дефолтные свойства для HTTP-сервера. Причем под «вообще» мы понимаем именно «вообще». Поясним на примере.

    В коде RESTinio разбросано логирование выполняемых сервером операций. Вот, скажем:

    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 );
       } );
    }

    Идет обращение к логгеру с передачей лямбда-функции, отвечающей за формирование сообщения для лога. Но если в качестве логгера используется restinio::null_logger_t (а это и происходит по умолчанию), то в null_logger_t методы trace(), info() и им подобные просто ничего не делают:

    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 {}
    ...

    Поэтому нормальный компилятор просто выбрасывает все обращения к логгеру и не генерирует никакого кода для логирования. «Не используешь — не платишь» в чистом виде.

    Выбираем regex-engine для express-роутера


    Еще один пример кастомизации за счет шаблонов продемонстрируем с использованием express-роутера, который есть в RESTinio. Express-роутер сделан в RESTinio по мотивам JavaScript-фреймворка Express. Использование express-роутера существенно упрощает работу с URL для выбора подходящего обработчика. Особенно, когда внутри URL «зашиты» нужные обработчику параметры.

    Вот небольшой пример, который показывает, как посредством express-роутера вешать обработчики на GET-запросы вида /measure/:id и /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;
    }

    Для того, чтобы разбирать URL-ы из запросов, express-роутеру нужна какая-то реализация регулярных выражений. По умолчанию используется std::regex, но std::regex, на данный момент, к сожалению, не может похвастаться отличной производительностью. Например, PCRE/PCRE2 гораздо быстрее std::regex.

    Поэтому в RESTinio можно задать другую реализацию регулярных выражений для express_router_t. Задать как? Правильно: через параметр шаблона. Например, для того, чтобы использовать PCRE2 вместо 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<>>;

    Причем внимательный читатель может заметить, что pcre2_regex_engine_t так же является шаблоном. В этот раз pcre2_regex_engine_t довольствуется дефолтными параметрами. Но мы можем легко это исправить…

    pcre2_regex_engine_t параметризуется собственным классом свойств, специфических для PCRE2. В настоящий момент в свойствах для pcre2_regex_engine_t можно задать такие параметры как опции для компиляции регулярного выражения, опции для pcre2_match, а также такой важный параметр, как max_capture_groups. Этот параметр определяет максимальное количество извлекаемых из строки фрагментов. По умолчанию max_capture_groups равен 20, что означает, что pcre2_regex_engine_t сразу выделит место под 20 фрагментов. В нашем случае это слишком много, т.к. максимальное количество элементов в строках с URL для нашего короткого примера — три. Давайте сделаем настройки, специфические для нашего конкретного случая:

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


    И еще про Traits


    Выше уже были показаны примеры использования классов свойств (т.е. traits) для управления поведения тех или иных сущностей. Но вообще именно Traits определяют все поведение HTTP-сервера в RESTinio. Ибо под капотом у показанных выше функций restinio::run() скрывается создание экземпляра шаблонного класса restinio::http_server_t. И шаблонный параметр Traits как раз определяет параметры работы HTTP-сервера.

    Если смотреть по большому сверху, то в Traits должны быть определены следующие имена типов:

    timer_manager_t. Определяет тип, который будет использоваться HTTP-сервером для отсчета таймаутов, связанных с подключениями к серверу. В RESTinio по умолчанию используется asio_timer_manager_t, использующий штатный механизм таймеров Asio. Так же есть so_timer_manager_t, который использует механизм таймеров SObjectizer-а. Есть еще null_timer_manager_t, который вообще ничего не делает и который оказывается полезным для проведения бенчмарков.

    logger_t. Определяет механизм логирования внутренней активности HTTP-сервера. По умолчанию используется null_logger_t, т.е. по умолчанию HTTP-сервер ничего не логирует. Есть штатная реализация очень простого логгера ostream_logger_t, полезная для отладки.

    request_handler_t. Определяет тип обработчика HTTP-запросов. По умолчанию используется default_request_handler_t, что есть всего лишь std::function<request_handling_status_t(request_handle_t)>. Но пользователь может задать и другой тип, если этот тип предоставляет operator() с нужной сигнатурой. Например, express-роутер, о котором речь шла выше, определяет свой тип обработчика запросов, который нужно задать в качестве request_handler_t в Traits HTTP-сервера.

    strand_t. Определяет тип т.н. strand-а для защиты Asio-шных потрохов при работе в многопоточном режиме. По умолчанию это asio::strand<asio::executor>, что позволяет безопасно запускать HTTP-сервер сразу на нескольких рабочих нитях. Например:

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

    Если же HTTP-сервер работает в однопоточном режиме, то можно избежать дополнительных накладных расходов определив Traits::strand_t как restinio::noop_strand_t (что и делается в restinio::default_single_thread_traits_t).

    stream_socket_t. Определяет тип сокета, с которым предстоит работать RESTinio. По умолчанию это asio::ip::tcp::socket. Но для работы с HTTPS этот параметр должен быть задан как restinio::tls_socket_t.

    В общем, даже в своем ядре — центральном классе http_server_t — в RESTinio применяется policy based design на С++ных шаблонах. Поэтому неудивительно, что отголоски этого подхода обнаруживаются и во многих других частях RESTinio.

    Ну и какая же трехэтажность без CRTP?


    В заголовке статьи упомянуты трехэтажные шаблоны, но до сих пор речь шла лишь о том, как широко шаблоны используются в RESTinio. Примеров же самой трехэтажности пока не было. Нужно устранить это упущение ;)

    Есть в C++ такая хитрая штука, как CRTP (что расшифровывается как Curiously recurring template pattern). Вот с помощью этой штуки в RESTinio реализована работа с параметрами сервера.

    Перед тем, как запустить HTTP-сервер, ему нужно задать несколько обязательных параметров (+ еще можно задать несколько необязательных). Например, в этом примере задается порт и адрес, которые должен слушать HTTP-сервер, обработчик для запросов, а так же тайм-ауты для различных операций:

    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));

    На самом деле здесь нет ничего особо сложного: функция on_this_thread конструирует и возвращает объект server_settings, который далее уже модифицируется посредством вызова методов-setter-ов.

    Однако, говоря «нет ничего особо сложного» мы немного лукавим, поскольку on_this_thread возвращает экземпляр вот такого типа:

    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;
    };

    Т.е. мы уже видим уши CRTP. Но еще интереснее заглянуть в определение 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>
    {
    ...
    };

    Тут можно увидеть еще один шаблон, который используется в качестве базового типа. Сам по себе он ничего интересного не представляет:

    template <typename Settings, typename Socket>
    class socket_type_dependent_settings_t
    {
    protected :
       ~socket_type_dependent_settings_t() = default;
    };

    Но зато его можно специализировать для различных сочетаний Settings и Socket. Например, для поддержки 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() {...}
    ...
    };

    И вот если все это сложить вместе, например, вот в такой ситуации:

    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)));

    То тут уж точно шаблон сидит на шаблоне и шаблоном погоняет. Что особенно хорошо становится заметно в сообщениях об ошибках компилятора, если где-то случайно опечатаешься…

    Заключение


    Вряд ли мы ошибемся, если скажем, что отношение к C++ным шаблонам среди практикующих C++программистов очень разное: кто-то использует шаблоны повсеместно, кто-то время от времени, кто-то категорически против. Еще более неоднозначное отношение к С++ым шаблонам у завсегдатаев профильных форумов/ресурсов, особенно среди тех, кто профессионально разработкой на C++ не занимается, но мнение имеет. Поэтому наверняка у многих прочитавших статью возникнет вопрос: «А оно того стоило?»

    По нашему мнению — да. Хотя нас, например, не сильно смущает время компиляции C++ного кода. Кстати говоря, у компиляции RESTinio+Asio вполне себе нормальная скорость. Это когда к этому добавляется еще и Catch2, вот тогда да, время компиляции увеличивается в разы. Да и сообщений об ошибках от C++ компилятора мы не боимся, тем более, что от года к году эти самые сообщения становятся все более и более вменяемыми.

    В любом случае, на C++ программируют очень по-разному. И каждый может использовать тот стиль, который ему наиболее подходит. Начиная от оберток над чисто сишными библиотеками (вроде mongoose или civetweb) или C++ных библиотек, написанных в Java-подобном «Си с классами» (как это происходит, скажем, в POCO). И заканчивая активно использующими C++ные шаблоны CROW, Boost.Beast и RESTinio.

    Мы вообще придерживаемся того мнения, что в современном мире, при наличии таких конкурентов, как Rust, Go, D и, не говоря уже про C# и Java, у С++ не так уж много серьезных и объективных достоинств. И C++ные шаблоны, пожалуй, одно из немногих конкурентных преимуществ C++, способное оправдать применение C++ в конкретной прикладной задаче. А раз так, то какой смысл отказываться от C++ных шаблонов или ограничивать себя в их использовании? Мы такого смысла не видим, поэтому и задействуем шаблоны в реализации RESTinio настолько активно, насколько это нам позволяет здравый смысл (ну или его отсутствие, тут уж с какой стороны посмотреть).

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 19

      +1
      Шаблоны — это круто, и позволяют существенно меньше кода писать в некоторых случаях, экономя усилия по сравнению с другими языками. Но использование шаблонов только ради шаблонов работает наоборот — кода больше, размер генерируемого кода больше, читаемость хуже. Именно из-за этого столько холиваров, и всякие радикалы предлагают обходиться без шаблонов вообще.

      В http клиенте все же лучше без шаблонов обходиться и не дискредитировать этот отличный инструмент языка. Вы это даже сами понимаете, судя по вашим осторожным оговоркам. Имхо.
        0
        А какие кстати есть хорошие реализации http серверов на С++? Или даже не http серверов, а CGI/FastCGI фреймворков, которые работают через штатные apache или nginx, установленные на сервере?
          0
          а CGI/FastCGI фреймворков
          Вот не подскажу. Да и непонятно, зачем для написания CGI, например, иметь целый фреймворк…
            0
            cppcms — одно время игрался — очень интересная штука — за вечер сайт с регистрацией и блогом ( с комментариями, модерацией, тегами и прочими мелочами ) сделал. также kore — не игрался, но по документации вполне мощный аппарат.
              0

              Я два года назад искал, всё могло поменяться. На тот момент был proxygen от facebook и сервер от микрософта, забыл название.

                0
                proxygen, AFAIK, это фреймворк с полноценной реализацией встраиваемого http-сервера.

                Сервер от Микрософта — это, скорее всего, C++ REST SDK. Опять же, это полноценный встраиваемый http-сервер. Но с никакой (на данный момент) производительностью под Unix-ами (это вроде как даже сами разработчики в обсуждениях на reddit-е подтверждали).
                +1
                Fastcgi Daemon — фреймворк с открытым исходным кодом, разработанный в Яндексе и предназначенный для создания высоконагруженных FastCGI-приложений на C++.

                Статья Веб-приложение на C++, или укрощение демона FastCGI.
              0
              append_header(restinio::http_field::content_type, "test/plain; charset=utf-8");

              test/plain, а не text/plain это опечатка?

                0
                Опечатка, спасибо.
                0
                >>И нам время от времени приходилось создавать HTTP-точки входа в C++ компоненты.
                А не дешевле было написать Node.JS плагин и потом обвязку на JavaScript?

                Т.е. хочется знать, почему решили именно в плюсовом коде работать с сетью?
                  0
                  А не дешевле было написать Node.JS плагин и потом обвязку на JavaScript?
                  AFAIK, это выгодно делать в случае, когда на C++ пишется какой-то «тяжелый» вычислительный код, необходимый Web-приложению. Но даже и в таких ситуациях не всем нравится сопрягать Node.JS и C++.

                  Т.е. хочется знать, почему решили именно в плюсовом коде работать с сетью?
                  Чаще всего это были задачи, когда уже есть работающий C++ компонент, к которому нужно присобачить HTTP-вход для того, чтобы этот компонент научился работать с внешним миром по какому-нибудь REST-у или XML-RPC. Тут (в наших условиях) было бы проще встроить http-вход прямо в C++ный компонент, нежели приделывать сбоку что-то вроде Node.JS.
                  +1
                  Во-вторых, кое-кого из нас, видимо, сильно покусал Александреску. И это до сих пор сказывается, хотя времени с тех пор прошло уже немало.

                  Если посмотреть на эту фотографию, то неудивительно, что он мог покусать:
                  image
                    0
                    В вашем репозитории не нашел cmake-finder-а для вашей библиотеки. Общепринятым подходом является поставлять либу с Finde.cmake скриптом. Рекомендую добавить его
                      0
                      У нас CMake не основная система сборки, так что косяки в поддержке CMake возможны.
                      Видимо, поддержку поиска RESTinio через CMake мы добавим когда будем опакечивать RESTinio для vcpkg и conan-а.
                      +1
                      А раз так, то какой смысл отказываться от C++ных шаблонов или ограничивать себя в их использовании? Мы такого смысла не видим, поэтому и задействуем шаблоны в реализации RESTinio настолько активно, насколько это нам позволяет здравый смысл

                      Не поспоришь, тем более, что описание библиотеки в небольшой статье выглядит вполне понятным и оставляет впечатление хорошего с т.з. использования шаблонов решения. Несмотря на предварительные угрозы о трехэтажных матах ))
                        0

                        Если это трехэтажные шаблоны, то какова, интрересно, этажность tuple или call.

                          0
                          Этажность tuple и call не видна пользователю.

                          А вообще было бы хорошо, если бы вы развернули свою мысль, а то осталось непонятно, что вы сказать хотели.
                            0

                            Посмотрел код на битбакете. Вы правы, шаблонов тоже много.
                            Но субъективно, tuple выглядит какой-то жуткой магией, а тут понятно, что происходит.
                            Возможно из-за того, что оформление лучше.

                              0
                              Но субъективно, tuple выглядит какой-то жуткой магией, а тут понятно, что происходит.
                              Ну мы еще в самом начале. Со временем код проще не станет :)

                        Only users with full accounts can post comments. Log in, please.