LINUX.ORG.RU

RESTinio: небольшая header-only библиотка для асинхронного REST API на C++

 


1

5

Некоторое время назад мы столкнулись с задачей создания REST API для старой C++ софтины. Особенность была в том, что обработка одного запроса могла занимать секунды, а то и десятки секунд. Мы посмотрели на то, что есть вокруг, остались не сильно довольны и попробовали сделать свое маленькое, кросс-платформенное, header-only, асинхронное решение. Так мы начали делать RESTinio и вот первая публичная альфа-версия.

Если вы разрабатываете REST сервисы на C++, то что вам нравится и чего не хватает в существующих библиотеках (Beast, C++REST SDK, Crow, restbed, proxygen,...)?

Репозиторий: https://bitbucket.org/sobjectizerteam/restinio-0.1


RESTinio 0.2

Вышла новая версия RESTinio 0.2: https://bitbucket.org/sobjectizerteam/restinio-0.2

Что добавилось:

  • Поддержка HTTP pipelining;
  • Поддердка chunked encoding;
  • Роутер для набора хэндлеров (а-ля express.js).

API RESTinio-0.2 не совместим с API RESTinio 0.1, что связано с добавлением новых возможностей.

В дополнение к выходу новой версии был сделан тест производительности разичных фрэймворков для REST на C++: https://bitbucket.org/sobjectizerteam/restinio-benchmark-jun2017

kola
() автор топика
Ответ на: комментарий от eao197

Наверное потому что никому ваша либа не сдалась на таких условиях. 4K строк кода и AGPL, лол.

      29 text files.
      29 unique files.                              
       4 files ignored.

http://cloc.sourceforge.net v 1.60  T=0.09 s (268.2 files/s, 63230.3 lines/s)
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
C/C++ Header                    24            941            947           3991
Ruby                             1              4              0             10
-------------------------------------------------------------------------------
SUM:                            25            945            947           4001
-------------------------------------------------------------------------------
anonymous
()
Ответ на: комментарий от kawaii_neko

Там http_parser на C, который компилировать надо. А в header-only плюсовая часть, которая практически вся на шаблонах и иначе, как в виде header-only ее не сделать.

eao197 ★★★★★
()
Ответ на: комментарий от eao197

Просто есть вот такая хрень: https://github.com/ipkn/crow, тоже http_parser и компилируется десятками секунд.

header only должно умереть. Зачем наружу торчать шаблонами мне в принципе неведомо. Наверное это какое-то паталогическое нежелание (или неспособность?) продумать API. Шаблоны-то в паблик интерфейсе кроме хэндлеров нафиг не сдались.

kawaii_neko ★★★★
()
Ответ на: комментарий от kawaii_neko

Зачем наружу торчать шаблонами мне в принципе неведомо.

Для гибкой настройки под нужды пользователя посредством trait-ов и policy-классов.

eao197 ★★★★★
()
Ответ на: комментарий от eao197

Что там настраивать? Сокет - это сокет, по нему передаются байты. Вот это все перечисленное просто рак, убивший кресты. Вместо того, чтобы эффективно решить задачу, начинают разводить трейты на ровном месте. В итоге 400 строчек компилируются полторы минуты, каждый раз инстанциируя одни и те же никому не нужные дефолтные параметры шаблона.

kawaii_neko ★★★★
()
Последнее исправление: kawaii_neko (всего исправлений: 1)
Ответ на: комментарий от kawaii_neko

шел бы ты отсюда, не позорился, петушок.

anonymous
()
Ответ на: комментарий от kawaii_neko

Что там настраивать?

Например, если RESTinio хотят использовать в однопоточном окружении и без логирования просходящего с соединениями, то посредством шаблонов сервер настраивается так, что там нет ни примитивов синхронизации, ни кода для логирования.

Если есть конкретные вопросы о том, как в RESTinio используются шаблоны, или есть конкретные претензии по поводу того, как в RESTinio шаблоны использовать не нужно, тогда велкам: вас выслушают и вам все расскажут.

Но есть подозрение, что вам хочется лишь какашками в современный C++ побросаться. В таком случае вы темой ошиблись.

eao197 ★★★★★
()
Ответ на: комментарий от eao197

что там нет ни примитивов синхронизации

Начнем с того, зачем там вообще многопоточность и синхронизация? «Прочитать из сокета» и «распарсить HTTP заголовки» — на такой задаче одно ядро способно обслужить около 200-300к запросов в секунду с одного ядра в режиме «echo server».

ни кода для логирования

Обалдеть: 1) весь мир для этого использует loglevels и не жалуется, приятным бонусом все логгирование управляется из конфига; 2) зато в бинарнике присутствует куча дублирующегося кода, порожденного темплейтами (дабы не ходить далеко за примерами, число вариантов идентичных, но дергающих чуточку разные callback-и, schedule_operation_timeout_callback-ов будет велико)

Ну и заодно хочу метнуть кучку говнеца в сторону asio. Конкретно говоря, в сторону его замечательной reactor модели и deadline таймеров: если по какой-то причине (высокая загрузка CPU, или же просто медленная работа обработчиков) срабатывает таймаут, он не посмотрит, что соответствующий ему io handler уже готов к работе и вместо этого выстрелит таймаутом (иногда раньше может «выстрелить» соответствующий обработчик IO, но это редкость).

В итоге все начинает «таймаутить» на ровном месте: вполне реально получить ситуацию, когда deadline timer будет «срабатывать» до начала следующей итерации внутреннего IO цикла, если на сама итерация окончилась в тот момент, к которому deadline timer должен был сработать (это мое предположение — как иначе можно получить «client read timeout» с кучей данных в соответствующем сокете?).

kawaii_neko ★★★★
()
Ответ на: комментарий от kawaii_neko

Начнем с того, зачем там вообще многопоточность и синхронизация?

Во-первых, масштабирование. Во-вторых, обеспечение асинхронной обработки запросов.

«Прочитать из сокета» и «распарсить HTTP заголовки» — на такой задаче одно ядро способно обслужить около 200-300к запросов в секунду с одного ядра в режиме «echo server».

Так никого в реальном мире не интересуют задачи уровня «echo server». Нас, например, интересуют сценарии, когда обработка запроса может занимать от нескольких сотен миллисекунд до нескольких десятков секунд (за счет того, что нужно обратиться к нескольким сторонним сервисам, собрать от них данные, как эти данные обработать).

1) весь мир для этого использует loglevels и не жалуется, приятным бонусом все логгирование управляется из конфига;

Прекрасно. Эта возможность у вас остается. Только вот когда вам вообще не нужно логирование, то у вас его вообще нет. Никаких if(log_level<=current_log_level) и пр. Если вы считаете, что на такие расходы не стоит обращать внимания, то:

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

2) есть те, кто вашу точку зрения не разделяет и не хочет видеть у себя не нужных им if-ов и следов кода для логирования.

если по какой-то причине (высокая загрузка CPU, или же просто медленная работа обработчиков) срабатывает таймаут, он не посмотрит, что соответствующий ему io handler уже готов к работе и вместо этого выстрелит таймаутом

А кто делает по-другому?

eao197 ★★★★★
()
Ответ на: комментарий от eao197

Во-вторых, обеспечение асинхронной обработки запросов.

Каким боком «асинхронная обработка» требует «примитивов синхронизации»?

Так никого в реальном мире не интересуют задачи уровня «echo server».

А как тогда оценивать оверхед, вносимый всем этим фреймворком?

например, интересуют сценарии, когда обработка запроса может занимать от нескольких сотен миллисекунд до нескольких десятков секунд

Ага, то есть и IO придется писать на asio, завернувшись в колбечную лапшу по самые уши? А вот в mordor (да упокоятся его исходники) это сделали намного удобней.

Никаких `if(log_level<=current_log_level)` и пр

Цена этих if-ов околонулевая. Цена параметризации if-ов в шаблонах дает +n секунд ко времени компиляции и +N байт к размеру бинарника за каждый инстанциируемый варинт со «слегка отличным логгированием».

А кто делает по-другому?

Ага. В proactor-е (привет, libev!) все делают по-другому, ведь

  1. можно не проверять на expiration таймер, взведенный в текущей итерации до начала следующей
  2. при наступлении таймаута всегда можно глянуть, а не запланировано ли соответствующее IO событие к исполнению в текущей итерации event loop-а
  3. есть вменяемые приоритеты событий и можно тупо назначать таймаутам более низкий приоритет, тогда второго пункта можно даже не делать
kawaii_neko ★★★★
()
Ответ на: комментарий от kawaii_neko

Каким боком «асинхронная обработка» требует «примитивов синхронизации»?

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

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

А как тогда оценивать оверхед, вносимый всем этим фреймворком?

Зачем? Нет, реально, зачем? Вы делаете прототип решения вашей задачи на фреймворке X, получаете замеры производительности прототипа, если вас устраивает — двигаетесь дальше. Оверхед на «hello_world-ах» к этому будет иметь косвенное отношение.

Ага, то есть и IO придется писать на asio, завернувшись в колбечную лапшу по самые уши?

Какой IO, о чем вы?

Цена этих if-ов околонулевая. Цена параметризации if-ов в шаблонах дает

нулевую цену, потому что if-ов в результирующем бинарнике вообще не будет. Для этого можно лишние n секунд при сборке и потерпеть.

В proactor-е (привет, libev!) все делают по-другому,

Если то, о чем вы рассказываете, станет для нас проблемой, мы либо научимся готовить Asio по-другому, либо возьмем за основу что-нибудь другое. Когда пользователь берет RESTinio, он вообще Asio, как такового, видеть не должен.

eao197 ★★★★★
()
Ответ на: комментарий от eao197

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

Тю, я-то думал, что события можно обрабатывать прямо в потоке сервера, разбирающего HTTP. А тут обычная очередь. И, я так понимаю, предполагается, что в соседних потоках будет пул воркеров, крутящихся в IO? Так это неэффективно.

А если воркеры могут крутиться в чем угодно, то это «что угодно» с вероятностью 100% будет называться «asio», поскольку интеграция двух разных event loop-ов в общем случае не является тривиальной задачей.

Вы делаете прототип решения вашей задачи на фреймворке X

Допустим, дано два фреймворка: X и Y. X поудобнее, Y позаковыристей. Обработчик задачи, скажем, требует 0.05s процессорного времени на запрос, нужно обработать, к примеру, 100 запросов в секунду на 8-ядерной машине.

Итого, приложению на обработку сотни запросов потребуется 500% cpu, а в 300% оставшихся должен уместиться «фреймворочный оверхед». И если X требует 0.04s на обработку запроса, а Y 0.01, то X можно даже не рассматривать.

Какой IO, о чем вы?

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

Это «обращение к сторонним сервисам» называют «IO». И если задача IO bound, то в 2017 году принято заниматься асинхронщиной aka мультиплексирование — обрабатывать множество IO операций внутри одного потока, когда задачи не блокируют друг друга на ожидании IO.

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

kawaii_neko ★★★★
()
Ответ на: комментарий от kawaii_neko

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

Так я вам и говорю: есть вопросы по фреймворку — задавайте, вам на них ответят. А вы придумали себе какую-то картину мира и вещаете про нее, а не про то, что есть на самом деле.

В RESTinio request_handler вызывается на контексте IO-потока. Но request_handler не обязан сразу формировать response, он может сказать, что request принят и response когда-нибудь будет сформирован. Что позволит IO-потоку перейти к обработке других request-ов. А формирование response может быть передано другой рабочей нити.

Но если все это оверкилл и можно сразу же сделать response, то это выполняется request_handler-ом прямо на IO-потоке.

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

поскольку интеграция двух разных event loop-ов в общем случае не является тривиальной задачей.

Не тривиальной != сложной. И, уж тем более, != невозможной.

Итого, приложению на обработку сотни запросов потребуется 500% cpu, а в 300% оставшихся должен уместиться «фреймворочный оверхед». И если X требует 0.04s на обработку запроса, а Y 0.01, то X можно даже не рассматривать.

Это все какая-то херня на уровне маркетингового булшита. Вы можете намерять 500K req/sec на каком-нибудь «hello_world»-е для фреймворка X и 150K req/sec на том же «hello_world»-е для фреймворка Y. Но в реальной задаче выясниться, что в X нет нормального разбора HTTP, нет поддержки keep-alive, нет тайм-аутов для медленных соединений и пр. И когда вы это все наколбасите чтобы ваш продакшен сервер не падал при обслуживании корявых клиентов, то выясниться, что никакими 500K req/sec у вас не пахнет, и даже до 50K req/sec вы едва-едва достигаете. Тогда как в Y это все уже из коробки и в реальном продакшене вы будете блики к тем самим 150K.

Это «обращение к сторонним сервисам» называют «IO».

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

Ну и самый козырный вопрос, о котором тоже «никто никогда не думает»: как поведет себя RESTinio, если клиентское приложение закроет соединение до того, как обработчик приступит к выполнению запроса?

На этот вопрос, полагаю, kola даст более развернутый ответ.

eao197 ★★★★★
()
Ответ на: комментарий от kawaii_neko

Если клиентское приложение закроет соединение ...

Как поведет себя RESTinio, если клиентское приложение закроет соединение до того, как обработчик приступит к выполнению запроса?

Для начала предлагаю прояснить, что происходит с одним клиентом который не дождавшись ответа закрывает соединение. Если для клиента эта ситуация всегда выглядит одинаково — «отправил запрос, а ответа нет...», то на сервере возможны разные варианты.

Процесс обслуживания соединения упрощенно можно представить в следующем виде:

Чтение:

1. Начинаем читать из сокета (говорим asio читай в такой-то буфер и даем callback, с которого начнется обработка того что прийдет).

2. Получив сколько-то байт, скармливаем их в парсер, если парсер на этих данных дочитал сообщение полностью, то переходим к следующему шагу, иначе снова п.1.

3 Вызываем обработчик. Тут возможны две ситуации: ответ может быть сформирован сразу и тогда сразу начнет работать логика записи (назовем это sync-обработка), либо запрос (а с ним и средства для создания ответа, что включает и shared_ptr на весь контекст связанный с connection-ом) будет делегирован в домен пользователя и там «когда-нибудь» он будет обработан и на него будет отправлен ответ (назовем это async-обработка).

4. Если нужен pipelining, то возвращаемся к п.1, иначе ждем когда клиенту будет отправлен ответ.

Запись:

1. Ничего не делается — ждем пока появятся буферы для отправки(это std::string-и с уже сериализованными частями ответа). 2. Получаем буферы, которые можно отправить. Это одно из двух: первое — при sync-обработке вызов логики записи (с этими буферами) будет инициирован из Запись-п.3, второе — при async-обработке вызов этой логики будет в callback поставленного через asio::post(). Когда буферы получены отправляем их клиенту (вызываем asio::async_write и вешаем callback, чтобы при необходимости начать чтение по новой или закрыть соединение.

Тут «затык» может случится в разных местах и то, что получится зависит в первую очередь от контроля таймаутов на различные операции (чтения запроса, обработки запроса, запись ответа):

  • Данные медленно приходят на сервер, тогда соединение постоянно чередует Запись-п.1-2. Если от момента начала чтения проходит время T (таймаут ожидания запроса), то соединение закрывается и весь контекст с ним связанный уничтожается. Если нет таймаутов, то чтение-парсинг идет до победного конца, а значит чем больше таких соединений, тем больше накладных расходов мы несем.
  • Получаем запрос полностью и делаем sync-обработку, которая «много думает». Что же, против лома нет приема, если зависли в пользовательском коде, то ждем-с, когда же нас отпустит, причем со всеми вытекающими: если asio крутится на одной нити, то плохо будет всем. Но, допустим sync-обработка очуняла, тогда мы делаем попытку отправить ответ, и если клиент на своей стороне закрыл соединение, то в callback-е который мы повесили на asio::async_write мы получим ошибку и закроем соединение.
  • Получаем запрос полностью и делаем async-обработку, которая «много думает». В этом варианте хотябы не замирает asio, но развязка примерно такая же, только в этом случае помогает таймаут на обработку: если данных ответа нет в течении времени T (таймаут на обработку), то пользователю отправляется ответ 504 Gateway Timeout, и тогда либо мы получим ошибку в callback-е навешенном на asio::async_write (если пользователь уже закрыл соединение), либо же пользователь получит ответ, а мы в соответствующем callback-е получим error_code=«все нормально» но соединение все равно закроем. Но в любом случае, если запрос (а значит и shared_ptr на контекст соединения) хранится где-то у пользователя, то хотя сокет и будет закрыт но сами «мертвые» структуры данных будут существовать. И тут без черного пояса по хитростям владения объектами в условях многопоточности вряд ли что-то можно сделать. Если же пользователь у себя поддерживает логику, отбрасывания запросов, с которыми он не может справиться, то может оказаться, то все может завершиться раньше, т.е. запрос, который он отвергнет будет единственным хранителем ссылки на контекст соединения, и тогда оно просто закроется (не очень аккуратно, но все же).

    Замечания: если задействуется http-piplining, тогда после прочтения одного запроса начинается чтение следующего, а значит переданный пользователю объект запроса уже не единственный хранитель ссылки. Т.е. все усложняется, но опять таки сработает таймаут на обработку, если он есть, конечно.

  • Мы получили запрос обработали его (тут sync/async не имеет значения) и инициировали asio::async_write, которая долго не завершается (ни ошбики, ни завершения). Тогда если используются таймауты, то должен сработать таймаут на операцию записи, и соединение будет закрыто. Если таймаутов нет, то чем больше таких соединений, тем хуже.
kola
() автор топика
Ответ на: комментарий от eao197

Это все какая-то херня на уровне маркетингового булшита

И тем не менее, результаты даже такого «булшитного» бенчмарка были бы полезными, потому как если фреймворки X и Y предоставляют их для оценки производительности, можно понять, имеет ли смысл смотреть в сторону более сложного, если производительности простого не хватает.

kawaii_neko ★★★★
()

то на сервере возможны разные варианты

К слову, nginx не передает апстримам запросы, если клиент уже закрыл соединение, через которые запросы были отправлены (подозреваю, что он определяет это по получению 0 байт при попытке считать следующий запрос, если это не является индикатором окончания текущего запроса). И сценарий, когда после рестарта куча всего ломится в сервис, отваливается по 5-секундному таймауту, а сервис еще 5 минут «тупит», разгребая накопившуюся никому не нужную кучу запросов, довольно распространена.

Правильно ли я понял, что с момента полного вычитывания запроса (включая тело), RESTinio вызывает нужный handler, а ожидает не более T времени получения ответа на этот запрос? С чем вообще связано наличие этого таймаута?

kawaii_neko ★★★★
()
Ответ на: комментарий от kawaii_neko

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

eao197 ★★★★★
()
Ответ на: комментарий от kawaii_neko

Правильно ли я понял, что с момента полного вычитывания запроса (включая тело), RESTinio вызывает нужный handler, а ожидает не более T времени получения ответа на этот запрос? С чем вообще связано наличие этого таймаута?

Да, если делается async-обработка, то RESTinio ждет ответа, но чтобы не ждать его вечно, есть таймаут на обработку (отсчитывается с момента вызова обработчика). Если ответа нет, то RESTinio старается как-то сгладить ситуацию и отвечает 504 и закрывает соединение.

kola
() автор топика
17 октября 2017 г.

RESTinio 0.3

Вышла новая версия RESTinio: https://bitbucket.org/sobjectizerteam/restinio-0.3

Библиотека теперь распространяется под BSD-3-CLAUSE лицензией. Мы говорим, что она находится в состоянии beta-версии, т.к. не уверены, что ее API в достаточной степени стабилизировался и не претерпит ломающих изменений в будущем. Но сама реализация достаточно стабильна.

Вот как будет выглядеть простейший http-сервер, который отвечает на все запросы hello-world сообщением:

#include <iostream>
#include <restinio/all.hpp>

int main()
{
  restinio::run(
    restinio::on_this_thread() // Run server on this thread.
      .port(8080)
      .address("localhost")
      .request_handler([](auto req) {
        return req->create_response().set_body("Hello, World!").done();
      }));

  return 0;
}

В данном примере обработчик запросов предельно прост, но, конечно, RESTinio дает доступ к о всем параметрам запроса, что позволяет делать более сложные обработчики.

Возможности:

  • Асинхронная обработка запросов. В случаях, когда данные для ответа на запрос не могут быть получены сразу (или почти сразу), то можно сохранить хэндл запроса для дальнейшей обработки (например, в другом контексте исполнения) и вернуться к этому запросу, когда все данные будут готовы.
  • HTTP pipelining. Хорошо работает в связке с асинхронная обработка запросов.
  • Контроль за таймаутами. *RESTinio* может помочь в обработке “плохих” соединений, например из которых приходит «GET /», а затем они просто висят.
  • Построители ответов. Например, если нужно тело chunked-encoding, то в *RESTinio* есть и такой билдер.
  • Поддержка TLS (HTTPS).
  • Базовая поддержка websocket. При помощи restinio::websocket::basic::upgrade() можно начать websocket сессию используя соединение, в котором был получен исходный upgrade-запрос.
  • Может быть запущен на стороннем asio::io_context. RESTinio отделен от контекста исполнения, что, например, позволяет запустить 2 сервера используя один asio::io_context или встраивать RESTinio в существующее приложение построенное на ASIO и для этого не потребуется отдельный io_context.
  • Некоторые настройки для оптимизации. Можно задать дополнительные опции для акцептора и сокета. Если RESTinio работает на пуле, то можно задать чтобы соединения принимались параллельно и/или создание внутренних объектов для работы с соединением создавались отдельно, это позволит быстрее принимать новые соединения.
kola
() автор топика
Ответ на: RESTinio 0.3 от kola

Немного о производительности

Для проверки производительности RESTinio в независимом от нас бенчмарке, я поучаствовал в конкурсе HighloadCup-2017 от Mail.ru. В итоге оказался в финале и занял итоговое 41-е место.

Вот само решение для HighloadCup. В принципе, это практически то самое решение, которое и было использовано в конкурсе, но с двумя небольшими правками:

  • решение для конкурса работало на промежуточной версии RESTinio, у которой API несколько отличался от API RESTinio-0.3, поэтому опубликованный код был адаптирован к версии 0.3.0;
  • после того, как топ-финалисты начали делиться секретами производительности, в код решения был добавлен специальный ключик, активирующий busy-waiting на ASIO (а-ля epoll(0)). Это дает некоторый прирост производительности, но то решение, которое заняло 41-е место в финале busy-waiting не использовало.

По поводу самого решения. Использовалось in-memory хранилище. При загрузке json-файлов использовался честный парсинг, данные в ОП хранились уже в C++ном представлении. Результирующие json-оны генерировались на лету при формировании ответа. Поскольку основные затраты были связаны с ASIO, то возня с предварительной генерацией ответных json-ов никакого выигрыша не давала, а после увеличения объема данных (что было сделано оргами) всех их хранить в памяти уже и не получалось.

Изначально для роутинга запросов использовался expressjs-like роутер из RESTinio, но т.к. шла борьба за latency был написан ручной разбор query_string и определение типа запроса.

Еще для этого конкурса использовался сервер без таймеров:

using traits_t =
  restinio::traits_t<
    restinio::null_timer_factory_t,
    restinio::null_logger_t,
    root_req_handler_t >;

using server_t = restinio::http_server_t< traits_t >;

С таймерами на базе ASIO результат был бы хуже, т.к. в настоящее время таймеры имеют заметные накладные расходы, в следующей версии RESTinio мы с этим будем отдельно разбираться.

kola
() автор топика
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.