Межпроцессная реплікація об'єктів за допомогою QtRemoteObjects

7 жовтня 2014 року в публічному доступі з'явилися джерело Qt-модуля QtRemoteObjects. Річ, на мій погляд, дуже перспективна. Модуль дозволяє, наприклад, передавати сигнали між об'єктами по мережі. Але цим можливості модуля не обмежуються. Більш точно суть модуля описує його попереднє найменування — Replica, так як об'єкти «реплікуються» між процесами.



Ключовою ідеєю QtRemoteObjects, яка якісно відрізняє його від інших способів межпроцессного взаємодії/віддаленого виклику процедур, є ідея повністю продублювати Qt-об'єкт в інші процеси. Це означає, що всі зміни властивостей (properties) в об'єкті — джерелі відображаються (з повідомленням допомогою сигналів) в об'єкті-репліці. Будь-які сигнали, які емітуються об'єктом-джерелом будуть емітовані в кожному об'єкті-репліці. Також можна встановлювати властивості, викликати слоти і в об'єкті-репліці, при цьому запити надсилаються об'єкта-джерела, який їх обробляє і потім зміни відображаються на інших об'єктах-репліках допомогою сигналів чи з допомогою зміни властивостей. В результаті всі об'єкти (включаючи об'єкт-джерело) синхронізуються. При цьому вся складність межпроцессного взаємодії прихована всередині QtRemoteObjects.

Збірка і установка модуля

1. Встановити приватні відмінності файли для вашої версії Qt 5
У своїй системі виконано шляхом установки пакета qtbase5-private-dev

2. Отримати код модуля
git clone gitorious.org/qtplayground/qtremoteobjects.git

3. Зібрати і встановити модуль
Збирав проект в QtCreator, використовуючи тіньову збірку. В результаті в директорії build-qtremoteobjects-qt5_3_0-Release з'явилися всі необхідні файли. Якщо потім в даній директорії виконати make install то відмінності файли модуля, бібліотека і кодогенератор для rep файлів будуть встановлені у відповідні директорії. Єдине, що не було встановлено — це вміст директорії qtremoteobjects/mkspecs/features, його потрібно скопіювати в директорію /usr/lib/x86_64-linux-gnu/qt5/mkspecs/features.

Якщо є проблеми з make install, то можна все зробити «вручну»: скопіювати бібліотеку libQt5RemoteObjects.so (dll) в директорію бібліотек Qt а директорію /include/QtRemoteObjects в директорію підключаються файлів Qt відповідно. Також в «ручному» режимі знадобиться скопіювати бінарники з директорії складання bin/repc в директорію бінарників Qt (в моєму випадку це /usr/lib/x86_64-linux-gnu/qt5/bin/repc ).

Використання модуля

Наявність об'єкта — джерела і об'єкта — репліки навіває думку про клієнт-серверній архітектурі. Тому для демонстрації роботи модуля створимо два проекти — клієнт і сервер. Об'єкт-джерело на сервері буде емітувати сигнал, параметри якого буде передаватися текст, а в слоті клієнта, підключеного до сигналу об'єкта-репліки, переданий текст буде оброблятися.
Приклад дуже простий, але на такому прикладі буде простіше показати роботу модуля. Дві картинки одержані додатків і далі код.



Клієнт


Проекти клієнта і сервера
У нас буде одна загальна директорія (наприклад remoteobj) і 2 піддиректорії: client і server, в якій будуть розташовуватися проекти двох додатків. Файли проектів обох додатків необхідно підключити модуль.
QT += remoteobjects

Файли .rep
Файли з розширенням .rep використовуються для опису інтерфейсу об'єкта, який буде використовуватися для межпроцессного взаємодії. В директорії, батьківської по відношенню до директорії проектів клієнта і сервера створюємо тектовый файл MessageSender.rep з наступним вмістом:
#include <QtCore>
class MessageSender
{
SIGNAL(sendMessage(const QString &message));
};

Пам'ятаєте я писав про файли з директорії mkspecs/features? Саме в цих файлах описуються прищепила для обробки файлів rep. Для того щоб вони були опрацьовані в ході складання, файли проектів необхідно додати наступні рядки:
для клієнта
REPC_REPLICA += ../MessageSender.rep
для сервера
REPC_SOURCE += ../MessageSender.rep

Під час складання файлу MessageSender.rep будуть згенеровані відмінності файли
для клієнта — rep_MessageSender_replica.h

#ifndef REP_MESSAGESENDER_REPLICA_H
#define REP_MESSAGESENDER_REPLICA_H

// This is an autogenerated file.
// Do not edit this file, any changes made will be lost the next time it is generated.

#include <QObject>
#include <QVariantList>
#include <QMetaProperty>

#include <QRemoteObjectNode>
#include <QRemoteObjectReplica>
#include <QRemoteObjectPendingReply>

#include <QtCore>
class MessageSenderReplica : public QRemoteObjectReplica
{
Q_OBJECT
Q_CLASSINFO(QCLASSINFO_REMOTEOBJECT_TYPE, "MessageSender")
friend class QRemoteObjectNode;
private:
MessageSenderReplica() : QRemoteObjectReplica() {}
void initialize()
{
QVariantList properties;
properties.reserve(0);
setProperties(properties);
}
public:
virtual ~MessageSenderReplica() {}


Q_SIGNALS:
void sendMessage(const QString & message);
};

#endif // REP_MESSAGESENDER_REPLICA_H



сервера rep_MessageSender_source.h

#ifndef REP_MESSAGESENDER_SOURCE_H
#define REP_MESSAGESENDER_SOURCE_H

// This is an autogenerated file.
// Do not edit this file, any changes made will be lost the next time it is generated.

#include <QObject>
#include <QVariantList>
#include <QMetaProperty>

#include <QRemoteObjectNode>
#include <qremoteobjectsource.h>

#include <QtCore>
class MessageSenderSource : public QObject
{
Q_OBJECT
Q_CLASSINFO(QCLASSINFO_REMOTEOBJECT_TYPE, "MessageSender")
friend class QRemoteObjectNode;
public:
MessageSenderSource(QObject *parent = Q_NULLPTR) : QObject(parent)
{
}
public:
virtual ~MessageSenderSource() {}


Q_SIGNALS:
void sendMessage(const QString & message);
};

class MessageSenderSimpleSource : public QObject
{
Q_OBJECT
Q_CLASSINFO(QCLASSINFO_REMOTEOBJECT_TYPE, "MessageSender")
friend class QRemoteObjectNode;
public:
MessageSenderSimpleSource(QObject *parent = Q_NULLPTR) : QObject(parent)
{
}
public:
virtual ~MessageSenderSimpleSource() {}


Q_SIGNALS:
void sendMessage(const QString & message);
};

template < class ObjectType>
struct MessageSenderSourceAPI : public SourceApiMap
{
MessageSenderSourceAPI()
{
_properties[0] = 0;
_signals[0] = 1;
_signals[1] = qtro_signal_index<ObjectType>(&ObjectType::sendMessage, static_cast<void (QObject::*)(QString)>(0),signalArgCount+0,signalArgTypes[0]);
_methods[0] = 0;
}

QString name() const Q_DECL_OVERRIDE { return QStringLiteral("MessageSender"); }
int propertyCount() const Q_DECL_OVERRIDE { return _properties[0]; }
int signalCount() const Q_DECL_OVERRIDE { return _signals[0]; }
int methodCount() const Q_DECL_OVERRIDE { return _methods[0]; }
int sourcePropertyIndex(int index) const Q_DECL_OVERRIDE { return _properties[index+1]; }
int sourceSignalIndex(int index) const Q_DECL_OVERRIDE { return _signals[index+1]; }
int sourceMethodIndex(int index) const Q_DECL_OVERRIDE { return _methods[index+1]; }
int signalParameterCount(int index) const Q_DECL_OVERRIDE { return signalArgCount[index]; }
int signalParameterType(int sigIndex, int paramIndex) const Q_DECL_OVERRIDE { return signalArgTypes[sigIndex][paramIndex]; }
int methodParameterCount(int index) const Q_DECL_OVERRIDE { return methodArgCount[index]; }
int methodParameterType(int methodIndex, int paramIndex) const Q_DECL_OVERRIDE { return methodArgTypes[methodIndex][paramIndex]; }
int propertyIndexFromSignal(int index) const Q_DECL_OVERRIDE
{
Q_UNUSED(index);
return -1;
}
const QByteArray signalSignature(int index) const Q_DECL_OVERRIDE
{
switch (index) {
case 0: return QByteArrayLiteral("sendMessage(QString)");
}
return QByteArrayLiteral("");
}
const QByteArray methodSignature(int index) const Q_DECL_OVERRIDE
{
Q_UNUSED(index);
return QByteArrayLiteral("");
}
QMetaMethod::MethodType methodType(int) const Q_DECL_OVERRIDE
{
return QMetaMethod::Slot;
}
const QByteArray typeName(int index) const Q_DECL_OVERRIDE
{
Q_UNUSED(index);
return QByteArrayLiteral("");
}

int _properties[1];
int _signals[2];
int _methods[1];
int signalArgCount[1];
const int* signalArgTypes[1];
int methodArgCount[0];
const int* methodArgTypes[0];
};
#endif // REP_MESSAGESENDER_SOURCE_H



Дані файли необхідно підключити в заголовних файлах клієнта і сервера — відповідно їх приналежності.

Клієнт
Першим розберемо клієнт, так як він простіше сервера.
Потрібно додати член класу клієнта:
QRemoteObjectNode clientNode;

І потім ініціалізуючої функції (або прямо в конструкторі) пишемо:

clientNode = QRemoteObjectNode::createNodeConnectedToRegistry(); //створюємо ноду
QRemoteObjectReplica *sender = m_client.acquire< MessageSenderReplica >(); //отримуємо вказівник на репліку
connect(sender, SIGNAL(sendMessage(const QString &)), this, SLOT(appendMessage(const QString &))); //коннектим сигнал до слоту

У слоті appendMessage отримана рядок просто додається в список і тому його опис я пропущу і перейду до опису клієнта.

Так як в згенерованих заголовних файлах знаходиться тільки інтерфейс, то для виконання корисної роботи нашим об'єктом — джерелом, необхідно додати йому функціонал. Для цього наследуемся від згенерованого класу і визначаємо слот:

class MessageSender : public MessageSenderSource
{
Q_OBJECT

public slots:
void postMessage(const QString &message);
};

Реалізація цього слота буде просто емітувати сигнал.

void MessageSender::postMessage(const QString &message)
{
emit sendMessage(message);
}


Тепер додаємо наступні члени класу сервера:

MessageSender *serverSender; //описаний вище клас
QRemoteObjectNode registryHostNode; //нода регістра
QRemoteObjectNode objectNode; //нода об'єкта


І ініціалізуючої функції (або прямо в конструкторі) пишемо:

connect(ui->sendButton, SIGNAL(clicked()), this, SLOT(startSendMessage())); //обробка кнопки
registryHostNode = QRemoteObjectNode::createRegistryHostNode(); //створюємо ноду регістра
objectNode = QRemoteObjectNode::createHostNodeConnectedToRegistry(); //створюємо ноду об'єкта
serverSender = new MessageSender(); //створюємо об'єкт-джерело
objectNode.enableRemoting(serverSender);//биндим його до ноде


У слоті startSendMessage() буде викликатися слот об'єкта-джерела:

QString messageText = ui->messageTextEdit->text();
serverSender->postMessage(messageText);


Тепер запускаємо програми: спочатку сервер а потім клієнт.

Робота по мережі
В даному прикладі було описанно межпроцессное взаємодія в рамках одного хоста. При створенні нод без параметрів вважається, що взаємодія локальне.

Для взаємодії по мережі потрібно модифікувати код створення нод
на стороні сервера

objectNode = QRemoteObjectNode::createHostNode(QUrl("tcp://localhost:9999"));
//registryHostNode не використовується

на стороні клієнта

clientNode = QRemoteObjectNode();
clientNode.connect(QUrl("tcp://localhost:9999"));


Замість висновку
У статті не розглядається використання властивостей (properties). Приклад використання властивостей і слотів (які також описані у файлі rep) можна побачити в examples, що поставляються разом з модулем.

Посилання на архів з исходниками сервера і клієнта.

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

0 коментарів

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