RESTinio — это относительно небольшая C++14 библиотека для внедрения HTTP/WebSocket сервера в C++ приложения. Мы старались сделать RESTinio простой в использовании, с высокой степенью кастомизации, с приличной производительностью. И, вроде бы, пока что это получается. Ранее здесь уже были статьи про RESTinio, но в них речь больше шла о том, что и как было сделано в потрохах библиотеки. Сегодня же хочется рассказать о том, что появилось в свежей версии RESTinio, и зачем это появилось. А так же сказать несколько слов о том, почему этот релиз, скорее всего, станет последним большим обновлением в рамках ветки 0.6. И о том, чего хотелось бы достичь при работе над веткой 0.7. Кому интересно, милости прошу под кат. Главной целью, которую мы преследовали начиная в 2017-ом году проект RESTinio, было упрощение написания HTTP-точек входа в C++ приложения. И одним из способов такого упрощения было заимствование лучшего из того, что нас окружало. В частности, в RESTinio мы сделали аналог роутера запросов из ExpressJS. В итоге express_router стал чуть ли не наиболее востребованной из возможностей RESTinio. Но в ExpressJS кроме роутера есть еще важная штука: middleware. И вот её-то мы изначально в RESTinio и не стали переносить. Сперва эта функциональность нам была не нужна. Но по мере взросления RESTinio мы стали сталкиваться с ситуациями, в которых что-то похожее на middleware из ExpressJS было бы полезным. А раз так, то захотелось эти самые middleware поиметь и в RESTinio. Что оказалось далеко не таким простым делом, как хотелось бы. Но давайте обо всем по порядку. Итак, начиная с версии 0.6.13 обработчики запросов в RESTinio можно выстраивать в цепочки. И такие обработчики будут последовательно вызываться для обработки очередного запроса. Движение по цепочке от обработчика до обработчика происходит пока все они возвращают специальное значение not_handled. Если же какой-то из обработчиков возвращает accepted или rejected, то обработка запроса прекращается и оставшиеся в цепочке обработчики не вызываются. Давайте представим себе, что прежде чем начать обрабатывать запрос, нам нужно сделать три действия: Каждое из этих действий теперь может быть представлено в виде отдельного обработчика запросов: Для того, чтобы выстроить эти обработчики в цепочку мы должны сделать несколько шагов. Во-первых, нужно задекларировать новый тип обработчика запросов в свойствах сервера. В составе RESTinio доступно два готовых к использованию типа, один из которых мы и посмотрим в данном примере. Итак, поскольку количество элементов в цепочке у нас строго фиксировано, то используем fixed_size_chain_t: Во-вторых, саму цепочку обработчиков нужно сформировать и отдать серверу при старте: Вот, собственно, и все. RESTinio строился с прицелом именно на асинхронную обработку запросов. Но добавленные в версию 0.6.13 цепочки обработчиков отрабатывают только синхронно. Почему так? Тут надо зайти издалека. Начнем с того, что для RESTinio любой обработчик запроса выглядит как синхронный. Давайте, для простоты, рассмотрим случай, когда RESTinio запускается на одной рабочей нити. На этой нити RESTinio делает все: слушает серверный сокет, принимает новые подключения, вычитывает данные из принятых подключений, разбирает прочитанные данные, вызывает обработчик для полностью принятых запросов, записывает исходящие данные, контролирует тайм-ауты... Соответственно, когда RESTinio вызывает на этой рабочей нити обработчик запроса, то нить, фактически, блокируется до тех пор, пока обработчик не завершит свою работу. Поэтому-то для RESTinio все обработчики синхронные. Как только вызванный обработчик возвращает управление назад, RESTinio получает возможность продолжить свою работу. При этом RESTinio, по большому счету, все равно, что возвратил обработчик: rejected или accepted. Разница между синхронностью и асинхронностью важна для программиста, который пишет обработчик запроса. Программист может либо полностью сформировать ответ прямо внутри обработчика (т.е. выполнить create_response()...done()) и тогда обработка будет синхронной. Либо же может делегировать обработку на какой-то другой рабочий контекст, где в конце-концов и будет вызван done(). Так вот, суть в том, что когда обработчик запроса возвращает accepted, то RESTinio не знает, был ли запрос уже обработан полностью. Или же обработка запроса была кому-то делегирована. Или же часть обработки была выполнена сразу, а оставшаяся часть отложена на какое-то время. Для RESTinio факт возврата accepted из обработчика запросов означает, что пользователь взял на себя ответственность за дальнейшую судьбу запроса. И RESTinio не может строить предположений о том, в каком состоянии находится запрос. Теперь вернемся к цепочкам. Цепочка выглядит для RESTinio всего как один обработчик. Собственно, показанный выше fixed_size_chain_t — это объект, метод которого и вызывается RESTinio. А уже внутри этого метода происходит последовательный вызов заданных пользователем обработчиков. Сам RESTinio про этот последовательный вызов ничего не знает, для RESTinio никакой последовательности нет вообще. Предположим, что один из обработчиков вернул не not_handled, не rejected, а accepted. В каком состоянии находится запрос? Неизвестно. Возможно, обработка запроса делегирована на отдельный рабочий поток и прямо сейчас на этом отдельном рабочем потоке с запросом что-то уже делают. Поэтому обработка цепочки сразу же прерывается. Т.к. небезопасно вызывать следующий обработчик в цепочке, если на самом деле с запросом что-то делают на другом рабочем контексте. Из-за этого в версии 0.6.13 поддерживается только цепочка из синхронных обработчиков. Т.е. эта цепочка вызывается здесь и сейчас, от начала и до конца. Нельзя вызвать первый обработчик из цепочки, затем вернуть управление RESTinio, а потом, где-то и когда-то вызвать второй обработчик из цепочки. Нет, все обработчики из цепочки вызываются здесь и сейчас. И лишь после того, как все они отработают, управление будет возвращено RESTinio. Значит ли это, что внутри цепочки нельзя делегировать обработку кому-то еще? Нет. Обработчик может делегировать обработку запроса на какую-то другую нить. Но после этого обработчик должен возвратить accepted и цепочка будет прервана. Можно ли сделать цепочку из асинхронных обработчиков? Есть ощущение, что можно. В принципе. Но в рамках работ над версией 0.6.13 и при сохранении совместимости в рамках ветки 0.6 у меня не получилось придумать такого способа. Есть одна смутная и не до конца оформившаяся идея, только вот она требует изменения API RESTinio. Выстраивание обработчиков в цепочку дело не хитрое. Гораздо сложнее организовать обмен данными между стадиями обработки запросов. Давайте представим себе, что у нас есть следующая цепочка обработчиков: Первый обработчик должен породить некий объект user_permissions, в котором будет содержаться идентификатор пользователя и информация о его правах. Далее этот объект должен использоваться в permissions_checker-е (для проверки возможности доступа к ресурсу) и в admin_access_logger (для фиксации в журнале). Соответственно, возникает вопрос, как созданный внутри authentification_handler объект сделать доступным в последующих обработчиках? При поиске ответа на этот вопрос было рассмотрено несколько вариантов. В работу пошел вариант, который позволяет встроить пользовательские данные внутрь RESTinio-вского объекта request_t. Выглядит это следующим образом. Сперва пользователь определяет некую структуру/класс, экземпляры которой и должны встраиваться в объект-запрос: Далее нужно создать тип т.н. extra-data-factory, т.е. фабрики для этой самой дополнительной информации: Затем нужно указать тип нашей фабрики в свойствах сервера: Ну и, самое, важное: теперь у обработчиков запросов поменяется формат. Вместо аргумента типа restinio::request_handle_t они будут получать restinio::generic_request_handle_t<per_request_data>: Собственно, это все. Если наша фабрика не содержит внутри себя никаких данных и является DefaultConstructible типом, то ее экземпляр при запуске сервера даже создавать не нужно — она будет создана автоматически. Но вот если фабрика представляет из себя stateful-объект, который требует инициализации, то пользователю придется создать ее самостоятельно. Например: Внутри обработчика запросов доступ к дополнительным данным можно получить посредством метода extra_data у объекта generic_request_t: По сути, начиная с версии 0.6.13, RESTinio работает уже с двумя новыми типами: шаблонным классом generic_request_t<Extra_Data> и шаблонным псевдонимом generic_request_handle_t<Extra_Data> (который есть std::shared_ptr<generic_request_t<Extra_Data>>). А для того, чтобы такое кардинальное нововведение не поломало ранее написанный код, старые имена request_t и request_handle_t теперь являются всего лишь псевдонимами для generic_request_t<no_extra_data_factory_t::data_t> и generic_request_handle_t<no_extra_data_factory_t::data_t>, где no_extra_data_factory_t — это новый тип для фабрики по умолчанию. В restinio::traits_t, restinio::default_traits_t и restinio::default_single_thread_traits_t именно no_extra_data_factory_t используется в качестве extra_data_factory_t. Поэтому старый код, который использует имена request_t и request_handle_t, сохраняет свою работоспособность и требует только лишь перекомпиляции. Выше уже говорилось, что express_router, сделанный по мотивам ExpressJS, является одной из наиболее востребованных возможностей RESTinio. При этом express_router вводит собственный формат для обработчиков запросов. Соответственно, появление extra-data для запроса сказалось и на express_router-е. Если программист хочет использовать extra-data с запросами, которые обрабатываются посредством express_router-а, то ему нужно явно указать express_router-у тип фабрики extra-data. Например: Вот после этого первым аргументом в обработчик запроса для express_router вместо request_handle_t будет generic_request_handle_t<my_traits::extra_data_factory_t::data_t>. Тоже самое относится и к easy_parser_router: Теперь можно сказать несколько слов о том, почему же время жизни ветки 0.6 подходит к концу и зачем начинать ветку 0.7. Причин несколько. Во-первых, RESTinio с самого начала базировался на библиотеке http-parser. Но теперь, кажется, эта библиотека остается без поддержки. Соответственно, какую бы замену для http-parser мы не приготовили (стороннюю или же собственную), это скажется на списке зависимостей RESTinio. Что является достаточным поводом, чтобы сменить номер версии. Во-вторых, в коде RESTinio уже обнаружилось несколько просчетов, которые было бы желательно исправить. Но исправления поломали бы совместимость, поэтому пока что эти исправления не вносились. Однако, рано или поздно с этим нужно было бы что-то делать. Так почему бы не сейчас? В-третьих, есть ряд хотелок, воплощение которых в жизнь, как представляется, слишком затратно в рамках ветки 0.6 с оглядкой на совместимость. К таким хотелкам можно отнести следующие: Как по мне, так вместе все это образует достаточно увесистый набор причин для того, чтобы оставить ветку 0.6 как есть и пойти вперед. Поскольку мы стоим на пороге изменений в функциональности RESTinio, то сейчас отличный момент для того, чтобы высказать свои пожелания к RESTinio. Могу уверить, что любые конструктивные соображения, замечания и предложения будут самым внимательным образом рассмотрены и приняты к сведению. Сразу скажу, что клиента на базе RESTinio мы за свой счет не потянем. Об этом не просите :( Если только просьба не будет подкреплена материально ;) В общем, приглашаю всех желающих высказать свои соображения о функциональности, которую хотелось бы видеть в RESTinio, в Issues или Discussions на GitHub. Или в Google-группу. Ну или можно прямо сюда, в комментарии. 2020-й год подходит к концу. Год был, мягко говоря, непростой. Тем не менее, RESTinio живет и развивается. Если не ошибаюсь, нам удалось выкатить порядка десятка релизов и опубликовать здесь несколько посвященных RESTinio статей. Немалую роль в этом сыграли наши пользователи, которые рискнули выбрать RESTinio для своих задач. А кто-то и нашел время/возможность для обратной связи. Благодаря чему удалось избавить RESTinio от нескольких недостатков и снабдить несколькими фичами. Так что хочу сказать большое спасибо всем, кто интересуется RESTinio. Ваше внимание очень важно для нас. И, конечно же, огромное спасибо всем, кто использует RESTinio. Без вас у этого проекта бы не было развития. Ну и с наступающим Новым Годом! Велосипедостроитель, программист-камикадзе