LINUX.ORG.RU

RESTinio: header-only, кросс-платформенная библиотека для встраивания HTTP сервера с удобным express-like маршрутизатором

 , ,


5

6

RESTinio - это header-only, кросс-платформенный инструмент для встраивания HTTP сервера с удобным express-like маршрутизатором и вебсокетами. Главная задачей RESTinio является упрощение асинхронной обработки запросов. Чтобы, грубо говоря, обработчик спокойно мог потратить 15 секунд на формирование ответа, но это бы не влияло на параллельные запросы.

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

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

#include <restinio/all.hpp>

int main()
{
  restinio::run(
    restinio::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 есть и такой билдер.
  • Express-like маршрутизатор. Нужно создать нетривиальное REST API со множеством маршрутов, в которых к тому же заключены параметры? Просто возьмите RESTinio с express-like маршрутизатором, который создан по мотивам известного js-фрэймворка.
  • Поддержка TLS (HTTPS).
  • Базовая поддержка websocket. При помощи restinio::websocket::basic::upgrade() можно начать websocket сессию используя соединение, в котором был получен исходный upgrade-запрос.
  • Может быть запущен на стороннем asio::io_context. RESTinio отделен от контекста исполнения, что, например, позволяет запустить 2 сервера используя один asio::io_context или встраивать RESTinio в существующее приложение построенное на ASIO и для этого не потребуется отдельный io_context.
  • Некоторые настройки для оптимизации. Можно задать дополнительные опции для акцептора и сокета. Если RESTinio работает на пуле, то можно задать чтобы соединения принимались параллельно и/или создание внутренних объектов для работы с соединением создавались отдельно, это позволит быстрее принимать новые соединения.

RESTinio для своей работы использует standalone версию ASIO, а также ряд других хорошо известных open-source библиотек (nodejs/http-parser и fmtlib), а также catch2 для тестов.

RESTinio-0.4 можно рассматривать как стабильную бета версию.

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

Документация: https://stiffstream.com/en/docs/restinio/0.4

Взгляд со стороны, пожелания, предложения и конструктивная критика приветствуются!


Выглядит неплохо и лицензия хорошая. У меня как раз скоро будет задача по реализации REST api. Можно будет попробовать применить.

В каких-нибудь проектах уже применялась?

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

Сравнение с CppCMS, наверное, мало что покажет, т.к. библиотеки служат для решения разных задач.

С libmicrohttpd сравнивать можно, но, боюсь что у меня это отнимет много времени (и выделить его на это я не смогу), т.к. в Cи я не силен. По этой же причине Си библиотеки этого класса не просмотривал. Но возможно добавлю реализацию простого сервера на libmicrohttpd для бенчей: https://bitbucket.org/sobjectizerteam/restinio-benchmark

kola ()

После вопиющих непрофессионализма и невоспитанности проявленных eoe137 или как его там, и тотального разгрома на хабре его неработающего фреймворка на принципиально нарабочей конценции акторов, доверия в чему-то со словами sobjectizer в пути нет. Ну и о многом говорит забившаяся подальше от сообщества в тёмный угол битбакета и меркуриала тёплая ламповая маргинальность.

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

Как казал бы Николай Николаевич Дроздов - Оъ. В высшей степени воспитанный человек, немногие так могут.

Суть вашего высказывания, по большому счету, — оффтопик. Отмечу, что сторонний читатель может сам уточнить правдивость констатирующей части.

А по единственному вопросу хоть и неявному, есть зеркало на гитхабе: https://github.com/Stiffstream/restinio, т.е. в самом освещенном самим солнцем месте!

kola ()

Сначала, когда прочитал описание к фреймворку, я подумал «О! круто! наконец-то на родном С++ сделали асинхронный вебсервер с адекватным удобным синтаксисом!»

Потом я спросил себя: «Но зачем?» Питон, пхп, руби и другие и так это умеют почти из коробки. Может вопрос в реальной скорости работы сервера и на с++ он будет работать на порядки быстрее, чем аналогичный на питоне, например, тот же старый торнадо? Да вроде не должен - обработка строк везде идет побайтово, а это несоптимизируешь сильно и на это тратится очень много времени работы сервера. Ну хорошо, сервер напрямую никак не связан с обработкой строк - за это отвечать будет pcre/еще что-нибудь. Тогда получается, что обмениваться будем не строками, а бинарными данными. Но тогда здесь нужен не прикладной уровень ISO/OSI, а че-нить пониже и прогать надо поближе к железу, а это в фреймворке никак не может быть представлено из-за того, что использованные зависимости изначально работают в прикладном уровне.

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

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

Представте, что у вас уже есть С++ приложение или вы будете создавать С++ приложение (и С++ выбран в качестве языка реализации по ряду причин, на которые никак не повлияешь), и вам надо (или хорошо бы) выставлять какой-то REST API из этого приложения, одним из вариантов будет как раз встраивание http сервера в само приложение.

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

Это какие-то жесткие рамки для проекта. Но одних таких чуваков я только что вспомнил. Есть такой проект ROOT.CERN. Там ребята года полтора назад писали свой хттп-сервер на плюсах. Им это нужно было, чтобы код, который обрабатывает события с коллайдера выглядел везде одинаково, работал быстро и легко разворачивался. Потому они решили использовать cling - интерпретатор С++. А чтобы прямо из этой же проги представлять результаты в онлайн-режиме и писался этот сервер. До чего у них там развилось - я не знаю, не заглядывал с того момента. Может написали крутую штуку, может, решили забить и использовать питоновскую обертку. А может и пригодится им RESTinio... В общем, скачай да посмотри, если интересно. Может, в ЦЕРН устроишься=) Вот к этой штуке доки.

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

а если обработчик предполагается писать императивно, тогда простите, как достигается одновременная возможность работы заявленного количества воркеров (как воркер «спит» в ожидании io)? не подвержены ли кресты тому самому callback-hell?

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

Бенчи есть: https://bitbucket.org/sobjectizerteam/restinio-benchmark

узкое место давно не фреймворк

Это, видимо, все-таки зависит. Вот, для примера, возьмем условия с highloadcup.ru (подробнее), и в итоге все топы скатились в голый epoll/timeout=0 (busy wait), хотя в начале фрэймворков было много. А БД, которая была нужнав каком-то виде, мало влияла на результат и там были и велосипеды и production-ready in-memory хранилища.

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

пусть хранилище у нас даже в озу, у себя я пишу await websocket.send(dumps(event)), очевидно здесь это обернуто в саму библиотеку, построение ответа идет синхронно или я чего-то не понимаю.

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

я пишу await websocket.send(dumps(event)), очевидно здесь это обернуто в саму библиотеку, построение ответа идет синхронно или я чего-то не понимаю.

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

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

или я жду сообщений от вебсокета, это не req/res, где маленькая процедура активируется в ответ на сообщение и просто шлет его назад (понятно что одновременно байты перекладывать невозможно и потому этим можно принебречь), получается лямбда (или что там) на которую идет роут это одна такая большая синхронная функция, которая должна откуда-то что-то взять и куда-то положить и считается что она отработает и все либо умрет через 30 секунд, как в php когда-то?

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

дело в том что читал недавно про sobjectizer и отметил что код выглядет «лапшевато» и чем-то напоминает древнюю nodejs, и вот мне интерестно как практикующему асинхронную разработчку как на сервере так и на клиенте, это попытка хайпонуть или действительно достояная внимания библиотека?

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

или вот например я жду от редиса сообщение и делаю его broadcast по подключенным вебсокетам, выходит мне нужна еще тулза для управление задачами в фоне?

Давайте помедленее ;)

1. У нас есть N подключенных ws;

2. Есть условный источник данных (redis/mq/...), и мы можем вписать свой код в событие, когда родилось нечто из данных

3. Надо сделать бродкаст на подключенные ws'= filter(event_data, ws);

Из первого вытекает, что в каком-то виде привязки (в конечном итоге и к tcp-сокету) надо как-то и где-то хранить и при этом надо уметь добавлять/удалять наши ws, а в сиду второго условия надо предоставлять доступ к ним, чтобы из нашего кода (callback например), который срабатывает при появлении данных, мы могли бы отправить сообщения в ws. Попутно надо решать вопрос с синхронизацией добавления/удаления/доступа к элементам в хранилища наших ws. Если все это решено, то можно осуществить пункт 3.

Является ли средство, за счет которого, при появлении данных в источнике, наш код получает управление в следствии чего можно сделать бродкаст тулзой для управление задачами в фоне? Если да, тогда она нужна, если это предоставляет сам источник данных (драйвер к MQ, например), то, похоже, что — нет.

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

ну так вот, это просто список клиентов в памяти, и когда происходит ошибка записи в сокет - клиент удаляется из этого списка (со стороны браузера возобновляется соединение, это все обычная практика). только вот мне надо не одному пользователю передать сообщение, а передать всем пользователям, и генерируется этот вызов так же в одном из обработчиков сообщений. асинхронный фреймворк, это значит что каждый такой вызов в итоге будет обернут в асинхронную обертку и посажен на луп, в котором выполнится, а каждый await это прерывание контекста, что позволяет писать код в синхронном стиле, избегая лапши из колбеков и прочих обещаний в открытом виде, это как минимум. у вас же получается, что каждый такой вызов (каждый await) это отдельный обработчик, и возможности «уснуть» у него нет, только проверять поллингом со стороны клиента статус, получается?

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

чуешь разницу между

clients = []
async def websocket(websocket):
  clients[websocket.id] = websocket
  command1 = await websocket.recv()
  data1 = await db.find({command: command1})
  [await client.send(data1) for client in clients]


сколько здесь агентов? а если у меня три реплики, админку я пишу во вторую реплику, транзакции пишу в мастер, читаю из третьей реплики?

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

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

Так поинт в чем? Согласно этого определения, restinio не будет асинхронным.

Значит ли это что заявленный термин употреблен не к месту? Я так не считаю, в C++ понятие асинхронности несколько иное, и пока корутины отложены на C++20, он скорее всего таким и сохранится.

RESTinio в том понимании, что от обработчика запроса (который дергает restinio и который предоставляется пользователем) не требуется чтобы ответ был готов к моменту завершения обработчика, как в некоторых аналогичных библиотеках, где ответ одидается в виде возвращаемого значения или является одним из аргументов обработчика и если его не заполнить, то будет ошибка и либо сама ответит что-то типа internal server error. C restinio объект с запросом можно передавать в другой контекст и сделать его обработку как-нибудь потом, когда это будет удобно/возможно.

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

оброаботку сделать потом, но как потом вернуть ее результат в keep-alive соединение? вообщем ясно все, неясно только к чему те бенчмарки, printf(«Content-Type: text/html;\n\nHello») не велика задача в поток сделать.

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

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

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

но как потом вернуть ее результат в keep-alive соединение?

Запрос является shared_ptr-ом на контекст вокруг tcp-сокета со всей обвязкой для того чтобы обслуживать соединение в режиме http-сервера. Его можно мувать. Например, все обработчики складывают запросы в очередь, на отдельно нити идет их обработка. Эта обработка сотоит в следующем: берем 1000 или все (если их меньше) запросы из очереди, вытягиваем из них параметр(ы), например — topic_id, обращаемся к БД и одним запросом получаем данные для ответов по всей пачке, далее в цикле пишем ответ.

Схематично как-то так:

auto requests_to_handle = req_queue.pop_requests(1000);

auto ids = get_ids(requests_to_handle);

auto resp_data = m_db.get_items(ids);

for(const auto & r : requests_to_handle)
{ 
  auto it = std::find_if(resp_data.begin(),resp_data.end(),[&]( auto d ){ return d.m_id == r.m_id } );

  if(resp_data.end() != it)
  {
     r.m_req_handle->create_response()
        .append_header_date_field()
        .set_body( create_resp_body(*it))
        .done();
  }
  else
  {
    r.m_req_handle->create_response(404, "Not found")
        .append_header_date_field()
        .connection_close()
        .done();

  }
}

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

а если обработчик предполагается писать императивно, тогда простите, как достигается одновременная возможность работы заявленного количества воркеров (как воркер «спит» в ожидании io)?

RESTinio не берет на себя задачу управления рабочими контекстами. Это пользователь должен решить:

a) на каком контексте (контекстах) будет выполняться event-loop Asio. На этом же контексте будет вызываться заданный пользователем request_handler.

b) на каком контексте (контекстах) будет выполняться актуальная обработка http-запросов. Т.е. RESTinio дернет request_handler на контексте из пункта a), но request_handler может передать объект request_handle на другую рабочую нить с тем, чтобы запрос был обработан в другом месте асинхронно.

За все это отвечает пользователь. RESTinio лишь немного помогает разработчику с пунктом a), предоставляя restinio::run + restinio::on_this_thread и restinio::on_thread_pool.

дело в том что читал недавно про sobjectizer и отметил что код выглядет «лапшевато» и чем-то напоминает древнюю nodejs, и вот мне интерестно как практикующему асинхронную разработчку как на сервере так и на клиенте, это попытка хайпонуть или действительно достояная внимания библиотека?

SObjectizer и RESTinio — это два совершенно разных проекта/продукта. Общего между ними лишь то, что их делает одна команда и что SObjectizer и RESTinio можно объединять в рамках одного приложения (собственно, поэтому в RESTinio и есть пример интеграции с SObjectizer, хотя сама реализация RESTinio от SObjectizer никак не зависит).

Если под «практикующему асинхронную разработчку как на сервере так и на клиенте» вы подразумеваете Web, то SObjectizer вам не нужен.

чуешь разницу между

Между чем и чем здесь должна быть разница? Между динамически-типизированным Python-ом и статически-типизированным C++, в который async/await пока еще не завезли (и не понятно, когда stackful conroutines завезут вообще)?

У нас не было задачи сделать C++ный фреймворк, который был бы выразительнее, чем web-фреймворки для Python, Ruby, Erlang, PHP, JS, Clojure, Akka, C# и т.п. Мы делали C++ фреймворк, который был бы выразительнее других C++ фреймворков. Поэтому сравнивать C++ный код с кодом на Python, может быть и интересно в рамках праздного разговора на LOR-е, но для нас это не имеет никакого смысла.

неясно только к чему те бенчмарки, printf(«Content-Type: text/html;\n\nHello») не велика задача в поток сделать.

Бенчмарки, во-первых, показывают накладные расходы самого фреймворка. Может быть «не велика задача», но те же C++ REST SDK и RestBed, будучи достаточно продвинутыми по возможностям, в таких простых бенчмарках под Unix-ами показывают, что накладные расходы там велики.

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

я ожидал большего от современной кресто-вебни, если честно

Ну простите, что не оправдали ваших ожиданий.

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

Это какие-то жесткие рамки для проекта.

Тем не менее, такие ситуации встречаются. Мы, например, взялись делать RESTinio после того, как на горизонте в очередной раз замаячила задача выставить REST API для написанных на С++ и уже работающих компонентов. Причем фокус там был в том, что ответ на запрос не мог быть сформирован раньше, чем через несколько секунд (т.к. этот C++ный компонент взаимодействовал с другими компонентами, а это взаимодействие в принципе было не быстрым). Поэтому нам нужна была именно асинхронная обработка, чтобы event-loop, на котором идет работа с сокетами, не блокировался на время выполнения запроса.

ЕМНИП, в анонсе RESTinio-0.3 на reddit-е один из комментаторов сказал, что ему приходится интегрировать сложную вычислительную часть на C++ с Node.JS для того, чтобы нода делала Web-морду для C++ной части. И его это сильно запарило, поэтому он смотрит в сторону именно C++ных фреймворков, в которых интеграция C++ной вычислительной части и C++ной Web-части была бы проще и эффективнее.

Так что подобные «жесткие рамки» есть. Но то, что на фоне общего Web-а их капля в море — это да.

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

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

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

Я просто пытаюсь понять. Хорошо, вот я значит делаю тяжелые вычисления и они потенциально займут весь ресурс. то-есть там тупо while 1: sleep(1), такой воркер получается заблокирует всю систему или нет? На высоком уровне луп тоже в одном потоке крутится и это и есть профит, в том что между асинхронными вызовами не нужно шарить стейт, но как тогда будет эффективно работать система с долгими воркероами если один из них заблокирует работу остальных?

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

просто говорю что крестовики называют «асинхронным» нечто совсем другое, у вас выходит и это не имеет ничего общего с асинхронным программированием высокого уровня, увы.

Асинхронность — это когда на контексте T вы начинаете некую операцию A, после чего продолжаете работать на контексте T не дожидаясь завершения этой операции A. Результат операции A вы получаете тогда, когда это вам нужно, возможно, уже совсем в другом месте.

Реализация асинхронности может выглядеть по-разному. Может быть исполнение операции A будет передано параллельной нити P. Может быть операция A вообще не будет выполнена до тех пор, пока нить T не запросит результат явно.

Синтаксис асинхронных операций так же может быть совсем разный, особенно если в одном языке есть встроенная поддержка async/await, а в другом языке таковой нет.

Мы говорим, что RESTinio позволяет делать асинхронную обработку запросов потому, что в RESTinio изначально есть возможность делегировать выполнение обработке другой рабочей нити, но при это сразу же вернув значение request_accepted из обработчика запроса. Получается, что на нити с event-loop-ом вызывается обработчик, этот обработчик делегирует выполнение запроса другой нити, сразу возвращает request_accepted и нить с event-loop-ом переходит к выполнению следующего запроса. Когда другая нить завершит обработку первого запроса, то готовые к отправке данные попадут на нить с event-loop-ом, где и произойдет их запись в соответствующий сокет. Это чистой воды асинхронная обработка.

RESTinio при этом не берет на себя никаких забот по созданию рабочих контекстов для обработки запросов. Можно было бы, например, заложиться на использование stackful coroutines (в той или иной их реализации). Тогда бы RESTinio сам создавал для каждого соединения отдельную короутину и тогда бы код с короутинами мог бы выглядеть похожим на тот пример, который вы привели для Python-а.

Но мы не стали закладываться на использование короутин (пока).

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

Хорошо, вот я значит делаю тяжелые вычисления и они потенциально займут весь ресурс. то-есть там тупо while 1: sleep(1), такой воркер получается заблокирует всю систему или нет?

Что понимается под «всей системой»?

Представьте, что вы создаете 10 рабочих потоков внутри своей программы. Если только один из них войдет в бесконечный цикл, то остальные нити все равно продолжат свою работу и будут получать кванты времени от диспетчера ОС. И ваше приложение более-менее, но будет работать.

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

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

Тут я не понял о чем вы спрашиваете :(

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

Хорошо, я понял, тогда последнее уточнение: если вот в фоне операция завершилась и пора слать ответ, но этот ответ большой а канал медленный, выходит если за 30 сек он не уйдет получателю то передача будет прервана, причем эти 30 секунд все остальные будут ждать освобождения лупа, и если такая ситуация возможна то ее обработка так же ложиться на плечи разработчика, например бить ответ на чанки и постепенно слать из спец, актора?

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

У вас в команде разнарядка некая существует?

Кто в чем лучше разбирается, тот о том и постит. В SO-5 сейчас я ведущий разработчик. А в RESTinio — kola. По отношению к RESTinio я в большей степени проджект-менеджер, нежели разработчик.

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

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

Все остальные не будут ждать, поскольку используется неблокирующий ввод-вывод. Мы дергаем Asio и говорим: «запиши вот эти данные в канал», Asio нам отвечает: «Яволь! Сообщу, когда будет сделано». После чего нить event-loop-а освобождается для записи ответа в другой сокет (где так же будет начата неблокирующая операция записи). Соответственно, те 30 секунд, которые сетевая подсистема ОС будет записывать большой ответ в конкретный канал, будут потрачены event-loop-ом не на ожидание результата, а на обработку других каналов.

eao197 ★★★★★ ()