QT Remote Object(QT RO)进程通信/远端调用功能非常方便,通过qt封装好的异步调用策略能很灵活的实现程序中的各种功能,一般在进程间通信中应用广泛,实现方式和qt的MetaObject
异步调用方式有一定的相似性,因此放到一起做一些简单总结和记录。
QT中的QMetaObject
提供了一种很便捷的异步调用方式,一般情况我们执行比较耗时的操作又不想阻塞程序的一些既定功能时,使用 QMetaObject::invokeMethod
能很方便的完成这项工作,相对C++的手动创建线程并设计异步的同步机制来说,QMetaObject::invokeMethod
提供了比较丰富的调用方式和功能。在日常c++开发过程中,对于一些已经引用了qt的程序中使用这种方式是一种不错的选择。
static bool invokeMethod(QObject *obj, const char *member,
Qt::ConnectionType,
QGenericReturnArgument ret,
QGenericArgument val0 = QGenericArgument(Q_NULLPTR),
QGenericArgument val1 = QGenericArgument(),
QGenericArgument val2 = QGenericArgument(),
QGenericArgument val3 = QGenericArgument(),
QGenericArgument val4 = QGenericArgument(),
QGenericArgument val5 = QGenericArgument(),
QGenericArgument val6 = QGenericArgument(),
QGenericArgument val7 = QGenericArgument(),
QGenericArgument val8 = QGenericArgument(),
QGenericArgument val9 = QGenericArgument());
static inline bool invokeMethod(QObject *obj, const char *member,
QGenericReturnArgument ret,
QGenericArgument val0 = QGenericArgument(Q_NULLPTR),
QGenericArgument val1 = QGenericArgument(),
QGenericArgument val2 = QGenericArgument(),
QGenericArgument val3 = QGenericArgument(),
QGenericArgument val4 = QGenericArgument(),
QGenericArgument val5 = QGenericArgument(),
QGenericArgument val6 = QGenericArgument(),
QGenericArgument val7 = QGenericArgument(),
QGenericArgument val8 = QGenericArgument(),
QGenericArgument val9 = QGenericArgument())
{
return invokeMethod(obj, member, Qt::AutoConnection, ret, val0, val1, val2, val3,
val4, val5, val6, val7, val8, val9);
}
static inline bool invokeMethod(QObject *obj, const char *member,
Qt::ConnectionType type,
QGenericArgument val0 = QGenericArgument(Q_NULLPTR),
QGenericArgument val1 = QGenericArgument(),
QGenericArgument val2 = QGenericArgument(),
QGenericArgument val3 = QGenericArgument(),
QGenericArgument val4 = QGenericArgument(),
QGenericArgument val5 = QGenericArgument(),
QGenericArgument val6 = QGenericArgument(),
QGenericArgument val7 = QGenericArgument(),
QGenericArgument val8 = QGenericArgument(),
QGenericArgument val9 = QGenericArgument())
{
return invokeMethod(obj, member, type, QGenericReturnArgument(), val0, val1, val2,
val3, val4, val5, val6, val7, val8, val9);
}
static inline bool invokeMethod(QObject *obj, const char *member,
QGenericArgument val0 = QGenericArgument(Q_NULLPTR),
QGenericArgument val1 = QGenericArgument(),
QGenericArgument val2 = QGenericArgument(),
QGenericArgument val3 = QGenericArgument(),
QGenericArgument val4 = QGenericArgument(),
QGenericArgument val5 = QGenericArgument(),
QGenericArgument val6 = QGenericArgument(),
QGenericArgument val7 = QGenericArgument(),
QGenericArgument val8 = QGenericArgument(),
QGenericArgument val9 = QGenericArgument())
{
return invokeMethod(obj, member, Qt::AutoConnection, QGenericReturnArgument(), val0,
val1, val2, val3, val4, val5, val6, val7, val8, val9);
}
此函数用于调用被调用对象的信号或槽。调用成功返回true。如果没有此类成员或参数不匹配,则返回false。
参数说明:
obj:被调用对象的指针
member:成员方法的名称
type:连接方式,默认值为 Qt::AutoConnection
Qt::DirectConnection
,则会立即调用该成员。(同步调用)
Qt::QueuedConnection
,则会发送一个QEvent,并在应用程序进入主事件循环后立即调用该成员。(异步调用)
Qt::BlockingQueuedConnection
,则将以与Qt :: QueuedConnection
相同的方式调用该方法,除了当前线程将阻塞直到事件被传递。使用此连接类型在同一线程中的对象之间进行通信将导致死锁。(异步调用)
Qt::AutoConnection
,则如果obj与调用者位于同一个线程中,则会同步调用该成员; 否则它将异步调用该成员。
ret:接收被调用函数的返回值
val0~val9:传入被调用函数的参数,最多十个参数。
(必须要使用如Q_RETURN_ARG()
等等的宏来封装函数返回值、Q_ARG()
宏来封装函数参数。)
void CommonInterface::sendMsg(const QString& msg)
{
emit sigMessage(msg);
}
void MsgManager::send(const QString& msg)
{
QMetaObject::invokeMethod(ptr, "sendMsg", Q_ARG(QString, msg));
}
顺便可以使用Qt命名类型所提供的Q_ENUM()
或者Q_DECLARE_METATYPE()
、qRegisterMetaType()
来注册枚举或自定义类型。
struct MyStruct {
int param;
int result;
};
Q_DECLARE_METATYPE(MyStruct);
class CommonInterface :public CommonInterfaceSource
{
Q_OBJECT
public:
enum RES
{
RES_NO_ERROR,
RES_NOT_INIT,
RES_ERROR
};
Q_ENUM(RES);
explicit CommonInterface(QObject *parent = nullptr) {};
virtual void onMessage(QString msg);
public slots:
void sendMsg(const QString& msg);
RES test(MyStruct& ss);
signals:
void SigRecvMsg(const QString& msg);
};
进程间远程调用一般使用QT RemoteObject
,QtRO本质上应该也是基于Socket来封装的。每个进程通过QRemoteObjectNode
接入QtRO网络。服务方需要使用QRemoteObjectHost
将一个提供实际功能的QObject派生类注册进QtRO网络中,然后其他使用该功能的程序则通过各自的QRemoteObjectNode
连接到该Host上,然后acquire
一个该功能对象的Replica
。等到该Replica
初始化好后,该程序就能够使用Replica
中的信号、槽以及属性,就好像功能类就在本地一样。
Source
端和Replica
端必须严格使用同一版本的rep文件,即使rep文件内只是添加了一行注释,否则会连接不上。
rep文件是一种DSL(Domain Specific Language),专门用于定义QtRO接口。在编译的时候,该文件会首先经过repc.exe这个程序处理,生成对应的头文件和源文件。只要安装Qt时选择了Qt RemoteObjects模块,repc.exe就在Qt安装目录的bin目录中。
我们通过在rep文件中定义接口,用于QtRO中进行共享。查看详细说明
对于rep文件的手动命令参数如下(qt5.9.8为例):
repc tool v0.1 (Qt 5.9.8).
Options:
-?, -h, --help Displays this help.
-v, --version Displays version information.
-i <rep|src> Input file type:
rep: replicant template files.
src: C++ QObject derived classes.
-o <source|replica|merged|rep> Output file type:
source: generates source header. Is
incompatible with "-i src" option.
replica: generates replica header.
merged: generates combined replica/source
header.
rep: generates replicant template file from
C++ QOject classes. Is not compatible with "-i
rep" option.
-I <dir> Add dir to the include path for header files.
This parameter is needed only if the input
file type is src (.h file).
-c Always output `class` type for .rep files and
never `POD`.
-d Print out parsing debug information (for
troubleshooting).
Arguments:
[header-file/rep-file] Input header/rep file to read from, otherwise
stdin.
[rep-file/header-file] Output header/rep file to write to, otherwise
stdout.
对于手动生成来说
repc -o source CommonInterface.rep -c rep_Commoninterface_source.h
repc -o replica CommonInterface.rep -c rep_Commoninterface_replica.h
或者使用-I增加指定目录Dir。但是这种方式比较麻烦,增加了每次修改编译程序的工作量。
一般在VS工程中,我们将rep文件添加到project目录中并进行配置即可:
命令行使用刚才的命令即可(也可以手动添加QT RO相关头文件目录):
对于source:
$(QTDIR)\bin\repc.exe -o source -I $(QTDIR)\include -I $(QTDIR)\include/QtRemoteObjects -I $(QTDIR)\include -I $(QTDIR)\include/QtRemoteObjects CommonInterface.rep rep_CommonInterface_source.h
对于replica:
$(QTDIR)\bin\repc.exe -o replica -I $(QTDIR)\include -I $(QTDIR)\include/QtRemoteObjects -I $(QTDIR)\include -I $(QTDIR)\include/QtRemoteObjects CommonInterface.rep rep_CommonInterface_replica.h
输出:
$(SolutionDir)\$(ProjectName)\rep_%(Filename).h
对于代码,与线程调用区别不是很大,主要在初始化过程可能有些区别。这里贴个最最简单的例子:
服务端
头文件:
#include "rep_CommonInterface_source.h"
class CommonInterface :public CommonInterfaceSource
{
Q_OBJECT
public:
explicit CommonInterface(QObject *parent = nullptr) {};
virtual void onMessage(QString msg);
public slots:
void sendMsg(const QString& msg);
signals:
void SigRecvMsg(const QString& msg);
};
class MsgManager :public QObject
{
Q_OBJECT
public:
MsgManager(CommonInterface* p = nullptr);
void init();
void run();
void test();
public slots:
void sendMsg(const QString& msg);
void recvMsg(const QString& msg);
private:
CommonInterface* ptr;
};
cpp:
void CommonInterface::sendMsg(const QString& msg)
{
emit sigMessage(msg);
}
MsgManager::MsgManager(CommonInterface* p) :ptr(p)
{
init();
}
void MsgManager::init()
{
ptr = new CommonInterface;
QRemoteObjectHost* host = new QRemoteObjectHost();
host->setHostUrl(QUrl("local:wangx"));
host->enableRemoting(ptr);
QObject::connect(ptr, SIGNAL(SigRecvMsg(QString)), this, SLOT(recvMsg(QString)));
}
void MsgManager::recvMsg(const QString& msg)
{
qDebug() << msg;
}
void MsgManager::sendMsg(const QString& msg)
{
QMetaObject::invokeMethod(ptr, "sendMsg", Q_ARG(QString, msg));
}
客户端
头文件:
#include "rep_CommonInterface_replica.h"
class MsgManager :public QObject
{
Q_OBJECT
public:
MsgManager(QObject* p = nullptr);
void init();
public slots:
void sendMsg(const QString& msg);
void recvMsg(const QString& msg);
public:
CommonInterfaceReplica* ptr;
};
cpp
MsgManager::MsgManager(QObject* p) :QObject(p)
{
}
void MsgManager::init()
{
QRemoteObjectNode *remoteNode = new QRemoteObjectNode(this);
remoteNode->connectToNode(QUrl("local:wangx"));
ptr = remoteNode->acquire<CommonInterfaceReplica>();
QObject::connect(ptr, SIGNAL(sigMessage(QString)), this, SLOT(recvMsg(QString)));
}
void MsgManager::sendMsg(const QString& msg)
{
QMetaObject::invokeMethod(ptr, "onMessage", Q_ARG(QString, msg));
}
void MsgManager::recvMsg(const QString& msg)
{
qDebug() << msg;
}
动态方式不在需要严格限制服务端与客户端的rep文件完全一致,确切的说客户端根本不在需要在引用replica.h
的头文件。这种方式相对灵活性更多一些,一些个别场景也很实用。
其实与静态相比区别并不大,主要区别在于客户端初始化时不在使用传统的acquire
方式来获取RO对象而是转而使用动态的方式。并且将自定义的信号与槽函数的连接逻辑写到响应initialized
当中。
客户端修改如下:
MsgManagerDy::MsgManagerDy(QObject* p) :QObject(p)
{
init();
}
void MsgManagerDy::onInitConnect()
{
QObject::connect(m_pInterface, SIGNAL(sigMessage(QString)), this, SLOT(recvMsg(QString)));
}
void MsgManagerDy::init()
{
m_pRemoteNode = new QRemoteObjectNode();
m_pRemoteNode->connectToNode(QUrl("local:wangx_test"));
m_pInterface = m_pRemoteNode->acquireDynamic("wangx_test");
connect(m_pInterface, &QRemoteObjectDynamicReplica::initialized, this, &MsgManagerDy::onInitConnect);
}
对于服务端,只需要在enableRemoting
时增加一个名称即可:
MsgManagerDy::MsgManagerDy(CommonInterface* p) :ptr(p)
{
init();
}
void MsgManagerDy::init()
{
ptr = new CommonInterface;
QRemoteObjectHost* host = new QRemoteObjectHost();
host->setHostUrl(QUrl("local:wangx_test"));
host->enableRemoting(ptr, QStringLiteral("wangx_test"));
}
写了一些小demo,均为简单的非ui的控制台测试程,主要关于QT RO静态调用和动态调用逻辑,顺便将一些功能封装到了动态库dll中,可以简单实现一些一对一/多对多进程通信方案。
demo下载地址