В предыдущей статье про декларативный JSON-сериализатор я рассказал, как с помощью шаблонов C++ можно описывать структуры данных и сериализовать их. Это очень удобно, т.к. не только сокращает размер кода, но и минимизирует количество возможных ошибок.Концепция — если код компилируется, то он работает. Примерно такой же подход применен и в wjrpc, о которой речь пойдет в этой статье. Но поскольку wjrpc “выдран” из фреймворка, под интерфейсы которого он был разработан, я также затрону вопросы архитектуры и асинхронных интерфейсов. Я не буду описывать JSON-сериализатор, на котором работает wjrpc и с помощью которого осуществляется JSON-описание для структур данных сообщений. Про wjson я рассказал в предыдущей статье. Прежде чем рассматривать декларативный вариант описания API сервисов для JSON-RPC, рассмотрим, как можно реализовать разбор в “вручную”. Это потребует написания большего количества run-time кода по извлечению данных и проверок, но он проще для понимания. Все примеры вы можете найти в разделе examples проекта. В качестве примера рассмотрим API простого калькулятора, который может выполнять операции сложения, вычитания, умножения и деления. Сами запросы тривиальны, поэтому приведу только код только для операции сложения: Я предпочитаю структуры для каждой пары запрос-ответ размещать в отдельном файле в специально отведенной для этого папке. Это здорово облегчает навигацию по проекту. Использование пространств имен и определение вспомогательных типов, как показано в примере выше, не обязательно, но повышает читабельность кода. Смысл этих typedef-ов поясню позднее. Для каждого запроса создаем описание JSON. Зачем помещать его в структуру, я рассказывал в статье, посвященной wjson. Отмечу только, что здесь определения typedef-ов требуется, чтобы эти структуры были распознаны как json-описание. Обработку JSON-RPC сообщений можно разделить на два этапа. На первом этапе нужно определить тип запроса и имя метода, а на втором десериализовать параметры для этого метода. При таком описании в поля request::params и request::id json будет скопирован как есть, без какого-либо преобразования, а в поле request::method будет собственно имя метода. Определив имя метода, можем десериализовать параметры описанными выше структурами. Чтобы определить имя метода, не обязательно десериализовать весь запрос в промежуточную структуру данных. Достаточно его распарсить, а десериализовать только кусок запроса, относящийся к полю params. Это можно сделать с помощью wjson::parser напрямую, но wjson предоставляет также конструкцию raw_pair (в предыдущей статье я ее не рассматривал), которая позволит не десериализовать элементы, а запомнить его расположение во входном буфере. Рассмотрим, как это реализовано в wjrpc. Начнем с того, что wjrpc не работает со строками std::string, а определяет следующие типы: Можно рассматривать data_ptr как перемещаемый буфер, использование которого позволяет нам гарантировать, что данные лишний раз не будут скопированы по недосмотру. Любое входящее сообщение wjrpc попытается десериализовать в следующую структуру: Все элементы wjrpc::incoming представляют собой пары итераторов во входном буфере. Например, method.first при десериализации будет указывать на кавычку, которая открывает имя метода, после двоеточия, во входном запросе, а method.second — на следующую позицию после закрывающей кавычки. Эта структура описывает также не только запрос, но и ответ на запрос, а также сообщение об ошибке. Определить тип сообщения достаточно просто по заполненным полям. JSON -описание для такой структуры: Здесь нет полноценной десериализации — это по сути парсинг с запоминанием позиций во входном буфере. Очевидно, что после такой десериализации данные будут валидны, только пока существует входной буфер. Класс wjrpc::incoming_holder захватывает буфер с запросом и парсит его в описанную выше структуру. Я не буду подробно описывать его интерфейс, но покажу, как можно его использовать для реализации JSON-RPC. Здесь большую часть кода занимает формирование ответа, потому что не делаем проверок на ошибки. Сначала инициализируем incoming_holder строкой и парсим ее. На этом этапе входная строка десериализуется в описанную выше структуру incoming. Если строка содержит любой валидный json-объект, то этот этап пройдет без ошибок. Далее нужно определить тип запроса. Это легко сделать по наличию или отсутствию полей “method”, “result”, “error” и “id”. Если не выполняется ни одно из условий, то очевидно, что запрос неправильный. Здесь большая часть кода — это обработка ошибок, а точнее, формирования соответствующего сообщения. Но для всех типов ошибок код похожий, отличия только в типе ошибки. Можно сделать одну шаблонную функцию для сериализации всех типов ошибок. Для формирования сообщения об ошибке нам достаточно знать тип ошибки и идентификатор вызова, если он есть. Объект inholder перемещаемый и после формирования сообщения он больше не нужен. В примере он используется только для извлечения идентификатора вызова, но у него также можно “забрать” входной буфер — для сериализации туда сообщения, чтобы не создавать новый. Аналогичным образом можно и реализовать сериализацию результата. Но прежде чем продолжить избавляться от однотипного кода, приведем в порядок прикладную часть, которая здесь несколько затерялась и представлена всего одной строкой для каждого метода. Как я уже говорил, wjrpc выдран из фреймворка, в котором для компонентов нужно явно определять интерфейс. Причем, это не просто структура исключительно с чистыми виртуальными методами, но и предъявляются определенные ограничения к параметрам методов. Все входные и выходные параметры должны быть объединены в структуры, даже если там будет только одно поле. Это удобно не только для формирования json-описания для запроса, когда десериализованную структуру можем передать напрямую в метод, без предварительного преобразования, но и с позиции расширяемости. Например, во всех методах мы возвращаем число — результат выполнения операции. Смысл описывать результат структурой с одним полем, если можно числом? А входные параметры вообще можно было передать массивом (позиционными параметрами). Но теперь представьте, что требования немного изменились, и к списку параметров нужно добавить сопроводительную информацию, либо в ответе, для операции деления, флаг, означающий, что произошло деление на ноль. В этом случае для внесения исправлений нужно “перетряхнуть” не только интерфейс и все его реализации, но и все места, где он используется. Если у нас JSON-RPC сервис, то изменения коснутся не только сервера, но и клиента, интерфейсы которых нужно как-то согласовывать. А в случае со структурами достаточно просто добавить поле в структуру. Причем, обновлять клиентскую часть и серверную часть можно независимо. Еще одна особенность фреймворка в том, что все методы имеют асинхронный интерфейс, т.е. результат возвращается не напрямую, а через функцию обратного вызова. А чтобы избежать ошибок непреднамеренного копирования, входной и выходной объект описывается как std::unique_ptr<>. Для нашего калькулятора, с учетом описанных ограничений, получается вот такой интерфейс: С учетом вспомогательных typedef-ов, которые мы определили во входных структурах, выглядит вполне симпатично. Но вот реализация для подобных интерфейсов может быть достаточно объемной относительно простых примеров. Нужно удостовериться, что входной запрос не nullptr, а также проверить функцию обратного вызова. Если она не определена, то очевидно, что это уведомление, и в данном случае вызов нужно просто проигнорировать. Этот функционал легко шаблонизировать, как показано в примере реализации нашего калькулятора Статья все же не про реализацию калькулятора, поэтому представленный выше код не стоит воспринимать как best practice. Пример вызова метода: К теме интерфейсов еще вернемся, а сейчас продолжим “сокращать” код нашего сервиса. Для того, чтобы асинхронно сериализовать результат, необходимо “захватить” входящий запрос Т.к. incoming_holder перемещаемый, то чтобы его “захватить”, перемещаем его в std::shared_ptr. Здесь показано, как можно забрать у него буфер, но в данном случае это не имеет особого смысла — все равно результат помещаем в список строк. Захват res_list по ссылке — это только для примера, т.к. знаем, что запрос будет выполнен синхронно. Мы уже написали шаблонную функцию для сериализации ошибок, сделаем аналогичную для ответов. Но для этого, кроме типа результата, нужно передать его значение и json-описание. Здесь шаблонный параметр нужно указывать явно — это json-описание для структуры ответа, из которого можно взять тип описываемой структуры. С использованием этой функции код для каждого JSON-RPC метода существенно упростится Для каждого метода код сократился до минимума, но и этого мне мало. Получение параметров и проверка на ошибку — тоже однотипный код. Это уже почти так, как реализовано в wjrpc. В результате код демо примера сократится до минимума (здесь уже можно привести реализацию всех методов) Такое маниакальное желание сократить run-time объем кода обусловлено несколькими причинами. Убирая лишний if с помощью достаточно сложной конструкции, мы не только сокращаем объем кода, но и убираем потенциальные места, где программисту будет в радость наговнокодить. А еще программисты любят копипастить, особенно неинтересный код, связанный сериализацией, размазывая его по всему проекту, а то и привнося в другие проекты. Лень — двигатель прогресса, когда она заставляет придумывать человека то, что позволит ему меньше работать. Но не в том случае, когда дела откладываются на потом. Казалось бы, тривиальная проверка, но со словами — это же пока прототип, написание пары строк кода откладывается, потом забывается и одновременно копипастится по всему проекту, а также подхватывается другими программистами. На самом деле мы еще не начали рассматривать wjrpc. Я показал, как описываются запросы с помощью wjson, описал интерфейс и прикладную логику тестового примера, и рассмотрел, как можно было бы реализовать JSON-RPC вручную, чтобы было понимание, как он работает изнутри и почему. А вот полный пример на wjrpc: С помощью JSONRPC_TAG задаются имена методов для передачи в качестве шаблонных параметров, аналогично JSONNAME в wjson, разница только в именах сущностей, которые вместо префикса n, обрамляются символами подчеркивания. Далее с помощью wjrpc::method_list и wjrpc::invoke_method описываем все доступные методы. Список методов передаем в обработчик wjrpc::handler. Наряду с методами в списке был описан тип интерфейса объекта, с которым обработчик будет работать. Основным методом обработки является perform_io, который работает с wjrpc::data_ptr. Очевидно, что для синхронных серверов асинхронный интерфейс абсолютно не нужен. В этом случае, да и вообще, если не хотите привязываться к интерфейсу, нужно для каждого метода определить обработчик Здесь handler обработчик ответа, который первым параметром принимает собственно ответ, а вторым сообщения об ошибке. А t — это ссылка на объект JSON-RPC-обработчика (по аналогии self в python). Включается он в список методов следующим образом: Как можно догадаться, invoke_method<> это обертка для wjrpc::method<>, которая использует обработчик метода: Мы долгое время в своих проектах не использовали асинхронные интерфейсы, да и сама реализация JSON-RPC была несколько проще, но для каждого метода приходилось писать вот такой обработчик, основная задача которого вызвать метод соответствующего класса прикладной логики и отправить ответ. Но проблема таких обработчиков не в том, что они есть (а ведь можно и без них), а в том, что программисты часто (да почти всегда) начинают переносить туда часть прикладной логики. Абсолютно по разным причинам. Не проработан интерфейс, просто скопипастил откуда-то код, лень подумать и прочее. Зачастую на этапах прототипирования логика не намного сложнее нашего примера. Так зачем плодить сущности, продумывать инициализацию системы и пр? Пару-тройку синглетонов и впендюриваем логику прямо в обработчик. Тем более, не надо думать, как ошибку протаскивать. Потом все это обрастает кодом и потихоньку все забывают, что это прототип, и он уезжает в продакшн. Поддерживать такой код очень тяжело, невозможно покрыть тестами, проще переписать с нуля, чем отрефакторить подобный проект. В какой-то момент пришла идея “обязать” использовать интерфейсы и стандартизировать их. Благодаря этому появилась возможность сделать обертку типа wjrpc::invoke_method. На этой концепции построен весь фреймворк, а не только JSON-RPC. Концепция заключается в максимальном ограничении возможностей для “творчества” программисту в слоях, которые не связаны с прикладной логикой. Если вам не нужна асинхронность, то можно также проработать стандарт интерфейса и написать обертку типа wjrpc::invoke_method. Объект wjrpc::handler<> обычно создается в контексте соединения и имеет такое же время жизни. Если callback, например, нужен объект соединения, чтобы отправить ответ, то нужно поставить защиту на случай, если вызов произойдет после уничтожения объекта. Второй вариант не подходит, т.к. происходит захват объекта соединения на неопределенное время со всеми ресурсами. Вариант уже лучше, но мы не можем повторно использовать такой объект (например, для пула объектов). Для идентификации объекта можно использовать умный указатель произвольного типа, который нужно просто пересоздать, чтобы отправленные гулять по системе callback-и стали больше не актуальны. Для унификации подхода можно разработать вспомогательные классы (нет в wjrpc) Но это еще не все проблемы с функциями обратного вызова. Они должны быть вызваны обязательно и вызваны ровно один раз. Я не буду сейчас на этом подробно останавливаться, просто имейте это в виду. Напомню, что эта библиотека вырвана из фреймворка, и там эти проблемы так или иначе решены. Связываться или нет с асинхронностью, решать вам. Движок wjrpc::engine – это, по сути, реестр jsonrpc-обработчиков, который позволяет вынести их в отдельный модуль и управлять временем жизни. Например, входящий поток сообщений можно распределить по очередям с разными приоритетами, исходя из имен методов, и только потом передать в wjrpc::engine. Также он используется для реализации удаленных вызовов к другим сервисам. Для входящих запросов объект соединения захватывается callback-ом, который может гулять неопределенно долго по системе, поэтому он не нужен. Но для исходящих запросов нужен wjrpc::engine, чтобы связать сущность, отправившую запрос с обработчиком, чтобы доставить ответ, когда он придет. Для демонстрации удаленных вызовов сначала разработаем проксирующий объект, который также реализует интерфейс icalc. Это будет такой злобный прокси, который меняет входные и выходные значения для метода plus, инкрементируя их. Как видно, прокси инициализируется указателем на интерфейс icalc, куда перенаправляет все запросы как есть, кроме метода plus, в котором реализована его “злобность”. Также производится проверка входящих запросов и игнорируются все уведомления и нулевые запросы. Благодаря асинхронному интерфейсу нашему прокси абсолютно не важно, как будет обработан запрос: синхронно или асинхронно, как придет ответ, тогда он и сделает свои злобные дела. Запрос может быть поставлен в очередь, или может быть выстроена целая цепочка из таких прокси, или этот запрос может быть отправлен на другой сервер. Для того, чтобы отправить запрос на другой сервер, нам нужно его сериализовать, а для этого опишем JSON-RPC шлюз. Список методов проще, чем для сервисов, ведь нам не нужно описывать обработчик, а только указать JSON-описания запросов и ответов. Но обработчик исходящих вызовов будет посложнее — в нем нужно будет реализовать интерфейс icalc: Реализация каждого метода интерфейса однотипна — нужно вызвать метод call<> с тегом нужного метода, передать туда запрос, обработчик ответа и обработчик ошибки, если необходимо. Обработчик результата осядет в движке до тех пор, пока мы не сообщим ему ответ. Также можно указать время жизни, по истечении которого будет вызван callback с nullptr. Если в качестве ответа пришла JSON-RPC ошибка, то в callback также будет передан nullptr, что для прикладного кода будет означать, что произошла ошибка, не связанная с прикладной логикой. Пытаться протащить JSON-RPC коды в прикладную часть не очень хорошая идея. Лучше дополнить структуры ответа соответствующими статусами, описывающими ошибки именно прикладной логики. В качестве примера попробуем связать шлюз, сервис, прокси и собственно калькулятор следующим образом: 
Так как наш прокси имеет интерфейс калькулятора, то мы можем с ним работать как с калькулятором, а то, что он делает с запросом, нам не особо важно. Прокси может перенаправить запрос к шлюзу, который также имеет интерфейс калькулятора, поэтому нам не важно, с каким объектом связан прокси. Это может быть еще один прокси, а можно выстроить из них целую цепочку неограниченной длины. Задача шлюза — сериализовать запрос, который мы можем передать по сети. Но в примере мы сразу передадим его на сервис, который его десериализует и, в данном случае, передаст еще одному прокси.  Второй прокси также его модифицирует и уже передает непосредственно калькулятору. Калькулятор его честно отрабатывает и вызывает callback, в котором второй прокси модифицирует ответ и вызывает callback, который пришел от сервиса, который его сериализует. Сериализованный ответ передается JSON-RPC движку, который находит по id вызова соответствующий JSON-RPC обработчик и вызывает его. В нем ответ десериализуется и вызывается callback первого прокси, в котором ответ еще раз модифицируется и передается в наш исходный callback, в котором мы выводим ответ на экран. Результат: Первый прокси инкрементировал параметры запроса первый раз, поэтому получаем в JSON-RPC значения 2 и 3 вместо 1 и 2. Второй прокси их также инкрементирует, а потом инкрементирует результат, поэтому сервис отправляет значение 8. Первый прокси еще раз инкрементирует результат, поэтому финальное значение 9. Второй запрос отправляем напрямую в шлюз, минуя первый прокси, но он проходит через второй, а модификация для munus там не предусмотрена, поэтому в результате правильный ответ. Все двузначные числа в этом примере — это уникальные идентификаторы сущностей, которые должны быть уникальны и, разумеется, их нужно генерировать, а не прописывать в коде напрямую, например, так: Эти идентификаторы служат для связывания сущностей без прямых ссылок на них. Например, в этой строке: Мы в JSON-RPC движке шлюза регистрируем обработчик, который должен отправить данные на сервер. Он должен работать с каким-то объектом, клиентом, для некоего сервера, через который мы можем отправить уже сериализованный запрос. Можно сделать несколько параллельных подключений к серверу и зарегистрировать их. Но вместо этого сериализованный запрос передается напрямую на сервис. А число 44 — это идентификатор некоего объекта сервера, который был создан при подключении клиента. Такие коннекты можно также регистрировать в service::engine_type, но если у нас нет встречных вызовов (когда сервер вызывает метод клиента, а такое в wjrpc тоже можно реализовать), то этого делать не обязательно. Далее я хотел привести аналогичный пример с взаимодействием между процессами через пайпы(::pipe), с подробным описанием, но думаю, что я вас еще только больше запутаю, да и особо практического смысла это не имеет, а сделать более-менее реалистичный пример взаимодействия двух серверов, со всеми нюансами и чтобы они работали достаточно эффективно, потребует достаточно серьезной кодовой обвязки, к теме статьи отношения не имеющей. Пример построения цепочки процессов с взаимодействием через пайпы есть в разделе examples проекта, также, как и код остальных примеров, приведенных в этой статье. Но если вы нашли удовлетворительной для себя концепцию сериализации wjson, про которую я писал в предыдущей статье, то в связке wjrpc::incoming_holder вполне можно сделать эффективный JSON-RPC сервер. Если не вызывает отторжения концепция асинхронных интерфейсов, то с помощью wjrpc::handler вы можете сделать все то же самое, но с меньшим количеством run-time кода. Скачать wjrpc можно здесь. Вам также понадобятся faslib и wjson. Чтобы скомпилировать примеры и тесты: