Асинхронные HTTP-запросы на C++: входящие через RESTinio, исходящие через libcurl
https://t.me/Torchik_RuПреамбула
Наша команда занимается разработкой небольшого, удобного в использовании, встраиваемого, асинхронного 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 не хотелось. Поэтому сделали вот так. Код, конечно, попахивает, но для слепленной на коленке имитации пойдет.
Генератор случайных задержек
Тут вообще все просто, обсуждать, в принципе, нечего. Поэтому просто код. Ради того, чтобы был.
// Вспомогательный тип для генерации случайных задержек. 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() является функцией-шаблоном.