...

среда, 26 октября 2016 г.

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 http://ift.tt/2cDw5b3
git clone http://ift.tt/2dtpGp5
git clone http://ift.tt/2feGcZF

# нужно только для компиляции тестов wjrpc
cd faslib
mkdir build
cd build
cmake ..

# собираем примеры и тесты
cd ../../wjrpc
mkdir build
cd build
cmake -DWJRPC_BUILD_ALL=ON ..
make

Комментарии (0)

    Let's block ads! (Why?)

    Комментариев нет:

    Отправить комментарий