+7 (495) 229-0436   shopadmin@itshop.ru 119334, г. Москва, ул. Бардина, д. 4, корп. 3
 
 
Вход
 
 
Каталог
 
 
Подписка на новости
Новости ITShop
Windows 7 и Office: Новости и советы
Обучение и сертификация Microsoft
Вопросы и ответы по MSSQLServer
Delphi - проблемы и решения
Adobe Photoshop: алхимия дизайна
 
Ваш отзыв
Оцените качество магазина ITShop.ru на Яндекс.Маркете. Если вам нравится наш магазин - скажите об этом Google!
 
 
Способы оплаты
 
Курс расчета
 
 1 у.е. = 92.01 руб.
 
 Цены показывать:
 
 
 
 
  
Новости, статьи, акции
 

RMI средствами С++ и boost.preprocessor

06.11.2013 17:22

RMI - весьма банальная задача для ЯП, поддерживающих интроспекцию. Но, С++, к сожалению, к ним не относится.

Постановка задачи

1. Предоставить максимально простой синтаксис, чтоб невозможно было допустить ошибку.
2. Идентификация(связывание) процедур должна быть скрыта от пользователя для того, чтоб невозможно было допустить ошибку.
3. Синтаксис не должен накладывать ограничения на используемые С++ типы.
4. Должна присутствовать возможность версионности процедур, но, так, чтоб не ломалась совместимость с уже работающими клиентами.

Касательно четвертого пункта:
К примеру, у нас уже есть две процедуры add/div по одной версии каждой. Мы хотим для add добавить новую версию. Если просто добавить еще одну версию - у нас поплывут ID`ы процедур, о которых знают клиентские программы, собранные до внесения этого изменения.

Выбор инструмента

Т.к. конечный результат предполагается использовать совместно с С++ кодом, вариантов я вижу три:

  • Изобретаем синтаксис и пишем свой кодогенератор.
  • Используем С++ препроцессор.
  • Ищем нечто готовое и допиливаем под себя(если нужно).

Выскажусь о каждом из вариантов соответственно:

  • Зачем дополнительная стадия кодогенерации?
  • Препроцессор я люблю, и часто его использую.
  • Трата времени и сил. И, не понятно, будет ли в этом смысл.

Касательно первого, второго и третьего пунктов требований - препроцессорный вариант подходит.

Итак, выбор сделан - используем препроцессор. И да, разумеется boost.preprocessor.

Немного о препроцессоре

Типы данных С++ препроцессора:

  • arrays: (size, (elems, ....))
  • lists: (a, (b, (c, BOOST_PP_NIL)))
  • sequences: (a)(b)(c)
  • tuples: (a, b, c)

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

Несколько поясняющих примеров.
(a)(b)(c) - sequence. Тут, мы описали sequence, состоящий из трех элементов.
(a) - также sequence, но состоящий из одного элемента. (внимание!)
(a)(b, c)(d, e, f) - снова sequence, но состоящий из трех tuples. (обратите внимание на первый элемент - уловочка, однако, но это и правда tuple)
(a)(b, c)(d, (e, f)) - опять же sequence, и так же состоящий из трех tuples. Но! Последний tuple состоит их двух элементов: 1) любого элемента, 2) tuple.
И, напоследок, такой пример: (a)(b, c)(d, (e, (f)(g))) - тут уж разберитесь сами ;)
Как видно, все и вправду нереально просто.

Прототипируем синтаксис

После недолгого раздумья, вырисовывается такой синтаксис:
(proc_name0, // имя процедуры (signature_arg0, signature_arg1, signature_argN) // первая версия процедуры (signature_arg0) // вторая версия процедуры ) (proc_name1, // имя процедуры (signature_arg0, signature_arg1) // единственная версия этой процедуры ) (proc_name2, // имя процедуры () // единственная версия этой процедуры (без аргументов) )
Ну… весьма употрибительно, однако.

Некоторые детали реализации

Т.к. одно из требований - версионность процедур, да еще и такая, чтоб не ломалась совместимость с уже существующими клиентами - нам, для идентификации процедур, понадобятся два ID`а. Первый - ID процедуры, второй - ID версии.

Поясню на примере.
Допустим, это описание API нашего сервиса. Допустим, у нас уже есть клиентские программы, использующие этот API.
(proc_name0, // procID=0 (signature_arg0, signature_arg1) // sigID=0 ) (proc_name1, // procID=1 (signature_arg0, signature_arg1) // sigID=0 ) (proc_name2, // procID=2 () // sigID=0 )
Теперь, для proc_name0() нам нужно добавить еще одну версию с другой сигнатурой.
(proc_name0, // procID=0 (signature_arg0, signature_arg1) // sigID=0 (signature_arg0, signature_arg1, signature_arg2) // sigID=1 ) (proc_name1, // procID=1 (signature_arg0, signature_arg1) // sigID=0 ) (proc_name2, // procID=2 () // sigID=0 )
Таким образом, у нас появился новый ID версии процедуры, в то время как прежний остался без изменений.
Было: (0:0), стало: (0:0)(0:1)
Т.е. именно этого мы и пытались добиться. Прежние клиенты как использовали (0:0), так и далее будут использовать эти идентификаторы, не переживая о том, что появились новые версии этих процедур.
Также условимся в том, что все новые процедуры нужно добавлять в конец.

Далее, нам нужно позаботиться о том, чтоб ID`ы автоматически проставлялись на обоих сторонах сервиса. Запросто! - просто используем одну и ту же описанную последовательность дважды, для генерации клиентской и серверной сторон!

Самое время представить, как мы хотим видеть все это в конечном счете:
MACRO( client_invoker, // name of the client invoker implementation class ((registration, // procedure name ((std::string, std::string)) // message : registration key )) ((activation, ((std::string)) // message )) ((login, ((std::string)) // message )) ((logout, ((std::string)) // message )) ((users_online, ((std::vector<std::string>)) // without args )) , server_invoker, // name of the server invoker implementation class ((registration, ((std::string)) // username )) ((activation, ((std::string, std::string, std::string)) // registration key : username : password )) ((login, ((std::string, std::string)) // username : password )) ((logout, (()) // without args )) ((users_online, (()) // without args ((std::string)) // substring )) )
Чтоб не было путаницы в том, кто ведущий, а кто ведомый - условимся так, что процедуры, описываемые на одной из сторон, являются реализациями, находящимися на противоположной стороне. Т.е., к примеру,client_invoker::registration(std::string, std::string) говорит нам о том, что реализация этой процедуры будет находиться на стороне сервера, в то время как интерфейс к этой процедуре будет находиться на стороне клиента, и наоборот.
(двойные круглые скобки мы используем потому, что препроцессор при формировании аргумента для нашего MACRO(), развернет нами описанное API. это можно побороть, но не знаю, нужно ли?..)

Итог

Из приведенного выше макровызова, будет сгенерирован код, находящийся под спойлером.

Код

(в качестве сериализации используется другой мой проект - YAS)

Как бонус, была добавлена системная процедура yarmi_error() - используется для сообщения противоположной стороне о том, что при попытке произвести вызов произошла ошибка. Посмотрите внимательно, в client_invoker::invoke(), десериализация и вызов обернуты в try{}catch() блок, а в catch() блоках производится вызов yarmi_error(). Таким образом, если при десериализации или вызове процедуры возникнет исключение - оно будет успешно перехвачено catch()блоком, и информация об исключении будет отправлена вызывающей стороне. То же самое будет происходить и в противоположном направлении. Т.е. если сервер вызвал у клиента процедуру, в ходе вызова которой возникло исключение - клиент отправит серверу информацию об ошибке, также дополнительно сообщив ID и версию вызова, в котором возникло исключение. Но, использовать yarmi_error() вы можете и сами, ничто этого не запрещает. Пример: yarmi_error(call_id, version_id, "message");

Как вы могли заметить, к именам описанных нами процедур, на стороне их реализации добавляется префикс on_

Классы client_invoker и server_invoker принимают два параметра. Первый их низ - класс, в котором реализованы вызываемые процедуры, второй - класс, в котором реализован метод send(yas::shared_buffer buf).
Если у вас один и тот же класс выполняет обе роли, вы можете сделать так:
struct client_session: yarmi::client_base<client_session>, yarmi::client_invoker<client_session> { client_session(boost::asio::io_service &ios) :yarmi::client_base<client_session>(ios, *this) ,yarmi::client_invoker<client_session>(*this, *this) // <<<<<<<<<<<<<<<<<<<<<<<<< {} };
Конечный вариант выглядит так:
struct client_session: yarmi::client_base<client_session>, yarmi::client_invoker<client_session> { client_session(boost::asio::io_service &ios) :yarmi::client_base<client_session>(ios, *this) ,yarmi::client_invoker<client_session>(*this, *this) {} void on_registration(const std::string &msg, const std::string ®key) {} void on_activation(const std::string &msg) {} void on_login(const std::string &msg) {} void on_logout(const std::string &msg) {} void on_users_online(const std::vector<std::string> &users) {} };

Интерфейс к противоположной стороне будет унаследован из предкаyarmi::client_invoker. Т.е., к примеру, будучи в конструкторе нашегоclient_session, вы можете вызвать процедуру registration() следующим образом:
{ registration("niXman"); }
Ответ мы получим в нашу реализацию client_session::on_registration(std::string msg, std::string regkey)
Всё!

Из недоделок нужно отметить следующую:
В именах типов, описывающих процедуры, нельзя использовать запятые, ибо препроцессор не понимает контекста, в котором они используются. Будет исправлено.

В конечном счете, все это вылилось в проект под названием YARMI(Yet Another RMI).
Описанный кодогенератор закодирован в одном файле - yarmi.hpp. В сумме, на реализацию кодогенератора ушел один рабочий день.

Пример использования всего этого дела можно увидеть тут и тут. Первый тестовый проект все еще не завершен, к сожалению.

В дополнение к описанному, на странице проекта вы найдете коды асинхронного многопользовательского однопоточного сервера, и коды клинента.

Вместо заключения

Планы:
1. Генерация нескольких интерфейсов
2. Описать спецификацию (хоть она и проще некуда)
3. Возможность использовать собственный алокатор

Ссылки по теме

  
Помощь
Задать вопрос
 программы
 обучение
 экзамены
 компьютеры
Бесплатный звонок
ICQ-консультанты
Skype-консультанты

Общая справка
Как оформить заказ
Тарифы доставки
Способы оплаты
Прайс-лист
Карта сайта
 
Бестселлеры
Курсы обучения "Atlassian JIRA - система управления проектами и задачами на предприятии"
Microsoft Windows 10 Профессиональная 32-bit/64-bit. Все языки. Электронный ключ
Microsoft Office для Дома и Учебы 2019. Все языки. Электронный ключ
Курс "Oracle. Программирование на SQL и PL/SQL"
Курс "Основы TOGAF® 9"
Microsoft Office 365 Персональный 32-bit/x64. 1 ПК/MAC + 1 Планшет + 1 Телефон. Все языки. Подписка на 1 год. Электронный ключ
Курс "Нотация BPMN 2.0. Ее использование для моделирования бизнес-процессов и их регламентации"
 

О нас
Интернет-магазин ITShop.ru предлагает широкий спектр услуг информационных технологий и ПО.

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

Хорошие отзывы постоянных клиентов и высокий уровень специалистов позволяет получить наивысший результат при совместной работе.

В нашем магазине вы можете приобрести лицензионное ПО выбрав необходимое из широкого спектра и ассортимента по самым доступным ценам. Наши менеджеры любезно помогут определиться с выбором ПО, которое необходимо именно вам. Также мы проводим учебные курсы. Мы приглашаем к сотрудничеству учебные центры, организаторов семинаров и бизнес-тренингов, преподавателей. Сфера сотрудничества - продвижение бизнес-тренингов и курсов обучения по информационным технологиям.



 

О нас

 
Главная
Каталог
Новинки
Акции
Вакансии
 

Помощь

 
Общая справка
Как оформить заказ
Тарифы доставки
Способы оплаты
Прайс-лист
Карта сайта
 

Способы оплаты

 

Проекты Interface Ltd.

 
Interface.ru   ITShop.ru   Interface.ru/training   Olap.ru   ITnews.ru  
 

119334, г. Москва, ул. Бардина, д. 4, корп. 3
+7 (495) 229-0436   shopadmin@itshop.ru
Проверить аттестат
© ООО "Interface Ltd."
Продаем программное обеспечение с 1990 года