介绍

Qt Remote Objects 是Qt推出的进程间通信(IPC)模块,用于实现进程间或远程网络间通信,简化分布式系统的开发。通过抽象底层通信细节,让开发者能够像操作本地对象一样调用远程对象的属性和方法。

关键组成

  1. 源对象(Source)
    在服务端定义实际对象,对外暴露数据和方法。例如,一个控制设备的类可能包含状态属性和控制指令方法。
  2. 副本对象(Replica)
    客户端通过副本对象(Replica)与源对象(Source)交互。Replica 会将客户端的操作转发给服务端,并同步更新属性变化。
  3. 通信节点(Node)
    服务端通过 QRemoteObjectHost 发布对象,客户端通过 QRemoteObjectNode 连接服务端并获取副本对象(Replica)。

关键特性

  • 透明通信:客户端调用远程方法如同本地调用。
  • 自动同步:源对象的属性变化会自动同步到所有客户端副本。
  • 多协议支持:支持本地进程通信(local://)、TCP 网络通信(tcp://)等。
  • 动态发现:可选注册中心(Registry)实现服务自动发现。

典型场景

  • 多进程协作:例如 GUI 应用与后台服务分离。
  • 分布式应用:主控程序与多个子进程共享配置数据。
  • 跨设备控制:如通过 PC 控制嵌入式设备。
  • 微服务架构:将功能模块拆分为独立进程,通过 QtRO 通信。

QtRO与RPC框架的功能比较

通信模式Qt Remote ObjectsRPC(Remote Procedure Call)
多客户端
单向调用✅(Slots)
发布-订阅✅(Signals)
双向流式
状态同步

补充说明

Replica 编译器(repc)基于 API 定义文件(“rep”文件)生成 QObject 头文件。当 repc 处理这些文件时,repc 会生成 Source 头文件和 Replica 头文件。
Qt Remote Objects 支持静态动态副本两种方式,两种方式的客户端代码编写方式有所不同。

虽然 Qt Remote Objects 支持通过网络共享任何 QObject 对象,但使用 repc 定义对象有几个优点:

  • 虽然动态副本(DynamicReplicas)很有用,但使用起来更麻烦。在对象初始化之前,是不知道API的,并且使用 API 时需要通过元对象(QMetaObject)的方法进行字符串查找。
  • 在编译时知道接口可以更容易发现编译时与运行时的问题。
  • rep 格式支持默认值,如果您无法确保在实例化副本对象(Replica)时源对象(Source)可用,则可以使用默认值。

简单示例

静态方式

定义接口

QtRO 静态方式需要先定义副本对象接口文件(.rep),文件语法可以参考Qt Remote Objects Compiler

这里我编写了一个简单的设备控制接口文件:

devicecontroller.rep
1
2
3
4
5
6
7
8
class DeviceController
{
    ENUM Status { Ok, Error };  // 状态枚举
    PROP(Status status = Ok READONLY);  // 状态属性,只读
    SLOT(void controlDevice(QString command));  // 槽函数
    SLOT(void uploadFile(QString fileName, QByteArray fileData));  // 槽函数
    SIGNAL(fileReceived(QString fileName));  // 信号
};

接下来需要把接口文件添加到项目中,项目分为服务端和客户端。

myproject.pro
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# qmake中的写法

# 添加 Qt Remote Objects 支持
QT += remoteobjects

# 服务端项目(生成源对象)
REPC_SOURCE += devicecontroller.rep

# 客户端项目(生成副本对象)
REPC_REPLICA += devicecontroller.rep

# 同时生成源对象和副本对象的头文件
REPC_MERGED = devicecontroller.rep
CMakeLists.txt
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# cmake中的写法

set(CMAKE_INCLUDE_CURRENT_DIR ON)

# 添加 Qt Remote Objects 支持
find_package(Qt6 REQUIRED COMPONENTS Core RemoteObjects)

# 服务端项目(生成源对象)
qt_add_repc_sources(${PROJECT_NAME}
    devicecontroller.rep
)

# 客户端项目(生成副本对象)
qt_add_repc_replicas(${PROJECT_NAME}
    devicecontroller.rep
)

# 同时生成源对象和副本对象的头文件
qt_add_repc_merged(${PROJECT_NAME}
    devicecontroller.rep
)

添加完成后,构建一下项目,可以看到生成了源对象头文件rep_devicecontroller_source.h和副本对象头文件rep_devicecontroller_replica.h

rep_devicecontroller_source.h
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
#ifndef REP_DEVICECONTROLLER_SOURCE_H
#define REP_DEVICECONTROLLER_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 <QtCore/qobject.h>
#include <QtCore/qdatastream.h>
#include <QtCore/qvariant.h>
#include <QtCore/qmap.h>
#include <QtCore/qmetatype.h>

#include <QtRemoteObjects/qremoteobjectnode.h>
#include <QtRemoteObjects/qremoteobjectsource.h>


using namespace Qt::Literals::StringLiterals;

class DeviceControllerSource : public QObject
{
    Q_OBJECT
    Q_CLASSINFO(QCLASSINFO_REMOTEOBJECT_TYPE, "DeviceController")
    Q_CLASSINFO(QCLASSINFO_REMOTEOBJECT_SIGNATURE, "3947cd1a592cb801ecdf88688ceb676821764dc3")
    Q_PROPERTY(Status status READ status NOTIFY statusChanged)

public:
    enum Status {
        Ok = 0,
        Error = 1,
    };
    Q_ENUM(Status)

public:
    explicit DeviceControllerSource(QObject *parent = nullptr) : QObject(parent)
    {
        qRegisterMetaType<Status>();
    }

public:
    ~DeviceControllerSource() override = default;

    virtual Status status() const = 0;

Q_SIGNALS:
    void statusChanged(DeviceControllerSource::Status status);
    void fileReceived(QString fileName);

public Q_SLOTS:
    virtual void controlDevice(QString command) = 0;
    virtual void uploadFile(QString fileName, QByteArray fileData) = 0;

private:
    friend class QT_PREPEND_NAMESPACE(QRemoteObjectNode);
};

class DeviceControllerSimpleSource : public DeviceControllerSource
{
    Q_OBJECT

public:
    explicit DeviceControllerSimpleSource(QObject *parent = nullptr) : DeviceControllerSource(parent)
    , m_status(Ok)
    {}

public:
    ~DeviceControllerSimpleSource() override = default;

    Status status() const override { return m_status; }

protected:
    virtual void setStatus(Status status)
    {
        if (status != m_status) {
            m_status = status;
            Q_EMIT statusChanged(m_status);
        }
    }

private:
    Status m_status;
};

...

#endif // REP_DEVICECONTROLLER_SOURCE_H

服务端

接下来编写服务端代码:

mydeviceservice.h
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#ifndef MYDEVICESERVICE_H
#define MYDEVICESERVICE_H

#include "rep_devicecontroller_source.h"

class MyDeviceService : public DeviceControllerSimpleSource
{
    Q_OBJECT
public:
    explicit MyDeviceService(QObject *parent = Q_NULLPTR);
    ~MyDeviceService();

public Q_SLOTS:
    virtual void controlDevice(QString command) Q_DECL_OVERRIDE;
    virtual void uploadFile(QString fileName, QByteArray fileData) Q_DECL_OVERRIDE;
};

#endif // MYDEVICESERVICE_H
mydeviceservice.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include "mydeviceservice.h"
#include <QDebug>

MyDeviceService::MyDeviceService(QObject *parent)
    : DeviceControllerSimpleSource{parent}
{
    qDebug() << __PRETTY_FUNCTION__;
}

MyDeviceService::~MyDeviceService()
{
    qDebug() << __PRETTY_FUNCTION__;
}

void MyDeviceService::controlDevice(QString command)
{
    qDebug() << __PRETTY_FUNCTION__ << command;
    setStatus(Error);
}

void MyDeviceService::uploadFile(QString fileName, QByteArray fileData)
{
    qDebug() << __PRETTY_FUNCTION__;
    qDebug() << "Receiving file:" << fileName
             << "\nContent: \n" << QString::fromUtf8(fileData);
    setStatus(Ok);
    Q_EMIT fileReceived(fileName);
}

启动服务端:

main.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <QCoreApplication>
#include "mydeviceservice.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    QRemoteObjectHost host;
    // 进程间通信(IPC)
    // host.setHostUrl(QUrl("local:devicecontroller"));
    // 远程过程调用(RPC)
    host.setHostUrl(QUrl("tcp://0.0.0.0:2333"));

    MyDeviceService service;
    /**
     * 第一个参数是源对象指针
     * 第二个参数是源对象名字,客户端可根据名字获取源对象,
     * 使用动态副本的方式时,必须填写源对象名字。
     */
    host.enableRemoting(&service, "mydeviceservice");

    return a.exec();
}

客户端

客户端代码:

mydevicecontroller.h
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#ifndef MYDEVICECONTROLLER_H
#define MYDEVICECONTROLLER_H

#include <QObject>
#include <QPointer>

QT_BEGIN_NAMESPACE
class QRemoteObjectNode;
QT_END_NAMESPACE

class MyDeviceController : public QObject
{
    Q_OBJECT
public:
    explicit MyDeviceController(QObject *parent = Q_NULLPTR);

    Q_INVOKABLE void test(const QUrl &url);

private:
    QPointer<QRemoteObjectNode> node;
    // QPointer<DeviceControllerReplica> replica;
};

#endif // MYDEVICECONTROLLER_H
mydevicecontroller.h
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include "mydevicecontroller.h"
#include "rep_devicecontroller_replica.h"

MyDeviceController::MyDeviceController(QObject *parent)
    : QObject{parent}
    , node{new QRemoteObjectNode(this)}
{}

void MyDeviceController::test(const QUrl &url)
{
    node->connectToNode(url);
    qDebug() << "connected to:" << url;
    
    auto *replica = node->acquire<DeviceControllerReplica>("mydeviceservice");
    connect(replica, &DeviceControllerReplica::statusChanged, this, [&](DeviceControllerReplica::Status s) {
        qDebug() << "Device status changed to:" << s;
    });
    connect(replica, &DeviceControllerReplica::fileReceived, this, [&](QString fileName) {
        qDebug() << "File sent. file -" << fileName;
    });
    connect(replica, &DeviceControllerReplica::stateChanged, this,
            [this](QRemoteObjectReplica::State state, QRemoteObjectReplica::State oldState) {
        qDebug() << "replica state changed:" << oldState << "→" << state;
        // 副本对象状态可用时才能调用接口
        if (state == QRemoteObjectReplica::Valid) {
            DeviceControllerReplica *replica = qobject_cast<DeviceControllerReplica*>(sender());
            Q_ASSERT(replica);

            replica->controlDevice("echo");
            replica->uploadFile("test.txt", QString("你好!世界。").toUtf8());
        }
    });
}

客户端调用:

main.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <QCoreApplication>
#include <QUrl>
#include "mydevicecontroller.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    MyDeviceController client;
    // 进程间通信(IPC)
    // client.test(QUrl("local:devicecontroller"));
    // 远程过程调用(RPC)
    client.test(QUrl("tcp://192.168.1.3:2333"));

    return a.exec();
}

动态副本

动态副本的客户端代码有所不同。

mydevicecontroller.h
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#ifndef MYDEVICECONTROLLER_H
#define MYDEVICECONTROLLER_H

#include <QObject>
#include <QPointer>

QT_BEGIN_NAMESPACE
class QRemoteObjectNode;
QT_END_NAMESPACE

class MyDeviceController : public QObject
{
    Q_OBJECT
public:
    explicit MyDeviceController(QObject *parent = Q_NULLPTR);

    Q_INVOKABLE void test(const QUrl &url);

    Q_SLOT void onStatusChanged(int);
    Q_SLOT void onFileReceived(const QString&);

private:
    QPointer<QRemoteObjectNode> node;
};

#endif // MYDEVICECONTROLLER_H
mydevicecontroller.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include "mydevicecontroller.h"
#include "rep_devicecontroller_replica.h"

MyDeviceController::MyDeviceController(QObject *parent)
    : QObject{parent}
    , node{new QRemoteObjectNode(this)}
{}

void MyDeviceController::test(const QUrl &url)
{
    node->connectToNode(url);
    qDebug() << "connected to:" << url;
    
    auto *replica = node->acquireDynamic("mydeviceservice");
    connect(replica, &QRemoteObjectDynamicReplica::stateChanged, this,
            [this](QRemoteObjectReplica::State state, QRemoteObjectReplica::State oldState) {
                qDebug() << "replica state changed:" << oldState << "→" << state;
                if (state == QRemoteObjectReplica::Valid) {
                    QRemoteObjectDynamicReplica *replica = qobject_cast<QRemoteObjectDynamicReplica*>(sender());
                    Q_ASSERT(replica);

                    /** 由于使用动态副本方式,在副本初始化完成前,无法预先知道API,
                     *  只有在副本对象状态可用时,才能进行信号-槽连接。
                     *  并且无法使用自定义类型参数,如之前定义的枚举。
                     */
                    // connect(replica, SIGNAL(statusChanged(int)), this, SLOT(onStatusChanged(int)));
                    connect(replica, SIGNAL(fileReceived(QString)), this, SLOT(onFileReceived(QString)));

                    /** 调用接口需要使用元对象的`invokeMethod`方法
                      */
                    QMetaObject::invokeMethod(replica, "controlDevice", Q_ARG(QString, "echo"));
                    QMetaObject::invokeMethod(replica, "uploadFile", Q_ARG(QString, "test.txt"),
                                              Q_ARG(QByteArray, QString("你好!世界。").toUtf8()));
                }
            });
}

void MyDeviceController::onStatusChanged(int s)
{
    qDebug() << "Device status changed to:" << s;
}

void MyDeviceController::onFileReceived(const QString &fileName)
{
    qDebug() << "File sent. file -" << fileName;
}

注意事项

Qt Remote Objects 使用内部协议在进程和/或设备之间传递数据。所有部分都需要使用相同的协议版本:如果版本不匹配,连接节点将输出警告,主机节点将不发送任何数据。

目前发布的版本:

协议版本Qt版本
<1.2在 QtRO 的技术预览阶段使用。
1.25.12.0
1.35.12.4
2.06.2.0

See Qt Remote Objects Protocol Versioning.

参考