26 February 2018

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

Open sourceProgrammingC++

Преамбула


Наша команда занимается разработкой небольшого, удобного в использовании, встраиваемого, асинхронного 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. Но это уже совсем другая история…

Продолжение следует.
Tags:c++14httphttp-серверhttp-запросcurllibcurl
Hubs: Open source Programming C++
+16
6.7k 71
Leave a comment
Popular right now
C++ Developer. Professional
December 28, 202060,000 ₽OTUS
Программирование на языке C (Си)
December 14, 202022,990 ₽Специалист.ру
C++ Junior Developer
March 3, 202123,990 ₽Level UP
Machine Learning. Professional
November 26, 202048,000 ₽OTUS