qt/cpp程序实现跨线程/进程异步调用(QMetaObject/QT RemoteObject)

简介

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中的信号、槽以及属性,就好像功能类就在本地一样。

静态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;
}

动态Replica

动态方式不在需要严格限制服务端与客户端的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下载地址

本资源设置1个资源分,您可以下载作为捐献。 如果您有Git,还可以从http://www.goldenhawking.org:3000/goldenhawking/zoom.pipeline直接签出最新版本 (上一个版本“一种可伸缩的全异步C/S架构服务器实现”是有问题的,现在已经完成更改)。 服务由以下几个模块组成. 1、 网络传输模块。负责管理用于监听、传输的套接字,并控制数据流在不同线程中流动。数据收发由一定规模的线程池负责,实现方法完全得益于Qt线程事件循环。被绑定到某个Qthread上的Qobject对象,其信号-槽事件循环由该线程负责。这样,便可方便的指定某个套接字对象使用的线程。同样,受惠于Qt的良好封装,直接支持Tcp套接字及SSL套接字,且在运行时可动态调整。(注:编译这个模块需要Qt的SSL支持,即在 configure 时加入 -openssl 选项) 2、 任务流水线模块。负责数据的处理。在计算密集型的应用中,数据处理负荷较重,需要和网络传输划分开。基于普通线程池的处理模式,也存在队列阻塞的问题——若干个客户端请求的耗时操作,阻塞了其他客户端的响应,哪怕其他客户端的请求很短时间就能处理完毕,也必须排队等待。采用流水线线程池避免了这个问题。每个客户端把需要做的操作进行粒度化,在一个环形的队列中,线程池对单个客户端,每次仅处理一个粒度单位的任务。单个粒度单位完成后,该客户端的剩余任务便被重新插入到队列尾部。这个机制保证了客户端的整体延迟较小。 3、 服务集群管理模块。该模块使用了网络传输模块、任务流水线模块的功能,实现了进程的服务器ßà服务器链路。在高速局域网中,连接是快速、稳定的。因此,该模块被设计成一种星型无中心网络。任意新增服务器节点选择现有服务器集群中的任意一个节点,接入后,通过广播自动与其他服务器节点建立点对点连接。本模块只是提供一个服务器到服务器的通信隧道,不负责具体通信内容的解译。对传输内容的控制,由具体应用决定。 4、 数据库管理模块。该模块基于Qt的插件式数据库封装QtSql。数据库被作为资源管理,支持在多线程的条件下,使用数据库资源。 5、 框架界面。尽管常见的服务运行时表现为一个后台进程,但为了更好的演示服务器的功能,避免繁琐的配置,还是需要一个图形界面来显示状态、设置参数。本范例中,界面负责轮训服务器的各个状态,并设置参数。设置好的参数被存储在一个ini文件中,并在服务开启时加载。 6、应用专有部分模块。上述1-4共四个主要模块均是通用的。他们互相之间没有形成联系,仅仅是作为一种资源存在于程序的运行时(Runtime)之中。应用专有部分模块根据具体任务需求,灵活的使用上述资源,以实现功能。在范例代码中,实现了一种点对点的转发机制。演示者虚拟出一些工业设备,以及一些操作员使用的客户端软件。设备与客户端软件在成功认证并登录后,需要交换数据。改变这个模块的代码,即可实现自己的功能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值