Асинхронные HTTP-запросы на C++: входящие через RESTinio, исходящие через libcurl. Часть 1

    Преамбула


    Наша команда занимается разработкой небольшого, удобного в использовании, встраиваемого, асинхронного HTTP-сервера для современного C++ под названием RESTinio. Начали его делать потому, что нужна была именно асинхронная обработка входящих HTTP-запросов, а ничего готового, чтобы нам понравилось, не нашлось. Как показывает жизнь, асинхронная обработка HTTP-запросов в C++ приложениях нужна не только нам. Давеча на связь вышли разработчики из одной компании с вопросом о том, можно ли как-то подружить асинхронную обработку входящих запросов в RESTinio с выдачей асинхронных исходящих запросов посредством libcurl.

    По мере выяснения ситуации мы обнаружили, что эта компания столкнулась с условиями, с которыми сталкивались и мы сами, и из-за которых мы и занялись разработкой RESTinio. Суть в том, что написанное на C++ приложение принимает входящий HTTP-запрос. В процессе обработки запроса приложению нужно обратиться к стороннему серверу. Этот сервер может отвечать довольно долго. Скажем, 10 секунд (хотя 10 секунд — это еще хорошо). Если делать синхронный запрос к стороннему серверу, то блокируется рабочая нить, на которой выполняется HTTP-запрос. А это начинает ограничивать количество параллельных запросов, которые может обслуживать приложение.

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

    Фокус был в том, что в приложении для исходящих HTTP-запросов уже использовался libcurl. Но в виде curl_easy, т.е. все запросы выполнялись синхронно. У нас же спрашивали, а можно ли совместить RESTinio и curl_multi? Вопрос для нас самих оказался интересным, т.к. раньше libcurl в виде curl_multi применять не приходилось. Поэтому интересно было самим погрузиться в эту тему.

    Погрузились. Получили массу впечатлений. Решили поделиться с читателями. Может кому-нибудь будет интересно, как можно жить с curl_multi. Ибо, как показала практика, жить-то можно. Но осторожно… ;) О чем мы и расскажем в небольшой серии статей, основанных на опыте реализации несложной имитации описанной выше ситуации с медленно отвечающим сторонним сервисом.

    Необходимые disclaimer-ы


    Дабы предупредить бесполезный и неконструктивный флейм в комментариях (вроде того, что случилось с предыдущей статьей), хочется сделать несколько предупреждений:

    • во-первых, далее речь пройдет про C++. Если вам не нравится C++, если вы считаете, что C++ не место в современном мире вообще и в подобных задачах в частности, то эта статья не для вас. И у нас нет цели убедить кого-то в том, что C++ хорош и должен использоваться в таких задачах. Мы лишь рассказываем о том, как можно решить подобную задачу на C++ если вам вдруг пришлось это делать именно на C++. Так же мы не будем спорить о том, почему может такое потребоваться и почему в реальной жизни нельзя просто взять и переписать существующий C++ код на чем-то еще;
    • во-вторых, в C++ нет общепринятого code convention, поэтому какие-либо претензии со стороны приверженцев camelCase, PascalCase, Camel_With_Underscores_Case или даже UPPER_CASE восприниматься не будут. Мы постарались привести код в более-менее похожий на K&R стиль, дабы он выглядел привычно для наибольшего количества читателей. Ибо наш «фирменный» стиль оформления С++кода точно приемлют не все. Однако, если внешний вид кода нарушает ваши эстетические чувства и вы готовы высказать в комментариях свое веское «фи» по этому поводу, то задумайтесь, пожалуйста, вот о чем: всегда есть кто-то, кому не нравится используемый вами стиль. Всегда. Вне зависимости от того, какой именно стиль вы используете;
    • в-третьих, показанный нами код ни в коем случае не претендует на звание образца качества и надежности. Это не предназначенный для продакшена код. То, что вы увидите — это quick-and-dirty прототип, который был слеплен на коленке буквально за день и еще один день был потрачен на то, чтобы хоть чуть-чуть причесать получившийся код и снабдить его поясняющими комментариями. Так что претензии вида «да кто так пишет» или «за такой говнокод нужно бить по рукам» не принимаются, т.к. мы сами себе их высказываем ;)

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

    В чем суть разработанной имитации?


    В демонстрационных целях мы с помощью RESTinio и libcurl сделали несколько приложений. Самое простое из них — это имитатор стороннего, медленно отвечающего сервера, под названием delay_server. Для запуска имитации нужно запустить delay_server с необходимым набором параметров (адрес, порт, желаемые времена задержек для ответов).

    Так же в имитацию входит несколько «фронтов», под названием bridge_server_*. Именно bridge_server-а принимают запросы от пользователя и переадресуют запросы на delay_server. Предполагается, что пользователь запускает сперва delay_server, потом один из bridge_server-ов, после чего уже начинает «обстреливать» bridge_server удобным ему способом. Например, через curl/wget или утилиты вроде ab/wrk.

    В состав имитации входит три реализации bridge_server-ов:

    • bridge_server_1. Очень простой вариант, в котором используется всего две рабочих нити. На одной RESTinio обрабатывает входящие HTTP-запросы, а на второй посредством curl_multi_perform выполняются исходящие HTTP-запросы. Эта реализация будет рассматриваться во второй части серии;
    • bridge_server_1_pipe. Более сложный вариант bridge_server_1. Так же две рабочие нити, но используется дополнительный pipe для передачи нотификаций от нити RESTinio к нити libcurl-а. Изначально эту реализацию описывать мы не планировали, но если у кого-то будет интерес, то можно будет рассмотреть bridge_server_1_pipe в деталях в дополнительной статье;
    • bridge_server_2. Более сложный вариант, в котором используется пул рабочих нитей. Причем этот пул обслуживает как RESTinio, так и libcurl (используется curl_multi_socket_action). Эта реализация будет рассматриваться в заключительной части серии.

    А начнем эту серию с описания реализации delay_server-а. Благо это самая простая и, возможно, самая понятная часть. Реализации bridge_server-ов будут куда хардкорнее.

    delay_server


    Что делает delay_server?


    delay_server принимает HTTP GET запросы на URL-ы вида /YYYY/MM/DD, где YYYY, MM и DD — это цифровые значения. На все остальные запросы delay_server отвечает кодом 404.

    Если же приходит HTTP GET запрос на URL вида /YYYY/MM/DD, то delay_server выдерживает паузу и затем отвечает небольшим текстом, в котором есть приветствие «Hello, World» и величина выдержанной паузы. Например, если запустить delay_server с параметрами:

    delay_server -a localhost -p 4040 -m 1500 -M 4000

    т.е. он будет слушать на localhost:4040 и выдерживать паузу для ответов между 1.5s и 4.0s. Если затем выполнить:

    curl -4 http://localhost:4040/2018/02/22

    то получим:

    Hello world!
    Pause: 2347ms.


    Ну или можно включить трассировку происходящего. Для сервера это:

    delayed_server -a localhost -p 4040 -m 1500 -M 4000 -t

    Для curl-а это:

    curl -4 -v http://localhost:4040/2018/02/22

    Для delay_server-а мы увидим что-то вроде:

    [2018-02-22 16:47:54.441] TRACE: starting server on 127.0.0.1:4040
    [2018-02-22 16:47:54.441]  INFO: init accept #0
    [2018-02-22 16:47:54.441]  INFO: server started on 127.0.0.1:4040
    [2018-02-22 16:47:57.040] TRACE: accept connection from 127.0.0.1:38468 on socket #0
    [2018-02-22 16:47:57.041] TRACE: [connection:1] start connection with 127.0.0.1:38468
    [2018-02-22 16:47:57.041] TRACE: [connection:1] start waiting for request
    [2018-02-22 16:47:57.041] TRACE: [connection:1] continue reading request
    [2018-02-22 16:47:57.041] TRACE: [connection:1] received 88 bytes
    [2018-02-22 16:47:57.041] TRACE: [connection:1] request received (#0): GET /2018/02/22
    [2018-02-22 16:47:59.401] TRACE: [connection:1] append response (#0), flags: { final_parts, connection_keepalive }, bufs count: 2
    [2018-02-22 16:47:59.401] TRACE: [connection:1] sending resp data, buf count: 2
    [2018-02-22 16:47:59.402] TRACE: [connection:1] outgoing data was sent: 206 bytes
    [2018-02-22 16:47:59.402] TRACE: [connection:1] should keep alive
    [2018-02-22 16:47:59.402] TRACE: [connection:1] start waiting for request
    [2018-02-22 16:47:59.402] TRACE: [connection:1] continue reading request
    [2018-02-22 16:47:59.403] TRACE: [connection:1] EOF and no request, close connection
    [2018-02-22 16:47:59.403] TRACE: [connection:1] close
    [2018-02-22 16:47:59.403] TRACE: [connection:1] destructor called

    и для curl-а:

    *   Trying 127.0.0.1...
    * TCP_NODELAY set
    * Connected to localhost (127.0.0.1) port 4040 (#0)
    > GET /2018/02/22 HTTP/1.1
    > Host: localhost:4040
    > User-Agent: curl/7.58.0
    > Accept: */*
    >
    < HTTP/1.1 200 OK
    < Connection: keep-alive
    < Content-Length: 28
    < Server: RESTinio hello world server
    < Date: Thu, 22 Feb 2018 13:47:59 GMT
    < Content-Type: text/plain; charset=utf-8
    <
    Hello world!
    Pause: 2360ms.
    * Connection #0 to host localhost left intact

    Как delay_server это делает?


    delay_server представляет из себя простое однопоточное C++ приложение. На главной нити запускается встроенный HTTP-сервер, который дергает назначенный пользователем callback при получении запроса на подходящий URL. Этот callback создает Asio-шный таймер и взводит созданный таймер на случайно выбранную паузу (пауза выбирается так, чтобы попасть в заданные при запуске delay_server пределы). После чего callback возвращает управление HTTP-серверу, что дает возможность серверу принять и обработать следующий запрос. Когда срабатывает взведенный callback-ом таймер, то формируется и отсылается ответ на ранее полученный HTTP-запрос.

    Разбор реализации delay_server


    Функция main()


    Разбор реализации delay_server начнем сразу с функции main(), постепенно объясняя то, что происходит внутри и вне main()-а.

    Итак, код main() выглядит следующим образом:

    int main(int argc, char ** argv) {
      try {
        const auto cfg = parse_cmd_line_args(argc, argv);
        if(cfg.help_requested_)
          return 1;
    
        // Нам нужен собственный io_context для того, чтобы мы могли с ним
        // работать напрямую в обработчике запросов.
        restinio::asio_ns::io_context ioctx;
    
        // Так же нам потребуется генератор случайных задержек в выдаче ответов.
        pauses_generator_t generator{cfg.config_.min_pause_, cfg.config_.max_pause_};
    
        // Нам нужен обработчик запросов, который будет использоваться
        // вне зависимости от того, какой именно сервер мы будем запускать
        // (с трассировкой происходящего или нет).
        auto actual_handler = [&ioctx, &generator](auto req, auto /*params*/) {
            return handler(ioctx, generator, std::move(req));
          };
    
        // Если должна использоваться трассировка запросов, то должен
        // запускаться один тип сервера.
        if(cfg.config_.tracing_) {
          run_server<traceable_server_traits_t>(
              ioctx, cfg.config_, std::move(actual_handler));
        }
        else {
          // Трассировка не нужна, запускается другой тип сервера.
          run_server<non_traceable_server_traits_t>(
              ioctx, cfg.config_, std::move(actual_handler));
        }
    
        // Все, теперь ждем завершения работы сервера.
      }
      catch( const std::exception & ex ) {
        std::cerr << "Error: " << ex.what() << std::endl;
        return 2;
      }
    
      return 0;
    }

    Что здесь происходит?

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

    Во-вторых, мы создаем несколько объектов, которые нам понадобятся:

    • экземпляр asio::io_context, который будет использоваться как для обработки IO-операций HTTP-сервера, так и для таймеров, которые будут взводится в обработчике входящих HTTP-запросов;
    • генератор случайных задержек, который нужен как раз для того, чтобы HTTP-сервер медленно отвечал на запросы;
    • лямбда-функция, сохраненная в переменную actual_handler, которая и будет тем самым callback-ом, вызываемым HTTP-сервером для входящих HTTP-запросов. У этого callback-а должен быть определенный формат. Но функция handler(), которая и выполняет фактическую обработку запросов и о которой речь пойдет ниже, имеет другой формат и требует дополнительных аргументов. Вот лямбда-функция и захватывает нужные handler()-у аргументы, выставляя наружу ту сигнатуру, которую требует RESTinio.

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

    Вот, собственно и весь delay_server :)

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

    Конфигурация и разбор командной строки


    В delay_server используется очень простая структура для описания конфигурации сервера:

    // Конфигурация, которая потребуется серверу.
    struct config_t {
      // Адрес, на котором нужно слушать новые входящие запросы.
      std::string address_{"localhost"};
      // Порт, на котором нужно слушать.
      std::uint16_t port_{8090};
    
      // Минимальная величина задержки перед выдачей ответа.
      milliseconds min_pause_{4000};
      // Максимальная величина задержки перед выдачей ответа.
      milliseconds max_pause_{6000};
    
      // Нужно ли включать трассировку?
      bool tracing_{false};
    };

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

    Детали разбора аргументов командной строки
    // Разбор аргументов командной строки.
    // В случае неудачи порождается исключение.
    auto parse_cmd_line_args(int argc, char ** argv) {
      struct result_t {
        bool help_requested_{false};
        config_t config_;
      };
      result_t result;
      long min_pause{result.config_.min_pause_.count()};
      long max_pause{result.config_.max_pause_.count()};
    
      // Подготавливаем парсер аргументов командной строки.
      using namespace clara;
    
      auto cli = Opt(result.config_.address_, "address")["-a"]["--address"]
            ("address to listen (default: localhost)")
        | Opt(result.config_.port_, "port")["-p"]["--port"]
            ("port to listen (default: 8090)")
        | Opt(min_pause, "minimal pause")["-m"]["--min-pause"]
            ("minimal pause before response, milliseconds")
        | Opt(max_pause, "maximum pause")["-M"]["--max-pause"]
            ("maximal pause before response, milliseconds")
        | Opt(result.config_.tracing_)["-t"]["--tracing"]
            ("turn server tracing ON (default: OFF)")
        | Help(result.help_requested_);
    
      // Выполняем парсинг...
      auto parse_result = cli.parse(Args(argc, argv));
      // ...и бросаем исключение если столкнулись с ошибкой.
      if(!parse_result)
        throw std::runtime_error("Invalid command line: "
            + parse_result.errorMessage());
    
      if(result.help_requested_)
        std::cout << cli << std::endl;
      else {
        // Некоторые аргументы нуждаются в дополнительной проверке.
        if(min_pause <= 0)
          throw std::runtime_error("minimal pause can't be less or equal to 0");
        if(max_pause <= 0)
          throw std::runtime_error("maximal pause can't be less or equal to 0");
        if(max_pause < min_pause)
          throw std::runtime_error("minimal pause can't be less than "
              "maximum pause");
    
        result.config_.min_pause_ = milliseconds{min_pause};
        result.config_.max_pause_ = milliseconds{max_pause};
      }
    
      return result;
    }

    Для разбора мы попробовали использовать новую библиотеку Clara от автора широко известной в узких кругах библиотеки для unit-тестов в C++ под названием Catch2 (в девичестве просто Catch).

    В общем-то здесь ничего сложного за исключением одного фокуса: функция parse_cmd_line_args возвращает экземпляр локально определенной структуры. По хорошему, здесь следовало бы возвращать что-то вроде:

    struct help_requested_t {};
    using cmd_line_args_parsing_result_t = variant<config_t, help_requested_t>;

    Но в C++14 std::variant нет, а тащить какую-то реализацию variant/either из сторонней библиотеки или же полагаться на наличие std::experimental::variant не хотелось. Поэтому сделали вот так. Код, конечно, попахивает, но для слепленной на коленке имитации пойдет.

    Генератор случайных задержек


    Тут вообще все просто, обсуждать, в принципе, нечего. Поэтому просто код. Ради того, чтобы был.

    Реализация pauses_generator_t
    // Вспомогательный тип для генерации случайных задержек.
    class pauses_generator_t {
      std::mt19937 generator_{std::random_device{}()};
      std::uniform_int_distribution<long> distrib_;
      const milliseconds minimal_;
    public:
      pauses_generator_t(milliseconds min, milliseconds max)
        : distrib_{0, (max - min).count()}
        , minimal_{min}
        {}
    
      auto next() {
        return minimal_ + milliseconds{distrib_(generator_)};
      }
    };

    Требуется лишь дергать метод next() когда это нужно и будет возвращена случайная величина в диапазоне [min, max].

    Функция handler()


    Один из ключевых элементов реализации delay_server — это небольшая функция handler(), внутри которой и происходит обработка входящих HTTP-запросов. Вот весь код этой функции:

    // Реализация обработчика запросов.
    restinio::request_handling_status_t handler(
        restinio::asio_ns::io_context & ioctx,
        pauses_generator_t & generator,
        restinio::request_handle_t req) {
      // Выполняем задержку на случайную величину (но в заданных пределах).
      const auto pause = generator.next();
      // Для отсчета задержки используем Asio-таймеры.
      auto timer = std::make_shared<restinio::asio_ns::steady_timer>(ioctx);
      timer->expires_after(pause);
      timer->async_wait([timer, req, pause](const auto & ec) {
          if(!ec) {
            // Таймер успешно сработал, можно генерировать ответ.
            req->create_response()
              .append_header(restinio::http_field::server, "RESTinio hello world server")
              .append_header_date_field()
              .append_header(restinio::http_field::content_type, "text/plain; charset=utf-8")
              .set_body(
                fmt::format("Hello world!\nPause: {}ms.\n", pause.count()))
              .done();
          }
        } );
    
      // Подтверждаем, что мы приняли запрос к обработке и что когда-то
      // мы ответ сгенерируем.
      return restinio::request_accepted();
    }

    Эта функция (посредством лямбды, созданной в main()-е) вызывается каждый раз, как HTTP-сервер принимает входящий GET-запрос на нужный URL. Сам входящий HTTP-запрос передается в параметре req типа restinio::request_handle_t.

    Этот самый restinio::request_handle_t представляет из себя умный указатель на объект с содержимым HTTP-запроса. Что позволяет сохранить значение req и воспользоваться им позже. Именно это и является одним из краеугольных камней в асинхронности RESTinio: RESTinio дергает предоставленный пользователем callback и передает в этот callback экземпляр request_handle_t. Пользователь может либо сразу сформировать HTTP-ответ внутри callback-а (и тогда это будет тривиальная синхронная обработка), либо же может сохранить req у себя или передать req какой-то другой нити. После чего вернуть управление RESTinio. И сформировать ответ позже, когда для этого наступит подходящее время.

    В данном случае создается экземпляр asio::steady_timer и req сохраняется в лямбда-функции, передаваемой в async_wait для таймера. Соответственно, объект HTTP-запроса сохраняется до тех пор, пока не сработает таймер.

    Очень важный момент в handler()-е — это возвращаемое им значение. По возвращаемому значению RESTinio понимает взял ли пользователь ответственность за формирование ответа на запрос или нет. В данном случае возвращается значение request_accepted, что означает, что пользователь пообещал RESTinio сформировать ответ на входящий HTTP-запрос позже.

    А вот если бы handler() возвратил, скажем, request_rejected(), то RESTinio бы закончил обработку запроса и ответил бы пользователю кодом 501.

    Итак, handler() вызывается когда приходит входящий HTTP-запрос на нужный URL (почему именно так рассматривается ниже). В handler-е вычисляется величина задержки для ответа. После чего создается и взводится таймер. Когда таймер сработает, будет сформирован ответ на запрос. Ну и handler() обещает RESTinio сформировать ответ на запрос путем возврата request_accepted.

    Вот, собственно, и все. Маленькая мелочь: для формирования тела ответа используется fmtlib. В принципе, здесь без нее можно было бы и обойтись. Но, во-первых, нам fmtlib очень нравится и мы используем fmtlib при удобном случае. И, во-вторых, нам fmtlib все равно потребовалась в bridge_server-ах, так что не было смысла отказываться от нее в delay_server.

    Функция run_server()


    Функция run_server() отвечает за настройку и запуск HTTP-сервера. Она определяет какие запросы HTTP-сервер будет обрабатывать и как HTTP-сервер будет отвечать на все остальные запросы.

    Так же в run_server() определяется где будет работать HTTP-сервер. Для случая delay_server это будет главная нить приложения.

    Давайте сперва посмотрим на код run_server(), а потом рассмотрим несколько важных моментов, о которых мы еще не говорили.

    Итак, вот код:

    template<typename Server_Traits, typename Handler>
    void run_server(
        restinio::asio_ns::io_context & ioctx,
        const config_t & config,
        Handler && handler) {
      // Сперва создадим и настроим объект express-роутера.
      auto router = std::make_unique<express_router_t>();
      // Вот этот URL мы готовы обрабатывать.
      router->http_get(
          R"(/:year(\d{4})/:month(\d{2})/:day(\d{2}))",
          std::forward<Handler>(handler));
      // На все остальное будем отвечать 404.
      router->non_matched_request_handler([](auto req) {
          return req->create_response(404, "Not found")
              .append_header_date_field()
              .connection_close()
              .done();
        });
    
      restinio::run(ioctx,
          restinio::on_this_thread<Server_Traits>()
            .address(config.address_)
            .port(config.port_)
            .handle_request_timeout(config.max_pause_)
            .request_handler(std::move(router)));
    }

    Что в ней происходит и почему это происходит именно так?

    Во-первых, для delay_server будет использоваться функциональность, аналогичная системе роутинга запросов expressjs. В RESTinio это называется Express router.

    Нужно создать экземпляр объекта, который отвечает за маршрутизацию запросов на основе регулярных выражений. После чего в этот объект нужно поместить список маршрутов и задать каждому маршруту свой обработчик. Что мы и делаем. Создаем обработчик:

    auto router = std::make_unique<express_router_t>();

    И указываем интересующий нас маршрут:

    router->http_get(
          R"(/:year(\d{4})/:month(\d{2})/:day(\d{2}))",
          std::forward<Handler>(handler));

    После чего еще и задаем обработчик для всех остальных запросов. Который просто будет отвечать кодом 404:

    router->non_matched_request_handler([](auto req) {
          return req->create_response(404, "Not found")
              .append_header_date_field()
              .connection_close()
              .done();
        });

    На этом подготовка нужного нам Express router-а завершается.

    Во-вторых, при вызове run() мы указываем, что HTTP-сервер должен использовать заданный io_context и должен работать на той самой нити, на которой и сделали вызов run(). Плюс к тому для сервера задаются параметры из конфигурации (т.к. IP-адрес и порт, максимально допустимое время для обработки запросов и сам обработчик):

    restinio::run(ioctx,
        restinio::on_this_thread<Server_Traits>()
          .address(config.address_)
          .port(config.port_)
          .handle_request_timeout(config.max_pause_)
          .request_handler(std::move(router)));

    Здесь использование on_this_thread как раз и заставляет RESTinio запустить HTTP-сервер на контексте той же самой нити.

    Почему run_server() — это шаблон?


    Функция run_server() является функцией-шаблоном, зависящей от двух параметров:

    template<typename Server_Traits, typename Handler>
    void run_server(
        restinio::asio_ns::io_context & ioctx,
        const config_t & config,
        Handler && handler);

    Для того, чтобы пояснить, почему это так, начнем со второго шаблонного параметра — Handle.

    Внутри main() мы создаем актуальный обработчик запросов в виде лямбда-функции. Реальный тип этой лямбды знает только компилятор. Поэтому для того, чтобы передать лямбду-обработчик в run_server() нам и нужен шаблонный параметр Handle. С его помощью компилятор сам выведет нужный тип аргумента handler в run_server().

    А вот с параметром Server_Traits ситуация чуть посложнее. Дело в том, что HTTP-серверу в RESTinio нужно задать набор свойств, которые будут определять различные аспекты поведения и реализации сервера. Например, будет ли сервер приспособлен к работе в многопоточном режиме. Будет ли сервер выполнять логирование выполняемых им операций и т.д. Все это задается шаблонным параметром Traits для класса restinio::http_server_t. В данном примере этого класса не видно, т.к. экземпляр http_server_t создается внутри run(). Но все равно Traits должны быть заданы. Как раз шаблонный параметр Server_Traits функции run_server() и задает Traits для http_server_t.

    Нам в delay_server потребовалось определить два разных типа Traits:

    // Мы будем использовать express-router. Для простоты определяем псевдоним
    // для нужного типа.
    using express_router_t = restinio::router::express_router_t<>;
    
    // Так же нам потребуются два вспомогательных типа свойств для http-сервера.
    
    // Первый тип для случая, когда трассировка сервера не нужна.
    struct non_traceable_server_traits_t : public restinio::default_single_thread_traits_t {
      using request_handler_t = express_router_t;
    };
    
    // Второй тип для случая, когда трассировка сервера нужна.
    struct traceable_server_traits_t : public restinio::default_single_thread_traits_t {
      using request_handler_t = express_router_t;
      using logger_t = restinio::single_threaded_ostream_logger_t;
    };

    Первый тип, non_traceable_server_traits_t, используется когда сервер не должен логировать свои действия. Второй тип, traceable_server_traits_t, используется когда логирование должно быть.

    Соответственно, внутри функции main(), в зависимости от наличия или отсутствия ключа "-t", функция run_server() вызывается либо с non_traceable_server_traits_t, либо с traceable_server_traits_t:

    // Если должна использоваться трассировка запросов, то должен
    // запускаться один тип сервера.
    if(cfg.config_.tracing_) {
      run_server<traceable_server_traits_t>(
          ioctx, cfg.config_, std::move(actual_handler));
    }
    else {
      // Трассировка не нужна, запускается другой тип сервера.
      run_server<non_traceable_server_traits_t>(
          ioctx, cfg.config_, std::move(actual_handler));
    }

    Так что назначение нужных свойств HTTP-серверу — это еще одна причина того, почему run_server() является функцией-шаблоном.

    Более детально тема Traits для restinio::http_server_t затронута в нашей предыдущей статье о RESTinio.

    Заключение первой части


    Вот, собственно, и все, что можно было рассказать о реализации delay_server-а на базе RESTinio. Надеемся, что описанный материал оказался понятен. Если нет, то с удовольствием ответим на вопросы в комментариях.

    В последующих статьях мы уже будем говорить о примерах интеграции RESTinio и curl_multi, разбирая реализации bridge_server_1 и bridge_server_2. Там части, которые относятся именно к RESTinio, будут не объемнее и не сложнее того, что мы показали в этой статье. А основной объем кода и основная сложность будет проистекать из-за curl_multi. Но это уже совсем другая история…

    Продолжение следует.
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 0

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое