Преамбула
Наша команда занимается разработкой небольшого, удобного в использовании, встраиваемого, асинхронного 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};
};
Разбор командной строки довольно таки объемный, поэтому погружаться в него особо не будем. Но желающие могут заглянуть под спойлер, чтобы составить впечатление о происходящем.
Детали разбора аргументов командной строки
Для разбора мы попробовали использовать новую библиотеку Clara от автора широко известной в узких кругах библиотеки для unit-тестов в C++ под названием Catch2 (в девичестве просто Catch).
В общем-то здесь ничего сложного за исключением одного фокуса: функция parse_cmd_line_args возвращает экземпляр локально определенной структуры. По хорошему, здесь следовало бы возвращать что-то вроде:
Но в C++14 std::variant нет, а тащить какую-то реализацию variant/either из сторонней библиотеки или же полагаться на наличие std::experimental::variant не хотелось. Поэтому сделали вот так. Код, конечно, попахивает, но для слепленной на коленке имитации пойдет.
// Разбор аргументов командной строки.
// В случае неудачи порождается исключение.
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
Требуется лишь дергать метод next() когда это нужно и будет возвращена случайная величина в диапазоне [min, max].
// Вспомогательный тип для генерации случайных задержек.
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. Но это уже совсем другая история…
Продолжение следует.