Qt Remote Objects 是从 Qt5.9 加入的 RPC 远程调用模块,详情可以从文档中了解:
https://doc.qt.io/qt-5/qtremoteobjects-index.html
从个人使用实践来看,相比其他流行的 RPC 框架,也有一些优势:
- Qt-like:如果使用 grpc,那么一些 Qt 自带的类型,如 QString 、QByteArray 等需要转换成 grpc 支持的几种基本类型,序列化时会有字符串编解码不一致的风险。使用 QtRO 则天生支持这些 Qt 的类型,而且定义的接口支持信号槽。
- 轻便:QtRO 模块是一个轻量级的模块,使用简单,也不用再去编译第三方库。
也存在一些问题:
- 功能太少:目前只支持一些元对象之上的接口定义,如属性、信号槽,没有流式接口,没有普通类成员或函数。
- host 不能直接访问当前连接的 node,服务端是所有已连接的 node 共享的,如果 host-source 发信号,那么所有连接的 node 都会收到这个信号。从这点来看 QtRO 更适合单个客户端的进程交互,不适合多个客户端的并发访问,多个客户端时要独立操作则不该使用信号,可以通过槽函数返回值来返回结果。
QtRO 模块使用前先在 pro 文件加上 remoteobjects 启用该模块
QT += remoteobjects
有两种使用方式:1.Static Source,定义一个 rep 接口文件,分别生成服务端和客户端的接口定义;2.Dynamic Replica,服务端定义接口(用不用 rep 都可以),客户端连接上后动态获取元对象信息。
本文 Demo:
https://github.com/gongjianbo/MyTestCode/tree/master/Qt/QtRemoteObjects
1.Static Source(使用 rep 生成接口定义的方式)基本使用流程为:写接口定义文件 rep,生成 replica(客户端) 和 source(服务端) 两套接口实现,在代码中使用生成的接口进行远程调用。
1.1.接口定义 rep 文件接口定义参考官方文档:
https://doc.qt.io/qt-5/qtremoteobjects-repc.html
属性、信号槽的定义类似 QObject 子类中的定义方式。
这里分享几个我遇到的问题:
- ENUM 得定义在 class 中,不然不能直接访问,因为它会在外面套一层 QObject 类。
- POD 成员都是属性,如果想使用自定义的结构体不能直接写在 rep 中,需要 include 结构体头文件,这样只要信号槽支持的自定义类型就可以随便用了。
- 使用自定义类型需要写比较运算符以支持如自动生成的属性 set 函数中进行比较,还要写 QDataStream 的输入输出序列化。
下面是我的 simple.rep 接口定义文件和 Define.h 结构体头文件:
//rep文档https://doc.qt.io/qt-5/qtremoteobjects-repc.html
//引入结构体定义的头文件
#include "Define.h"
//POD,类似结构体,但是成员都是属性
POD MyPod(QString name, int age, bool sex)
//MODEL和QAbstractItemModel相关,太鸡肋,略去
//类
class MyObject{
//属性
PROP(int value)
}
//会生成两种接口
//[类名Replica]给客户端
//[类名Source]和[类名SimpleSource]给服务端
class Interface{
//属性
PROP(QString name="gongjianbo" READONLY)
PROP(QString tag)
//使用rep中定义的类作为属性
CLASS subObject(MyObject)
//信号
SIGNAL(dataChanged(const QString &data))
//槽函数,省略返回值则为void
SLOT(void setData(const QString &data))
SLOT(QString getData())
//枚举,不放在class里会自动再套一层QObject类不方便访问
ENUM MyEnum{
EnumA = 0,
EnumB
}
SLOT(void testEnum(MyEnum t))
//使用自定义的结构体
SLOT(void testStruct(MyStruct t))
}
#pragma once
#include
#include
//支持使用自定义结构体,但是只能以include的方式,不能直接在rep中定义
//由于自动生成的代码需要比较和序列化,所以要重载一些函数
struct MyStruct
{
QString name;
int age;
bool sex;
};
Q_DECLARE_METATYPE(MyStruct)
bool operator==(const MyStruct &left, const MyStruct &right);
bool operator!=(const MyStruct &left, const MyStruct &right);
//重载输入输出运算符,以支持datastream序列化自定义结构
QDataStream& operator (QDataStream& in, MyStruct& item);
定义好之后在接口客户端引入:
REPC_REPLICA += $$PWD/simple.rep
构建后会在输出目录生成 rep_simple_replica.h,里面有实现了接口的 QRemoteObjectReplica 子类。
在接口服务端引入:
REPC_SOURCE += $$PWD/simple.rep
构建后会在输出目录生成 rep_simple_source.h,里面有实现了部分接口(槽需要继承重写)的一个接口类。
当然也可以用 REPC_MERGED 引入,会同时生成客户端和服务端的两套接口。
1.2.基本操作生成接口类文件后,在客户端和服务端分别引入。服务端可能需要实现部分虚函数,客户端可以直接使用。
首先是客户端的基本操作(注意这个 Interface 是因为我 rep 接口定义取的名字叫 Interface):
QRemoteObjectNode remoteNode;
remoteNode.connectToNode(QUrl("local:qro_test"));
InterfaceReplica *replica = remoteNode.acquire();
//客户端可以判断连接与断开,服务端没有对应接口查询客户端列表
connect(replica,&InterfaceReplica::stateChanged,
this,[this](QRemoteObjectReplica::State state, QRemoteObjectReplica::State oldState){
});
//连接服务端信号
connect(replica,&InterfaceReplica::dataChanged,[]{});
//调用服务端的接口
if(replica->isReplicaValid())
replica->setData("hello");
比较可惜的是客户端不能直接发信号,只能接收服务端过来的信号,以及调用槽函数。connetToNode 调用后如果服务端还没开,它会自动检测,也可以调用 replica 的 waitForSource 接口主动等。服务端重启后 node 会自动重连。
服务端的基本操作:
QRemoteObjectHost host;
MySource source; //继承生成的类并实现了部分虚函数
host.setHostUrl(QUrl("local:qro_test"));
host.enableRemoting(&source);
//开启服务后可以发信号或者等被调用
emit source.dataChanged("hello");
客户端可以判断连接与断开,服务端没有对应接口查询客户端列表,这点比较可惜。
2.Dynamic Replica(动态获取接口信息的方式)基本使用流程为:source(服务端)定义好接口,replica(客户端)连接成功后通过宏形式的信号槽,或者 QMetaObject::invokeMethod 来调用服务端接口。
相对于 Static Source,主要区别在于客户端没法直接访问服务端接口了,只能通过字符串的形式来调用。而服务端原本 rep 生成的就只是个 QObject 的子类,我们自己定义都可以。
QRemoteObjectNode remoteNode;
remoteNode.connectToNode(QUrl("local:qro_test"));
//acquire的name,和host.enableRemoting的name配对
QRemoteObjectDynamicReplica *replica = remoteNode.acquireDynamic("QRO");
//客户端可以判断连接与断开,服务端没有对应接口查询客户端列表
connect(replica,&InterfaceReplica::stateChanged,
this,[this](QRemoteObjectReplica::State state, QRemoteObjectReplica::State oldState){
});
//连接服务端信号槽,需要在连接完成后
connect(replica,&QRemoteObjectDynamicReplica::initialized,
[](){
connect(replica, SIGNAL(serverSignal(const QString &)), this, SLOT(clientSlot(const QString &)));
connect(this, SIGNAL(clientSignal(const QString &)), replica, SLOT(serverSlot(const QString &)));
});
//调用服务端的接口
if(replica->isReplicaValid())
QMetaObject::invokeMethod(replica, "setData", Qt::DirectConnection, Q_ARG(QString, "hello"));
3.参考
文档:https://doc.qt.io/qt-5/qtremoteobjects-gettingstarted.html
博客:https://zhuanlan.zhihu.com/p/347981991
博客:https://zhuanlan.zhihu.com/p/36501814