JSON-RPC на C++

У попередньої статті про декларативний JSON-сериализатор я розповів, як за допомогою шаблонів C++ можна описувати структури даних і сериализации їх. Це дуже зручно, тому що не тільки скорочує розмір коду, але і мінімізує кількість можливих помилок.Концепція — якщо код компілюється, то він працює. Приблизно такий же підхід застосовано і в wjrpc, про яку мова піде в цій статті. Але оскільки wjrpc «виламаний» з фреймворку, під інтерфейси якого він був розроблений, я торкнуся питання архітектури і асинхронних інтерфейсів.

Я не буду описувати JSON-сериализатор, на якому працює wjrpc та з допомогою якого здійснюється JSON-опис структур даних повідомлень. Про wjson я розповів у попередній статті. Перш ніж розглядати декларативний варіант опису API сервісів для JSON-RPC, розглянемо, як можна реалізувати розбір в «вручну». Це потребує написання більшої кількості run-time коду по вилученню даних і перевірок, але він простіше для розуміння. Всі приклади ви можете знайти в розділі examples проекту.
В якості прикладу розглянемо API простого калькулятора, який може виконувати операції додавання, віднімання, множення і ділення. Самі запити тривіальні, тому наведу лише код тільки для операції додавання:
calc/api/plus.hpp
#pragma once
#include <memory>
#include <functional>

namespace request
{
struct plus
{
int first=0;
int second=0;
typedef std::unique_ptr<plus> ptr;
};
} // request

namespace response
{
struct plus
{
int value=0;
typedef std::unique_ptr<plus> ptr;
typedef std::function<void(ptr)> callback;
};
} // response

Я віддаю перевагу структури для кожної пари запит-відповідь розміщувати в окремому файлі у спеціально призначеній для цього папці. Це здорово полегшує навігацію по проекту. Використання просторів імен і визначення допоміжних типів, як показано в прикладі вище, не обов'язково, але підвищує читабельність коду. Сенс цих typedef-ів поясню пізніше.
Для кожного запиту створюємо опис JSON.
calc/api/plus_json.hpp
#pragma once
#include "calc/api/plus.hpp"
#include <wjson/json.hpp>
#include <wjson/name.hpp>

namespace request
{
struct plus_json
{
JSON_NAME(first)
JSON_NAME(second)

typedef wjson::object<
plus,
wjson::member_list<
wjson::member<n_first, plus, int, &plus::first>,
wjson::member<n_second, plus, int, &plus::second>
>
> type;
typedef typename type::serializer serializer;
typedef typename type::target target;
};
}

namespace response
{
struct plus_json
{
JSON_NAME(value)
typedef wjson::object<
plus,
wjson::member_list<
wjson::member<n_value, plus, int, &plus::value>
>
> type;
typedef typename type::serializer serializer;
typedef typename type::target target;
};
}

Навіщо поміщати його в структуру, я розповідав у статті, присвяченій wjson. Зазначу тільки, що тут визначення typedef-ів потрібно, щоб ці структури були розпізнані як json-опис.
Обробку JSON-RPC повідомлень можна розділити на два етапи. На першому етапі потрібно визначити тип запиту та ім'я методу, а на другому десериализовать параметри для цього методу.
Сериализованный JSON-RPC plus
<p>{
"jsonrpc":"2.0",
"method":"plus",
"params":{
"first":2,
"second":3
},
"id":1
}</p>
<source><!--</spoiler>-->
Наприклад, на першому етапі можна сериализации запити в таку структуру:
<!--<spoiler title="Структура для запитів">-->
``cpp
struct request
{
std::string version,
std::string method,
std::string params,
std::string id
}

JSON-опис для запитів
JSON_NAME(jsonrpc)
JSON_NAME(method)
JSON_NAME(params)
JSON_NAME(id)

typedef wjson::object<
request,
wjson::member_list<
wjson::member<n_jsonrpc, request, std::string &request::version>,
wjson::member<n_method, request, std::string &request::method>,
wjson::member<n_params, request, std::string &request::params, json::raw_value<> >,
wjson::member<n_id, request, std::string &request::id, json::raw_value<> >
>
> request_json;

При такому описі в поля
request::params
та
request::id
json буде скопійовано як є, без будь-якого перетворення, а в полі
request::method
буде власне ім'я методу. Визначивши ім'я методу, можемо десериализовать параметри описаними вище структурами.
Щоб визначити ім'я методу, не обов'язково десериализовать весь запит в проміжну структуру даних. Досить його розпарсити, а десериализовать тільки шматок запиту, що відноситься до поля params. Це можна зробити з допомогою wjson::parser безпосередньо, але wjson надає також конструкцію raw_pair (у попередній статті я її не розглядав), яка дозволить не десериализовать елементи, а запам'ятати його розташування у вхідному буфері. Розглянемо, як це реалізовано в wjrpc.
Почнемо з того, що wjrpc не працює з рядками std::string, а визначає наступні типи:
namespace wjrpc
{
typedef std::vector<char> data_type;
typedef std::unique_ptr<data_type> data_ptr;
}

Можна розглядати data_ptr як переміщуваний буфер, використання якого дозволяє нам гарантувати, що дані зайвий раз не будуть скопійовані за недогляд.
Будь-яке вхідне повідомлення wjrpc спробує десериализовать в наступну структуру:
wjrpc::incoming
namespace wjrpc
{
struct incoming
{
typedef data_type::iterator iterator;
typedef std::pair<iterator, iterator> pair_type;
pair_type method;
pair_type params;
pair_type result;
pair_type error;
pair_type id;
};
}

Всі елементи wjrpc::incoming являють собою пари ітераторів у вхідному буфері. Наприклад, method.first при десеріалізації буде вказувати на лапку, яка відкриває ім'я методу, після двокрапки, у вхідному запиті, а method.second — на наступну позицію після закриваючої лапки. Ця структура описує також не тільки запит, але й відповідь на запит, а також повідомлення про помилку. Визначити тип повідомлення досить просто заповненими полями. JSON -опис для такої структури:
wjrpc::incoming_json
namespace wjrpc
{
struct incoming_json
{
typedef incoming::pair_type pair_type;
typedef wjson::iterator_pair<pair_type> pair_json;

JSON_NAME(id)
JSON_NAME(method)
JSON_NAME(params)
JSON_NAME(result)
JSON_NAME(error)

typedef wjson::object<
incoming,
wjson::member_list<
wjson::member<n_method, incoming, pair_type, &incoming::method, pair_json>,
wjson::member<n_params, incoming, pair_type, &incoming::params, pair_json>,
wjson::member<n_result, incoming, pair_type, &incoming::result, pair_json>,
wjson::member<n_error, incoming, pair_type, &incoming::error, pair_json>,
wjson::member<n_id, incoming, pair_type, &incoming::id, pair_json>
>
> type;

typedef type::target target;
typedef type::member_list member_list;
typedef type::serializer serializer;
};
}

Тут немає повноцінної десеріалізації — це по суті парсинг з запам'ятовуванням позицій у вхідному буфері. Очевидно, що після такої десеріалізації дані будуть валідні, тільки поки існує вхідний буфер.
Клас wjrpc::incoming_holder захоплює буфер із запитом і парсити його в описану вище структуру. Я не буду детально описувати його інтерфейс, але покажу, як можна його використовувати для реалізації JSON-RPC.
Спочатку спрощений приклад з одним методом
#include "calc/api/plus.hpp"
#include "calc/api/plus_json.hpp"

#include <wjrpc/errors/error_json.hpp>
#include <wjrpc/incoming/incoming_holder.hpp>
#include <wjrpc/outgoing/outgoing_holder.hpp>
#include <wjrpc/outgoing/outgoing_result.hpp>
#include <wjrpc/outgoing/outgoing_result_json.hpp>
#include <wjrpc/outgoing/outgoing_error.hpp>
#include <wjrpc/outgoing/outgoing_error_json.hpp>

#include < iostream>

int main()
{
std::vector<std::string> req_list =
{
"{\"method\":\"plus\", \"params\":{ \"first\":2, \"second\":3 }, \"id\" :1 }",
"{\"method\":\"minus\", \"params\":{ \"first\":5, \"second\":10 }, \"id\" :1 }",
"{\"method\":\"multiplies\", \"params\":{ \"first\":2, \"second\":2 }, \"id\" :1 }",
"{\"method\":\"divides\", \"params\":{ \"first\":9, \"second\":3 }, \"id\" :1 }"
};
std::vector<std::string> res_list;

for ( auto& sreq : req_list )
{
wjrpc::incoming_holder inholder( sreq );
// Парсим без перевірок на помилки
inholder.parse(nullptr);

// Ім'я методу і ідентифікатор виклику
if ( inholder.method() == "plus" )
{
// Десериализация параметрів без перевірок
auto params = inholder.get_params<request::plus_json>(nullptr);
// Об'єкт для відповіді
wjrpc::outgoing_result<response::plus> res;
res.result = std::make_unique<response::plus>();

// Виконуємо операцію
res.result->value = params->first + params->second;
// Забираємо id в сирому вигляді як є
auto raw_id = inholder.raw_id();
res.id = std::make_unique<wjrpc::data_type>( raw_id.first, raw_id.second );
// Сериализатор відповіді
typedef wjrpc::outgoing_result_json<response::plus_json> result_json;
res_list.push_back(std::string());
result_json::serializer()( res, std::back_inserter(res_list.back()) );
}
/* else if ( inholder.method() == "minus" ) { ... } */
/* else if ( inholder.method() == "multiplies" ) { ... } */
/* else if ( inholder.method() == "divides" ) { ... } */
}

for ( size_t i =0; i != res_list.size(); ++i)
{
std::cout << req_list[i] << std::endl;
std::cout << res_list[i] << std::endl;
std::cout << std::endl;
}
}

Тут велику частину коду займає формування відповіді, тому що не робимо перевірок на помилки. Спочатку ініціалізуємо incoming_holder рядком і парсим її. На цьому етапі вхідний рядок десериализуется в описану вище структуру incoming. Якщо рядок містить будь валідний json-об'єкт, то цей етап пройде без помилок.
Далі потрібно визначити тип запиту. Це легко зробити за наявності або відсутності полів «method», «result», «error» і «id».








Комбінація Тип повідомлення Перевірка Отримати method і id запит is_request get_params<> method без id повідомлення is_notify get_params<> result і id відповідь на запит is_response get_result<> error id помилка у відповідь на запит is_request_error get_error<> error без id інші помилки is_other_error get_error<>
Якщо не виконується жодна з умов, то очевидно, що запит неправильний.
Приклад з перевірками і одним методом
#include "calc/api/plus.hpp"
#include "calc/api/plus_json.hpp"

#include <wjrpc/errors/error_json.hpp>
#include <wjrpc/incoming/incoming_holder.hpp>
#include <wjrpc/outgoing/outgoing_holder.hpp>
#include <wjrpc/outgoing/outgoing_result.hpp>
#include <wjrpc/outgoing/outgoing_result_json.hpp>
#include <wjrpc/outgoing/outgoing_error.hpp>
#include <wjrpc/outgoing/outgoing_error_json.hpp>
#include < iostream>

int main()
{
std::vector<std::string> req_list =
{
"{\"method\":\"plus\", \"params\":{ \"first\":2, \"second\":3 }, \"id\" :1 }",
"{\"method\":\"minus\", \"params\":{ \"first\":5, \"second\":10 }, \"id\" :1 }",
"{\"method\":\"multiplies\", \"params\":{ \"first\":2, \"second\":2 }, \"id\" :1 }",
"{\"method\":\"divides\", \"params\":{ \"first\":9, \"second\":3 }, \"id\" :1 }"
};
std::vector<std::string> res_list;

for ( auto& sreq : req_list )
{
wjrpc::incoming_holder inholder( sreq );
wjson::json_error e;
inholder.parse(&e);
if ( e )
{
typedef wjrpc::outgoing_error<wjrpc::error> error_type;
error_type err;
err.error = std::make_unique<wjrpc::parse_error>();

typedef wjrpc::outgoing_error_json<wjrpc::error_json> error_json;
std::string str;
error_json::serializer()(err, std::back_inserter(str));
res_list.push_back(str);
}
else if ( inholder.is_request() )
{
auto raw_id = inholder.raw_id();
auto call_id = std::make_unique<wjrpc::data_type>( raw_id.first, raw_id.second );
// Ім'я методу і ідентифікатор виклику
if ( inholder.method() == "plus" )
{
auto params = inholder.get_params<request::plus_json>(&e);
if ( !e )
{
wjrpc::outgoing_result<response::plus> res;
res.result = std::make_unique<response::plus>();
res.result->value = params->first + params->second;
res.id = std::move(call_id);
typedef wjrpc::outgoing_result_json<response::plus_json> result_json;
std::string str;
result_json::serializer()( res, std::back_inserter(str) );
res_list.push_back(str);
}
else
{
typedef wjrpc::outgoing_error<wjrpc::error> error_type;
error_type err;
err.error = std::make_unique<wjrpc::invalid_params>();
err.id = std::move(call_id);

typedef wjrpc::outgoing_error_json<wjrpc::error_json> error_json;
std::string str;
error_json::serializer()(err, std::back_inserter(str));
res_list.push_back(str);
}
}
/* else if ( inholder.method() == "minus" ) { ... } */
/* else if ( inholder.method() == "multiplies" ) { ... } */
/* else if ( inholder.method() == "divides" ) { ... } */
else
{
typedef wjrpc::outgoing_error<wjrpc::error> error_type;
error_type err;
err.error = std::make_unique<wjrpc::procedure_not_found>();
err.id = std::move(call_id);

typedef wjrpc::outgoing_error_json<wjrpc::error_json> error_json;
std::string str;
error_json::serializer()(err, std::back_inserter(str));
res_list.push_back(str);
}
}
else
{
typedef wjrpc::outgoing_error<wjrpc::error> error_type;
error_type err;
err.error = std::make_unique<wjrpc::invalid_request>();

typedef wjrpc::outgoing_error_json<wjrpc::error_json> error_json;
std::string str;
error_json::serializer()(err, std::back_inserter(str));
res_list.push_back(str);
}
}

for ( size_t i =0; i != res_list.size(); ++i)
{
std::cout << req_list[i] << std::endl;
std::cout << res_list[i] << std::endl;
std::cout << std::endl;
}
}

Тут велика частина коду — це обробка помилок, а точніше, формування відповідного повідомлення. Але для всіх типів помилок код схожий, відмінності тільки в типі помилки. Можна зробити одну шаблонну функцію для серіалізації всіх типів помилок.
Формування повідомлення про помилку
template < typename E>
void make_error(wjrpc::incoming_holder inholder, std::string& out)
{
typedef wjrpc::outgoing_error<wjrpc::error> common_error;
common_error err;
err.error = std::make_unique<E>();
if ( inholder.has_id() )
{
auto id = inholder.raw_id();
err.id = std::make_unique<wjrpc::data_type>(id.first, id.second);
}

typedef wjrpc::outgoing_error_json<wjrpc::error_json> error_json;
error_json::serializer()(err, std::back_inserter(out));
}

Для формування повідомлення про помилку нам достатньо знати тип помилки і ідентифікатор виклику, якщо він є. Об'єкт inholder переміщуваний і після формування повідомлення він більше не потрібен. У прикладі він використовується тільки для отримання ідентифікатора виклику, але в нього також можна «забрати» вхідний буфер — для серіалізації туди повідомлення, щоб не створювати новий.
Аналогічним чином можна реалізувати серіалізацію результату. Але перш ніж продовжити позбавлятися від однотипного коду, приведемо в порядок прикладну частину, яка тут кілька загубилася і представлена всього одним рядком для кожного методу.
Інтерфейси
Як я вже говорив, wjrpc виламаний з фреймворку, в якому для компонентів потрібно явно визначати інтерфейс. Причому, це не просто структура виключно з чистими віртуальними методами, але і пред'являються певні обмеження до параметрів методів.
Всі вхідні і вихідні параметри повинні бути об'єднані в структури, навіть якщо там буде тільки одне поле. Це зручно не тільки для формування json-опису для запиту, коли десериализованную структуру можемо передати безпосередньо в метод, без попереднього перетворення, але і з позиції розширюваності.
Наприклад, у всіх методах ми повертаємо число — результат виконання операції. Сенс описувати результат структурою з одним полем, якщо можна числом? А вхідні параметри взагалі можна було передати масивом (позиційними параметрами).
Але тепер уявіть, що вимоги трохи змінилися, і до списку параметрів потрібно додати супровідну інформацію, або у відповіді, для операції ділення, прапор, який означає, що відбулося ділення на нуль. В цьому випадку для внесення виправлень потрібно «перетрусити» не тільки інтерфейс і всі його реалізації, але і всі місця, де він використовується.
Якщо у нас JSON-RPC сервіс, зміни торкнуться не тільки сервера, але і клієнта, інтерфейси яких потрібно якось узгоджувати. А у випадку зі структурами досить просто додати поле в структуру. Причому, оновлювати клієнтську частину та серверну частину можна незалежно.
Ще одна особливість фреймворку в тому, що всі методи мають асинхронний інтерфейс, тобто результат повертається не безпосередньо, а через функцію зворотного виклику. А щоб уникнути помилок ненавмисного копіювання, вхідний і вихідний об'єкт описується як std::unique_ptr<>.
Для нашого калькулятора, з урахуванням описаних обмежень, виходить ось такий інтерфейс:
struct icalc
{
virtual ~icalc() {}
virtual void plus( request::plus::ptr req, response::plus::callback cb) = 0;
virtual void minus( request::minus::ptr req, response::minus::callback cb) = 0;
virtual void multiplies( request::multiplies::ptr req, response::multiplies::callback cb) = 0;
virtual void divides( request::divides::ptr req, response::divides::callback cb) = 0;
};

З урахуванням допоміжних typedef-ів, які ми визначили у вхідних структурах, виглядає цілком симпатично. Але ось реалізація для подібних інтерфейсів може бути досить об'ємною відносно простих прикладів. Потрібно впевнитися, що вхідний запит не nullptr, а також перевірити функцію зворотного виклику. Якщо вона не визначена, то очевидно, що це повідомлення, і в даному випадку виклик потрібно просто проігнорувати. Цей функціонал легко шаблонизировать, як показано в прикладі реалізації нашого калькулятора
calc1.hpp
#pragma once
#include "icalc.hpp"

class calc1
: public icalc
{
public:
virtual void plus( request::plus::ptr req, response::plus::callback cb) override;
virtual void minus( request::minus::ptr req, response::minus::callback cb) override;
virtual void multiplies( request::multiplies::ptr req, response::multiplies::callback cb) override;
virtual void divides( request::divides::ptr req, response::divides::callback cb) override;
private:
template < typename Res, typename ReqPtr, typename Callback, typename F>
void impl_( ReqPtr req, Callback cb, F f);
};

calc1.cpp
#include "calc1.hpp"
#include <wjrpc/memory.hpp>

template < typename Res, typename ReqPtr, typename Callback, typename F>
void calc1::impl_( ReqPtr req, Callback cb, F f)
{
// це повідомлення
if ( cb == nullptr )
return;

// немає параметрів
if ( req == nullptr )
return cb(nullptr);

auto res = std::make_unique<Res>();
res->value = f(req->first,req->second);
cb( std::move(res) );
}

void calc1::plus( request::plus::ptr req, response::plus::callback cb)
{
this->impl_<response::plus>( std::move(req), cb, [](int f, int s) { return f+s; } );
}

void calc1::minus( request::minus::ptr req, response::minus::callback cb)
{
this->impl_<response::minus>( std::move(req), cb, [](int f, int s) { return f-s; });
}

void calc1::multiplies( request::multiplies::ptr req, response::multiplies::callback cb)
{
this->impl_<response::multiplies>( std::move(req), cb, [](int f, int s) { return f*s; });
}

void calc1::divides( request::divides::ptr req, response::divides::callback cb)
{
this->impl_<response::divides>( std::move(req), cb, [](int f, int s) { return s!=0 ? f/s : 0; });
}

Стаття все ж не про реалізацію калькулятора, тому представлений вище код не варто сприймати як best practice. Приклад виклику методу:
calc->plus( std::move(params), [](response::plus::ptr result) { ... });

До теми інтерфейсів ще повернемося, а зараз продовжимо «скорочувати» код нашого сервісу. Для того, щоб асинхронно сериализации результат, необхідно «захопити» вхідний запит
наприклад, так
std::shared_ptr<wjrpc::incoming_holder> ph = std::make_shared<wjrpc::incoming_holder>( std::move(inholder) );
calc->plus( std::move(params), [ph, &res_list](response::plus::ptr result)
{
// Створюємо об'єкт результату
wjrpc::outgoing_result<response::plus> resp;
resp.result = std::move(result);
// ініціалізуємо ідентифікатор виклику
auto raw_id = ph->raw_id();
auto call_id = std::make_unique<wjrpc::data_type>( raw_id.first, raw_id.second );
resp.id = std::move(call_id);

// Сериализуем результат
typedef wjrpc::outgoing_result_json<response::plus_json> result_json;
typedef wjrpc::outgoing_result_json<response::plus_json> result_json;
// забираємо буфер
auto d = ph->detach();
d->clear();
result_json::serializer()( resp, std::back_inserter(d) );
res_list.push_back( std::string(d->begin(), d->end()) );
});

оскільки incoming_holder переміщуваний, то щоб «захопити», переміщаємо його в std::shared_ptr. Тут показано, як можна забрати у нього буфер, але в даному випадку це не має особливого сенсу — все одно результат поміщаємо в список рядків. Захоплення res_list за посиланням — це тільки для прикладу, т. к. знаємо, що запит буде виконаний синхронно.
Ми вже написали шаблонну функцію для серіалізації помилок, зробимо аналогічну для відповідей. Але для цього, крім типу результату, потрібно передати його значення і json-опис.
Універсальний сериализатор відповіді на запит
template < typename ResJ>
void send_response(std::shared_ptr<wjrpc::incoming_holder> ph, typename ResJ::target::ptr result, std::string& out)
{
typedef ResJ result_json;
typedef typename result_json::target result_type;
wjrpc::outgoing_result<result_type> resp;
resp.result = std::move(result);
auto raw_id = ph->raw_id();
resp.id = std::make_unique<wjrpc::data_type>( raw_id.first, raw_id.second );
typedef wjrpc::outgoing_result_json<result_json> response_json;
typename response_json::serializer()( resp, std::back_inserter( out ) );
}

Тут шаблонний параметр потрібно вказувати явно — це json-опис структури для відповіді, з якого можна взяти тип описуваної структури. З використанням цієї функції код для кожного JSON-RPC методу істотно спроститься
Нова версія методу plus
if ( inholder.method() == "plus" )
{
// Ручна обробка
auto params = inholder.get_params<request::plus_json>(&e);
if ( !e )
{
std::shared_ptr<wjrpc::incoming_holder> ph = std::make_shared<wjrpc::incoming_holder>( std::move(inholder) );
calc->plus( std::move(params), std::bind( send_response<response::plus_json>, ph, std::placeholders::_1, std::ref(out)) );
}
else
{
make_error<wjrpc::invalid_params>(std::move(inholder), out );
}
}
// else if ( inholder.method() == "minus" ) { ... }
// else if ( inholder.method() == "multiplies" ) { .... }
// else if ( inholder.method() == "divides" ) { .... }
else
{
make_error<wjrpc::procedure_not_found>(std::move(inholder), out );
}

Для кожного методу код скоротився до мінімуму, але і цього мені мало. Отримання параметрів і перевірка на помилку — теж однотипний код.
Сериализация і виклик методу
template<
typename JParams,
typename JResult,
void (icalc::*mem_ptr)(
std::unique_ptr<typename JParams::target>,
std::function< void(std::unique_ptr<typename JResult::target>) >
)
>
void invoke(wjrpc::incoming_holder inholder, std::shared_ptr<icalc> calc, std::string& out)
{
typedef JParams params_json;
typedef JResult result_json;
wjson::json_error e;
auto params = inholder.get_params<params_json>(&e);
if ( !e )
{
std::shared_ptr<wjrpc::incoming_holder> ph = std::make_shared<wjrpc::incoming_holder>( std::move(inholder) );
(calc.get()->*mem_ptr)( std::move(params), std::bind( send_response<result_json>, ph, std::placeholders::_1, std::ref(out) ) );
}
else
{
out = make_error<wjrpc::invalid_params>();
}
}

Це вже майже так, як реалізовано в wjrpc. У результаті код демо прикладу скоротиться до мінімуму (тут вже можна навести реалізацію всіх методів)
Кінцевий варіант прикладу з 'ручною обробкою'
int main()
{
std::vector<std::string> req_list =
{
"{\"method\":\"plus\", \"params\":{ \"first\":2, \"second\":3 }, \"id\" :1 }",
"{\"method\":\"minus\", \"params\":{ \"first\":5, \"second\":10 }, \"id\" :1 }",
"{\"method\":\"multiplies\", \"params\":{ \"first\":2, \"second\":2 }, \"id\" :1 }",
"{\"method\":\"divides\", \"params\":{ \"first\":9, \"second\":3 }, \"id\" :1 }"
};
std::vector<std::string> res_list;
auto calc = std::make_shared<calc1>();
for ( auto& sreq : req_list )
{
res_list.push_back( std::string() );
std::string& out = res_list.back();
wjrpc::incoming_holder inholder( sreq );
wjson::json_error e;
inholder.parse(&e);
if ( e )
{
out = make_error<wjrpc::parse_error>();
}
else if ( inholder.is_request() )
{
// Ім'я методу і ідентифікатор виклику
if ( inholder.method() == "plus" )
{
invoke<request::plus_json, response::plus_json, &icalc::plus>( std::move(inholder), calc, out );
}
else if ( inholder.method() == "minus" )
{
invoke<request::minus_json, response::minus_json, &icalc::minus>( std::move(inholder), calc, out );
}
else if ( inholder.method() == "multiplies" )
{
invoke<request::multiplies_json, response::multiplies_json, &icalc::multiplies>( std::move(inholder), calc, out );
}
else if ( inholder.method() == "divides" )
{
invoke<request::divides_json, response::divides_json, &icalc::divides>( std::move(inholder), calc, out );
}
else
{
out = make_error<wjrpc::procedure_not_found>();
}
}
else
{
out = make_error<wjrpc::invalid_request>();
}
}

for ( size_t i =0; i != res_list.size(); ++i)
{
std::cout << req_list[i] << std::endl;
std::cout << res_list[i] << std::endl;
std::cout << std::endl;
}
}

Таке маніакальне бажання скоротити run-time обсяг коду обумовлено декількома причинами. Прибираючи зайву
if
з допомогою досить складної конструкції, ми не тільки скорочуємо обсяг коду, але і прибираємо потенційні місця, де програмісту буде в радість наговнокодить. А ще програмісти люблять копіпаст, особливо нецікавий код, пов'язаний серіалізацією, розмазуючи його по всьому проекту, а то і привносячи в інші проекти. Лінь — двигун прогресу, коли вона змушує вигадувати людини те, що дозволить йому менше працювати. Але не в тому випадку, коли справи відкладаються на потім. Здавалося б, тривіальна перевірка, але зі словами — це ж поки прототип, написання пари рядків коду відкладається, потім забувається і одночасно копипастится по всьому проекту, а також підхоплюється іншими програмістами.
насправді ми ще не почали розглядати wjrpc. Я показав, як описуються запити з допомогою wjson, описав інтерфейс і прикладну логіку тестового прикладу, і розглянув, як можна було б реалізувати JSON-RPC вручну, щоб було розуміння, як він працює зсередини і чому. А ось повний приклад на wjrpc:
#include "calc/calc1.hpp"
#include "calc/api/plus_json.hpp"
#include "calc/api/minus_json.hpp"
#include "calc/api/multiplies_json.hpp"
#include "calc/api/divides_json.hpp"

#include <wjrpc/handler.hpp>
#include <wjrpc/method.hpp>

#include < iostream>
#include <functional>

JSONRPC_TAG(plus)
JSONRPC_TAG(minus)
JSONRPC_TAG(multiplies)
JSONRPC_TAG(divides)

struct method_list: wjrpc::method_list
<
wjrpc::target<icalc>,
wjrpc::invoke_method<_plus_, request::plus_json, response::plus_json, icalc, &icalc::plus>,
wjrpc::invoke_method<_minus_, request::minus_json, response::minus_json, icalc, &icalc::minus>,
wjrpc::invoke_method<_multiplies_, request::multiplies_json, response::multiplies_json, icalc, &icalc::multiplies>,
wjrpc::invoke_method<_divides_, request::divides_json, response::divides_json, icalc, &icalc::divides>
>{};

class handler: public wjrpc::handler<method_list> {};

int main()
{
std::vector<std::string> req_list =
{
"{\"method\":\"plus\", \"params\":{ \"first\":2, \"second\":3 }, \"id\" :1 }",
"{\"method\":\"minus\", \"params\":{ \"first\":5, \"second\":10 }, \"id\" :1 }",
"{\"method\":\"multiplies\", \"params\":{ \"first\":2, \"second\":2 }, \"id\" :1 }",
"{\"method\":\"divides\", \"params\":{ \"first\":9, \"second\":3 }, \"id\" :1 }"
};
std::vector<std::string> res_list;

auto calc = std::make_shared<calc1>();
handler h;
handler::options_type opt;
opt.target = calc;
h.start(opt, 1);

for ( auto& sreq : req_list )
{
h.perform( sreq, [&res_list](std::string out) { res_list.push_back(out);} );
}

for ( size_t i =0; i != res_list.size(); ++i)
{
std::cout << req_list[i] << std::endl;
std::cout << res_list[i] << std::endl;
std::cout << std::endl;
}
}

З допомогою JSONRPC_TAG задаються імена методів для передачі в якості шаблонних параметрів, аналогічно JSONNAME в wjson, різниця тільки в іменах сутностей, які замість префікса n, обрамляються символами підкреслення.
Далі з допомогою wjrpc::method_list і wjrpc::invoke_method описуємо всі доступні методи. Список методів передаємо в обробник wjrpc::handler. Поряд з методами у списку був описаний тип інтерфейсу об'єкта, з яким обробник буде працювати.
Основним методом обробки є perform_io, який працює з wjrpc::data_ptr.
Для рядків зроблена відповідна обгортка
typedef std::vector<char> data_type;
typedef std::unique_ptr<data_type> data_ptr;
typedef std::function< void(data_ptr) > output_handler_t;

void perform_io(data_ptr d, output_handler_t handler) { ... }
void perform(std::string str, std::function<void(std::string)> handler)
{
auto d = std::make_unique<data_type>( str.begin(), str.end() );
this->perform_io( std::move(d), [handler](data_ptr d)
{
handler( std::string(d->begin(), d->end()) );
});
}

Очевидно, що для синхронних серверів асинхронний інтерфейс абсолютно не потрібен. В цьому випадку, та й взагалі, якщо не хочете прив'язуватися до інтерфейсу, потрібно для кожного методу визначити обробник
наприклад, так
struct plus_handler
{
template < typename T>
void operator()(T& t, request::plus::ptr req)
{ // обробка повідомлення
t.target()->plus( std::move(req), nullptr );
}

template < typename T, typename Handler>
void operator()(T& t, request::plus::ptr req, Handler handler)
{
// обробка запиту
t.target()->plus( std::move(req), [handler](response::plus::ptr res)
{
if ( res != nullptr )
handler( std::move(res), nullptr );
else
handler( nullptr, std::make_unique<wjrpc::service_unavailable>() );
});
}
};

так
struct plus_handler
{
template < typename T>
void operator()(T&, request::plus::ptr req)
{
}

template < typename T, typename Handler>
void operator()(T&, request::plus::ptr req, Handler handler)
{
if (req==nullptr)
{
handler( nullptr, std::make_unique<wjrpc::invalid_params>() );
return;
}
auto res = std::make_unique<response::plus>();
res->value = req->first + req->second;
handler( std::move(res), nullptr );
}
};

Тут handler обробник відповіді, який першим параметром приймає власне відповідь, а другим повідомлення про помилку. А t — це посилання на об'єкт JSON-RPC-обробника (за аналогією self в python). Включається він в список методів наступним чином:
struct method_list: wjrpc::method_list
<
wjrpc::target<icalc>,
wjrpc::method< wjrpc::name<_plus_>, wjrpc::invoke<request::plus_json, response::plus_json, plus_handler> >,
wjrpc::invoke_method<_minus_, request::minus_json, response::minus_json, icalc, &icalc::minus>,
wjrpc::invoke_method<_multiplies_, request::multiplies_json, response::multiplies_json, icalc, &icalc::multiplies>,
wjrpc::invoke_method<_divides_, request::divides_json, response::divides_json, icalc, &icalc::divides>
>{};

Як можна здогадатися, invoke_method<> це обгортка для wjrpc::method<>, яка використовує обробник методу:
mem_fun_handler
template<
typename Params,
typename Result,
typename I,
void (I::*mem_ptr)(
std::unique_ptr<Params>,
std::function< void(std::unique_ptr<Result>) >
)
>
struct mem_fun_handler
{
typedef std::unique_ptr<Params> request_ptr;
typedef std::unique_ptr<Result> responce_ptr;
typedef std::unique_ptr< error> json_error_ptr;

typedef std::function< void(responce_ptr, json_error_ptr) > jsonrpc_callback;

template < typename T>
void operator()(T& t, request_ptr req) const;

template < typename T>
void operator()(T& t, request_ptr req, jsonrpc_callback cb) const;
};

Ми довгий час у своїх проектах не використовували асинхронні інтерфейси, так і сама реалізація JSON-RPC була дещо простіше, але для кожного методу доводилося писати ось такий обробник, основне завдання якого викликати метод відповідного класу прикладної логіки і відправити відповідь.
Але проблема таких обробників не в тому, що вони є (а можна і без них), а в тому, що програмісти часто (майже завжди) починають переносити туди частину прикладної логіки. Абсолютно з різних причин. Не пророблений інтерфейс, просто скопіпастив звідкись код, лінь подумати та інше. Найчастіше на етапах прототипування логіка не набагато складніше нашого прикладу. Так навіщо плодити сутності, продумувати ініціалізацію системи і пр? Пару-трійку синглетонов і впендюриваем логіку прямо в обробник. Тим більше, не треба думати, як помилку протягати. Потім все це обростає кодом і потихеньку всі забувають, що це прототип, і він їде в продакшн. Підтримувати такий код дуже важко, неможливо покрити тестами, простіше переписати з нуля, ніж отрефакторить подібний проект.
В якийсь момент прийшла ідея «зобов'язати» використовувати інтерфейси і стандартизувати їх. Завдяки цьому з'явилася можливість зробити обгортку типу wjrpc::invoke_method. На цій концепції побудований весь фреймворк, а не тільки JSON-RPC. Концепція полягає в максимальному обмеженні можливостей для «творчості» програмісту в шарах, які не пов'язані з прикладною логікою.
Якщо вам не потрібна асинхронність, то можна також опрацювати стандарт інтерфейсу і написати обгортку типу wjrpc::invoke_method.
Приклад синхронного інтерфейсу
struct icalc
{
virtual ~icalc() {}
virtual request::plus::ptr plus( request::plus::ptr req) = 0;
virtual request::minus::ptr minus( request::minus::ptr req) = 0;
virtual request::multiplies::ptr multiplies( request::multiplies::ptr req) = 0;
virtual request::divides::ptr divides( request::divides::ptr req) = 0;
};

Об'єкт wjrpc::handler<> зазвичай створюється в контексті з'єднання і має таке ж життя. Якщо callback, наприклад, потрібен об'єкт з'єднання, щоб відправити відповідь, то потрібно поставити захист на випадок, якщо виклик відбудеться після знищення об'єкта.
асинхронної середовищі так робити не можна
calc->plus( std::move(req), [this](response::plus::ptr) {} );

І так теж не можна
std::shared_ptr<calc> pthis = this->shared_from_this();
calc->plus( std::move(req), [pthis](response::plus::ptr) {} );

Другий варіант не підходить, оскільки відбувається захоплення об'єкта з'єднання на невизначений час з усіма ресурсами. Варіант
std::weak_ptr<calc> wthis = this->shared_from_this();
calc->plus( std::move(req), [wthis](response::plus::ptr)
{
if ( auto pthis = wthis.lock() )
{
/* ... */
}
} );

вже краще, але ми не можемо повторно використовувати такий об'єкт (наприклад, для пулу об'єктів). Для ідентифікації об'єкта можна використовувати розумний покажчик довільного типу, який потрібно просто відновити, щоб відправлені гуляти по системі callback-і стали більше не актуальні.
std::weak_ptr<int> w = this->_p; /* _p = std::shared_ptr<int>(1);*/
std::weak_ptr<calc> wthis = this->shared_from_this();
calc->plus( std::move(req), [wthis, w](response::plus::ptr)
{
if ( auto pthis = wthis.lock() )
{
if ( nullptr == w.lock() )
return;
/* ... */
}
} );

Для уніфікації підходу можна розробити допоміжні класи (немає в wjrpc)
callback-обгортка
template < typename H>
class owner_handler
{
public:
typedef std::weak_ptr<int> weak_type;
owner_handler() = default;

owner_handler(H&& h, weak_type alive)
: _handler( std::forward<H>(h) )
, _alive(alive)
{ }

template < class... Args>
auto operator()(Args&&... args)
-> typename std::result_of< H(Args&&...) >::type
{
if ( auto p = _alive.lock() )
{
return _handler( std::forward<Args>(args)... );
}
return typename std::result_of< H(Args&&...) >::type();
}
private:
H _handler;
weak_type _alive;
};

Власник (для наслідування або агрегації)
class owner
{
public:
typedef std::shared_ptr<int> alive_type;
typedef std::weak_ptr<int> weak_type;

owner()
: _alive( std::make_shared<int>(1) )
{ }

owner(const owner& ) = delete;
owner& operator = (const owner& ) = delete;

owner(owner&& ) = default;
owner& operator = (owner&& ) = default;

alive_type& alive() { return _alive; }
const alive_type& alive() const { return _alive; }
void reset() { _alive = std::make_shared<int>(*_alive + 1); }

template < typename Handler>
owner_handler<typename std::remove_reference<Handler>::type>
wrap(Handler&& h) const
{
return
owner_handler<
typename std::remove_reference<Handler>::type
>(
std::forward<Handler>(h),
std::weak_ptr<int>(_alive)
);
}
private:
mutable alive_type _alive;
};

Приклад
// owner - базовий клас
std::weak_ptr<calc> wthis = this->shared_from_this();
calc->plus( std::move(req), this->wrap([wthis](response::plus::ptr)
{
if ( auto pthis = wthis.lock() )
{
/* ... */
}
}));
// ...
// Скидання стану об'єкта
owner::reset(); // невідпрацьовані callback-і більше не спрацюють

Але це ще не всі проблеми з функціями зворотного виклику. Вони повинні бути викликані обов'язково і викликані рівно один раз. Я не буду зараз на цьому детально зупинятися, просто майте це на увазі. Нагадаю, що ця бібліотека вирвана з фреймворку, і там ці проблеми так чи інакше вирішені. Зв'язуватися чи ні з асинхронностью, вирішувати вам.
JSON-RPC Engine
Движок wjrpc::engine – це, по суті, реєстр jsonrpc-обробників, який дозволяє винести їх в окремий модуль і керувати часом життя. Наприклад, вхідний потік повідомлень можна розподілити по чергах з різними пріоритетами, виходячи з імен методів, і тільки потім передати в wjrpc::engine. Також він використовується для реалізації віддалених викликів до інших сервісів. Для вхідних запитів об'єкт з'єднання захоплюється callback-ом, який може гуляти невизначено довго по системі, тому він не потрібен. Але для вихідних запитів потрібен wjrpc::engine, щоб зв'язати сутність, отправившую запит з обробником, щоб доставити відповідь, коли він прийде.
Для демонстрації віддалених викликів спочатку розробимо проксирующий об'єкт, який реалізує інтерфейс icalc. Це буде такий злісний проксі, який змінює вхідні і вихідні значення для методу plus, инкрементируя їх.
calc/calc_p.hpp
#pragma once
#include "icalc.hpp"

class calc_p
: public icalc
{
public:
void initialize(std::shared_ptr<icalc>);
virtual void plus( request::plus::ptr req, response::plus::callback cb) override;
virtual void minus( request::minus::ptr req, response::minus::callback cb) override;
virtual void multiplies( request::multiplies::ptr req, response::multiplies::callback cb) override;
virtual void divides( request::divides::ptr req, response::divides::callback cb) override;
private:
template < typename ReqPtr, typename Callback>
bool check_( ReqPtr& req, Callback& cb);
std::shared_ptr<icalc> _next;
};

calc/calc_p.cpp
#include "calc_p.hpp"
#include <memory>

void calc_p::initialize(std::shared_ptr<icalc> next)
{
_next = next;
}

void calc_p::plus( request::plus::ptr req, response::plus::callback cb)
{
if ( !this->check_(req, cb))
return;
req->first++;
req->second++;
_next->plus(std::move(req), [cb](response::plus::ptr res)
{
res->value++;
cb(std::move(res) );
});
}

void calc_p::minus( request::minus::ptr req, response::minus::callback cb)
{
if ( this->check_(req, cb))
_next->minus(std::move(req), std::move(cb) );
}

void calc_p::multiplies( request::multiplies::ptr req, response::multiplies::callback cb)
{
if ( this->check_(req, cb))
_next->multiplies(std::move(req), std::move(cb) );
}

void calc_p::divides( request::divides::ptr req, response::divides::callback cb)
{
if ( this->check_(req, cb))
_next->divides(std::move(req), std::move(cb) );
}

template < typename ReqPtr, typename Callback>
bool calc_p::check_( ReqPtr& req, Callback& cb)
{
if ( cb==nullptr )
return false;
if ( req != nullptr )
return true;
cb(nullptr);
return false;
}

Як видно, проксі ініціалізується покажчиком на інтерфейс icalc, куди перенаправляє всі запити як є, крім методу plus, в якому реалізована його «злостивість». Також проводиться перевірка вхідних запитів та ігноруються всі повідомлення і нульові запити.
Завдяки асинхронного інтерфейсу нашому проксі абсолютно не важливо, як буде оброблений запит: синхронно або асинхронно, як прийде відповідь, тоді він і зробить свої лихі справи. Запит може бути поставлений в чергу, або може бути вибудувана ціла ланцюжок з таких проксі, або цей запит може бути відправлений на інший сервер.
Для того, щоб надіслати запит на інший сервер, нам потрібно його сериализации, а для цього опишемо JSON-RPC шлюз.
JSONRPC_TAG(plus)
JSONRPC_TAG(minus)
JSONRPC_TAG(multiplies)
JSONRPC_TAG(divides)

struct method_list: wjrpc::method_list
<
wjrpc::call_method<_plus_, request::plus_json, response::plus_json>,
wjrpc::call_method<_minus_, request::minus_json, response::minus_json>,
wjrpc::call_method<_multiplies_, request::multiplies_json, response::multiplies_json>,
wjrpc::call_method<_divides_, request::divides_json, response::divides_json, icalc>
>
{};

Список методів простіше, ніж для сервісів, адже нам не потрібно описувати обробник, а лише вказати JSON-опису запитів і відповідей. Але обробник вихідних дзвінків буде складніше — в ньому потрібно буде реалізувати інтерфейс icalc:
class handler
: public ::wjrpc::handler<method_list>
, public icalc
{
public:
virtual void plus( request::plus::ptr req, response::plus::callback cb) override
{
this->template call<_plus_>( std::move(req), cb, nullptr );
}

virtual void minus( request::minus::ptr req, response::minus::callback cb) override
{
this->template call<_minus_>( std::move(req), cb, nullptr );
}

virtual void multiplies( request::multiplies::ptr req, response::multiplies::callback cb) override
{
this->template call<_multiplies_>( std::move(req), cb, nullptr );
}

virtual void divides( request::divides::ptr req, response::divides::callback cb) override
{
this->template call<_divides_>( std::move(req), cb, nullptr );
}
};

Реалізація кожного методу інтерфейсу однотипна — потрібно викликати метод call<> з тегом потрібного методу, передати туди запит, обробник відповіді і обробник помилки, якщо необхідно. Обробник результату осяде в движку до тих пір, поки ми не дамо йому відповідь. Також можна вказати час життя, після закінчення якого буде викликаний callback з nullptr.
Якщо в якості відповіді прийшла JSON-RPC помилка, то в callback також буде переданий nullptr, що для прикладного коду буде означати, що сталася помилка, не пов'язана з прикладною логікою. Намагатися протягнути JSON-RPC коди в прикладну частину не дуже хороша ідея. Краще доповнити структури відповіді відповідними статусами, що описують помилки саме прикладної логіки.
В якості прикладу спробуємо пов'язати шлюз, сервіс, проксі і власне калькулятор наступним чином:

Так як наш проксі має інтерфейс калькулятора, то ми можемо працювати з ним як з калькулятором, а те, що він робить із запитом, нам не особливо важливо. Проксі може перенаправити запит до шлюзу, який також має інтерфейс калькулятора, тому нам не важливо, з яким об'єктом пов'язаний проксі. Це може бути ще один проксі, а можна вибудувати з них цілий ланцюжок необмеженої довжини. Завдання шлюзу — сериализации запит, який ми можемо передати по мережі. Але в прикладі ми відразу передамо його на сервіс, який його десериализует і, в даному випадку, передасть ще одному проксі.
Другий проксі також його модифікує і вже передає безпосередньо калькулятору. Калькулятор його чесно відпрацьовує і викликає callback, в якому другий проксі модифікує відповідь і викликає callback, який прийшов від сервісу, який його серіалізует. Сериализованный відповідь передається JSON-RPC движку, який знаходить по id виклику відповідний JSON-RPC обробник і викликає його. У ньому відповідь десериализуется і викликається callback першого проксі, в якому відповідь ще раз модифікується і передається в наш вихідний callback, в якому ми виводимо відповідь на екран.
calc/calc_p.cpp
#include "calc/calc1.hpp"
#include "calc/calc_p.hpp"
#include "calc/api/plus_json.hpp"
#include "calc/api/minus_json.hpp"
#include "calc/api/multiplies_json.hpp"
#include "calc/api/divides_json.hpp"

#include <wjrpc/engine.hpp>
#include <wjrpc/handler.hpp>
#include <wjrpc/method.hpp>

#include < iostream>
#include <functional>

namespace service
{
JSONRPC_TAG(plus)
JSONRPC_TAG(minus)
JSONRPC_TAG(multiplies)
JSONRPC_TAG(divides)

struct method_list: wjrpc::method_list
<
wjrpc::target<icalc>,
wjrpc::invoke_method<_plus_, request::plus_json, response::plus_json, icalc, &icalc::plus>,
wjrpc::invoke_method<_minus_, request::minus_json, response::minus_json, icalc, &icalc::minus>,
wjrpc::invoke_method<_multiplies_, request::multiplies_json, response::multiplies_json, icalc, &icalc::multiplies>,
wjrpc::invoke_method<_divides_, request::divides_json, response::divides_json, icalc, &icalc::divides>
>{};

class handler: public ::wjrpc::handler<method_list> {};

typedef wjrpc::engine<handler> engine_type;
}

namespace gateway
{
JSONRPC_TAG(plus)
JSONRPC_TAG(minus)
JSONRPC_TAG(multiplies)
JSONRPC_TAG(divides)

struct method_list: wjrpc::method_list
<
wjrpc::call_method<_plus_, request::plus_json, response::plus_json>,
wjrpc::call_method<_minus_, request::minus_json, response::minus_json>,
wjrpc::call_method<_multiplies_, request::multiplies_json, response::multiplies_json>,
wjrpc::call_method<_divides_, request::divides_json, response::divides_json>
>
{};

class handler
: public ::wjrpc::handler<method_list>
, public icalc
{
public:
virtual void plus( request::plus::ptr req, response::plus::callback cb) override
{
this->template call<_plus_>( std::move(req), cb, nullptr );
}

virtual void minus( request::minus::ptr req, response::minus::callback cb) override
{
this->template call<_minus_>( std::move(req), cb, nullptr );
}

virtual void multiplies( request::multiplies::ptr req, response::multiplies::callback cb) override
{
this->template call<_multiplies_>( std::move(req), cb, nullptr );
}

virtual void divides( request::divides::ptr req, response::divides::callback cb) override
{
this->template call<_divides_>( std::move(req), cb, nullptr );
}
};

typedef wjrpc::engine<handler> engine_type;
}

int main()
{
// Проксі N1
auto prx1 = std::make_shared<calc_p>();
// Шлюз
auto gtw система = std::make_shared<gateway::engine_type>();
// Сервіс
auto srv = std::make_shared<service::engine_type>();
// Проксі N2
auto prx2 = std::make_shared<calc_p>();
// Калькулятор
auto clc = std::make_shared<calc1>();
// Пов'язуємо другий проксі з калькулятором
prx2->initialize(clc);
// Пов'язуємо сервіс з другим проксі і запускаємо сервіс
service::engine_type::options_type srv_opt;
srv_opt.target = prx2;
srv->start(srv_opt, 11);

// Запускаємо шлюз
gateway::engine_type::options_type cli_opt;
gtw система->start(cli_opt, 22);

// Реєструємо обробник шлюзу і пов'язуємо його з серивисом
gtw система->reg_io(33, [srv]( wjrpc::data_ptr d, wjrpc::io_id_t /*io_id*/, wjrpc::output_handler_t handler)
{
std::cout << " REQUEST: " << std::string( d->begin(), d->end() ) << std::endl;
srv->perform_io(std::move(d), 44, [handler](wjrpc::data_ptr d)
{
// Перевизначення обробника для виведення JSON-RPC відповіді
std::cout << " RESPONSE: " << std::string( d->begin(), d->end() ) << std::endl;
handler(std::move(d) );
});
});
// Знаходимо зареєстрований обробник шлюзу за його ID
auto gtwh = gtw система->find(33);
// Зв'язуємо обробника шлюзу з першим проксі
prx1->initialize(gtwh);
// Викликаємо plus через проксі (prx1->gtw система->srv->prx2->clc)
auto plus = std::make_unique<request::plus>();
plus->first = 1;
plus->second = 2;
prx1->plus( std::move(plus), [](response::plus::ptr res)
{
std::cout << "1+2=" << res->value << std::endl;;
});

// Викликаємо plus через шлюз (gtw система->srv->prx2->clc)
auto minus = std::make_unique<request::minus>();
minus->first = 4;
minus->second = 3;
gtwh->minus( std::move(minus), [](response::minus::ptr res)
{
std::cout << "4-3=" << res->value << std::endl;;
});
}

Результат:
REQUEST: {"jsonrpc":"2.0","method":"plus","params":{"first":2,"second":3},"id":1}
RESPONSE: {"jsonrpc":"2.0","result":{"value":8},"id":1}
1+2=9
REQUEST: {"jsonrpc":"2.0","method":"minus","params":{"first":4,"second":3},"id":2}
RESPONSE: {"jsonrpc":"2.0","result":{"value":1},"id":2}
4-3=1

Перший проксі инкрементировал параметри запиту перший раз, тому отримуємо в JSON-RPC значення 2 і 3 замість 1 і 2. Другий проксі їх також инкрементирует, а потім инкрементирует результат, тому сервіс відправляє значення 8. Перший проксі ще раз инкрементирует результат, тому фінальне значення 9. Другий запит відправляємо безпосередньо в шлюз, минаючи перший проксі, але він проходить через другу, а модифікація для munus там не передбачена, тому в результаті правильну відповідь.
Всі двозначні числа в цьому прикладі — це унікальні ідентифікатори сутностей, які повинні бути унікальні і, зрозуміло, їх потрібно генерувати, а не прописувати в коді безпосередньо, наприклад, так:
create_id
inline wjrpc::io_id_t create_id()
{
static std::atomic<wjrpc::io_id_t> counter( (wjrpc::io_id_t(1)) );
return counter.fetch_add(1);
}

Ці ідентифікатори служать для зв'язування сутностей без прямих посилань на них. Наприклад, у цьому рядку:
gtw система->reg_io(33, []( wjrpc::data_ptr, wjrpc::io_id_t, wjrpc::output_handler_t)

Ми в JSON-RPC движку шлюзу реєструємо обробник, який повинен відправити дані на сервер. Він повинен працювати з якимось об'єктом, клієнтом, для якогось сервера, через який ми можемо відправити вже сериализованный запит. Можна зробити кілька паралельних підключень до сервера і зареєструвати їх. Але замість цього сериализованный запит передається безпосередньо на сервіс. А число 44 — це ідентифікатор якогось об'єкта сервера, який був створений при підключенні клієнта. Такі коннекти також можна реєструвати в service::engine_type, але якщо у нас немає зустрічних викликів (коли сервер викликає метод клієнта, а таке в wjrpc теж можна реалізувати), то цього робити не обов'язково.
Далі я хотів навести аналогічний приклад із взаємодією між процесами через пайпи(::pipe), з докладним описом, але думаю, що я вас ще тільки більше заплутаю, так і особливо практичного значення це не має, а зробити більш-менш реалістичний приклад взаємодії двох серверів, з усіма нюансами і щоб вони працювали досить ефективно, потребує досить серйозною кодової обв'язки, до теми статті стосунку не має. Приклад побудови ланцюжка процесів з взаємодією через пайпи є в розділі examples проекту, також, як і код інших прикладів, наведених у цій статті.
Але якщо ви знайшли задовільною для себе концепцію серіалізації wjson, про яку я писав у попередній статті, то в зв'язці wjrpc::incoming_holder цілком можна зробити ефективний JSON-RPC сервер. Якщо не викликає відторгнення концепція асинхронних інтерфейсів, за допомогою wjrpc::handler ви можете зробити все те ж саме, але з меншою кількістю run-time коду.
Завантажити wjrpc можна тут. Вам також знадобляться faslib і wjson. Щоб скомпілювати приклади і тести:
git clone https://github.com/migashko/faslib.git
git clone https://github.com/mambaru/wjson.git
git clone https://github.com/mambaru/wjrpc.git

# потрібно тільки для компіляції тестів wjrpc
cd faslib
mkdir build
cd build
cmake ..

# збираємо приклади і тести
cd ../../wjrpc
mkdir build
cd build
cmake -DWJRPC_BUILD_ALL=ON ..
make

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.