Qt新的远程通信机制简介:Qt Remote Objects (QtRO)



  • 0_1524119285345_222.jpg
    Qt新的远程通信机制简介:Qt Remote Objects (QtRO)

    从5.10开始加入。QtRO是一种进程间通信机制(IPC)。通过socket,它能实现进程间甚至是不同计算机之间的远程对象交互功能。QtRO扩展了Qt原有的功能,使得远程通信更加方便。

    接下来是QtRO的简单介绍,对各个部分进行概述。

    QtRO封装了序列化和反序列化过程。用户可以假装有一个对象可供访问,而实际上这个对象存在于另一个进程,甚至另一台计算机中。
    QtRO最大的特点,是它是基于现有的Qt对象API机制(使用Q_PROPERTY, signals以及slots定义的API)。即,有几个默认前提:
    1、远程的对象必须继承自QObject
    2、使用meta-object的机制(包括property、信号和槽)进行互动

    说一句题外话,其实Qt在很多交互场合都使用了这套机制,比如QML Engine和C++交互,JS Engine和C++交互,Web Engine和C++互动,我猜今年推出的Python Engine和C++交互也会使用MOC。

    互动的进程称为Node,每个Node都是对等的。Node上可以包含真正的对象,称作Source;Node也可以包含一个镜像对象,称作Replica。多个Replica可以连接到一个Source,当Source的属性发生变化,都会通知每个Replica。反过来,Replica可以调用函数来改变Source的属性。(每个Node都是对等的,可以既包含Source又包含Replica)

    Replica是一个轻量级的代理(映射Source的状态),同时也是一个继承自QObject的对象,因此可以与本地对象连接信号和槽。也就是说,Replica可以作为信号和槽的转发器,用户使用时就像对待本地对象一样。
    除了Source的映射外,Replica还具备一些属性,用来判断连接状态和是否初始完成。
    为了最大程度节省流量,Replica和Source的通信是基于事件机制。因此在初始时需要对所有属性进行初始化同步。

    QtRO的Source设计时有主动推送变化的功能,而不需要客户端去查询并等待(传统RPC机制)。相对来说更省流量。它和传统RPC有很明显的区别:
    1、双向通信,基于事件机制
    2、完全面向对象而不是函数

    接下来是技术细节介绍。(如果有兴趣可以往下看,内容有些枯燥,也可以参考官方的手册)

    Replica的使用。有两种方法可以获得Replica:
    1、编译时加入
    编译时加入的Replica继承自QRemoteObjectReplica。REPC生成器会自动生成Replica的定义文件(一个头文件)。在pro文件中加入REPC_REPLICA宏可以把生成过程集成到项目编译过程中去。Replica是一个完整的类型(不是虚类),但是没有公开的构造函数,用户需要使用QRemoteObjectNode::acquire模板函数来实例化Replica。
    2、动态获取
    动态获取的Replica继承自QRemoteObjectDynamicReplica。它由非模板函数QRemoteObjectNode::acquire()生成,必须提供一个Source名称。动态Replica使用起来可能有点罗嗦,但是不需要编译时的头文件。并且在QML或者一些脚本语言中使用起来更方便。动态Replica不支持初始化值,在初始化完成前也不能使用。
    以上两种实现方式的一个很重要的区别,就在于未初始化时的状态。因为一个动态Replica只有在初始化后才能获取metaObject,因此在初始化前它基本上没有API可用。反过来,编译时加入的Replica已经具备metaObject,因此在未初始化前它已经有完整的API。用户甚至可以指定属性的默认值,这些默认值可以一直用到Replica被远程Source初始化。

    Source是一个对象。它挂载到Node上,包含有公开API。
    你可以选择使用一个QObject的类直接作为Source,也可以选择在一个.rep文件中定义需要的API,交由REPC生成器生成模板。
    如果你已经有一个完整的QObject类,可以把它传给QRemoeObjectHostBase:: enableRemoting()函数,让它成为一个Source。
    你可以让REPC生成器为你生成Source,即一个定义了API的头文件(在pro文件中使用REPC_SOURCE宏)。有三个选择去实现这些API(假设你的Source类的名字叫做Foo):
    1、继承FooSimpleSource
    在头文件里,定义了一个<Type>SimpleSource类。它给每个属性提供最基本的getter/ setter方法。这里的<Type>代表了.rep文件中Source类名称。即如果.rep文件中定义的类名称叫做“MyType”,那么在生成的头文件中就会有一个MyTypeSimpleSource的类。最简单快速的方法就是继承这个类,并且实现这个类中的API(都是纯虚函数)。你也可以Override任何需要的属性和函数。
    2、继承FooSource
    如果你需要隐藏实现的细节,那么你可以使用<Type>Source类。在生成的头文件中,它是第二个定义的类。这个类的定义没有包含任何数据对象,getter/setting函数也变成了纯虚函数。继承该类,用户有更多的灵活性去实现这个类,当然用户要写更多的代码。
    3、在用户自己的QObject对象里实现FooSourceAPI
    最后这个办法,<Type>SourceAPI类是一个模板类,专门给QRemoeObjectHostBase:: enableRemoting()的模板函数使用。它让你可以使用任意QObject子类(必须含有API)作为Source。如果这个类没有提供正确的API,你会收到编译时的警告。使用这个类让你可以隐藏或变换属性或信号槽。

    在每个端点可以存在许多Source,因此每个Source需要一个独一无二的名称。所有的REPC生成的头文件都有一个方式来确定名称(Q_CLASSINFO宏,用于replica/ simplesource/source,对于SourceAPI使用一个静态的name()函数)。如果你使用自己的QObject子类作为Source(使用QRemoteObjectHostBase::enableRemoting),那么Source的名字使用如下逻辑来决定:
    1、如果该类或者任何它的父类的Q_CLASSINFO序列中定义过RemoteObjectType,那么QRemoteObjectHostBase::enableRemoting中传入的name参数将被使用。
    2、否则,QObject的objectName被使用
    3、如果以上都失败,QRemoteObjectHostBase::enableRemoting返回错误。

    注意:QObject层面的API不会同步给Replica。比如,Replica有一个destroyed信号,它不会触发source的destroyed信号,反之亦然。因为Source和每个Replica都是一个独立的QObject。同步的API在.rep模板文件中定义,或者对于直接使用的QObject的情况,同步的API是在QObject的继承链上的元素。除非在某个父类中定义了Q_CLASSINFO("RemoteObject Type")。如果定义过Q_CLASSINFO("RemoteObject Type")那么那个父类是API检索中最低层。

    Node的使用。Node是进程间通信的平台和基础。所有QtRO的交互数据都被Node封装成尽可能小的独立数据包。
    每个使用QtRO的进程都必须实例化一种Node类(可以是QRemoteObjectNode,QRemoteObjectHost或者是QRemoteObjecRegistryHost)。后面几种Node提供了更多功能。QRemoteObjectHost和QRemoteObjecRegistryHost都支持enableRemoting()/disableRemote()函数,用来把Source对象发布到网络中去。如果要使用Registry功能,网络上必须要有一个QRemoteObjectRegistryHost的Node。然后其他的Node可以传递RegistryHost的URL参数到Node的registryAddress构造函数,或者传送URL参数到setRegistryUrl()函数。
    QtRO是P2P的网络。为了acquire()一个有效的Replica,承载Replica的Node必须连接到带Source的Node。Host Node允许其他Node通过设置独一无二的地址连接到它(地址传递到其他Node的构造函数,或者是使用setHostUrl方法)。Replica所在的Node必须建立与Host Node的连接,才能初始化Replica并保持数据一致。

    使用QtRO的URL连接Node
    Host Node使用用户自定义的URL来简化连接。目前QtRO支持两种连接方式(很有可以会扩展)。一个是使用标准TCP/IP的连接,它即支持同一台设备不同进程间通信,也支持不同设备通信。另一种使用local连接,它协议简单,具体实现依赖于OS特性。但不支持设备间通信。
    使用local连接时,必须使用一个独一无二的名字。使用TCP连接时,必须使用一个独一无二的IP和Port的组合。
    目前QtRO不支持zeroconf(零配置网络规范)。因此所有Node必须事前直销所有连接配置。QRemoteObjectRegistry用来简化连接过程,特别是对与有多个Host Node的网络。
    连接类型总结如下表:

    URL Host Node Connecting Node
    QUrl("local:replica") QLocalServer("replica") QLocalSocket("replica")
    QUrl("tcp://192.168.1.1:9999") QTcpServer("192.168.1.1",9999) QTcpSocket("192.168.1.1",9999)

    Node有多个enableRemoting()函数,用来在网络上共享对象(如果Node不是Host Node则会报错)。其他设备/进程,如果需要与其交互,可以使用Node的acquire()函数来实例化Replica。



  • 首发最新模块的介绍!很棒啊。强烈支持一个!而且我觉得这个技术,和微软的COM组件对象编程很相似,值得我们去推广使用!🤑 :face_with_stuck-out_tongue: :face_with_stuck-out_tongue:



  • 没写完呢。
    这个对Qter非常有用。
    其他平台其实都有类似实现,其实Qt平台上已经有点晚了。



  • @linbin823 这种情况是晚了一些,大家都在使用COM技术,还有各种脚本语言的实现。现在推广来说稍微难了。



  • 期待下文,最好能做个Demo



  • 这东西是不是相当于java的RMI?都是十几年前的东西了,为什么Qt要到现在才开始提供?而且我记得我面试Java工作的时候,RMI和序列化数据也是一个重点,Qt就一点感觉都没有吗?

    我还有一个疑惑很多年了,从学Qt的时候一直到现在:就是J2SE=Qt桌面版,J2ME=Qt嵌入式,但是J2EE为什么在Qt里没有相应的东西?Java真正能火起来其实是靠J2EE,后来又攀上了Android这颗大树。但是桌面版和嵌入式,绝对是Qt完胜。



  • @stlcours 这也不是什么很复杂的东西。QtRO解决的是远程信号槽以及属性的传递。都是非常上层的封装。

    Qt不做服务端,有好多技术和历史因素,毕竟它一路走来都是定位客户端和界面的开发框架。

    当然github上也有用Qt开发的服务端软件,有些benchmark分数还挺高。这个就只能说有需求就去玩玩,但估计成不了主流。



  • @linbin823 能举几个例子吗?有哪些Qt开发的服务端软件?是不是主流不要紧,关键是给我们这些人使用方便啊。



  • @stlcours
    做httpserver:tufao,cutelyst
    这两天还看到一个https://github.com/hgoldfish/qtnetworkng



  • 谢谢Linbin。虽然qtnetworkng是全英文文档,但是例子居然是"https://news.163.com,看来是国人的作品啊!!


Log in to reply
 

走马观花

最近的回复

  • Qt for MCUs

    搭建Qt for MCUs PC端开发环境。qt for mcus提供了一个完整的图形框架和工具包,包含了在MCUs上设计、开发和部署gui所需的一切。它允许您在裸机或实时操作系统上运行应用程序。

    先决条件

    开发主机环境支持仅限于Windows 10

    MSVC compiler v19.16 (Visual Studio 2017 15.9.9 or newer) x64

    CMake v3.13 or newer (you can install it using the Qt Online installer) x64

    使用Qt联机安装程序安装Qt for MCUs,该安装程序可通过Qt帐户下载

    安装Qt 5.14和Qt Creator 4.11 or higher

    安装链接

    › Qt: https://account.qt.io/downloads
    › CMake: https://cmake.org/download/
    › Python 2.7 32-bit: https://www.python.org/downloads/release/python-2716/
    › Arm GCC: https://developer.arm.com/tools-and-software/open-source-software/developer-tools/gnutoolchain/gnu-rm/downloads
    › J-Link Software Pack: https://www.segger.com/downloads/jlink/JLink_Windows.exe
    › J-Link OpenSDA Firmware: https://www.segger.com/downloads/jlink/OpenSDA_MIMXRT1050-EVKHyperflash
    › STM32CubeProgrammer: https://www.st.com/en/development-tools/stm32cubeprog.html
    › STM32 ST-LINK Utility: https://www.st.com/en/development-tools/stsw-link004.html​​​​​​​

    Qt Creator设置 启用Qt Creator插件 选择“帮助>关于插件”,然后从列表中选择“MCU支持(实验性)”插件,重新启动Qt Creator以应用更改
    替代文字 为MCU创建Qt工具包

    选择工具>选项>设备>MCU

    选择Qt for MCUs-Desktop 32bpp作为目标

    如果尚未设置,请提供Qt for MCUs安装目录的路径。

    单击Apply应用。

    替代文字

    替代文字
    替代文字

    注意:

    编译器要选X64,Qt版本要选64bit,CMake Tool选x64

    打开恒温器项目demo

    选择文件>打开文件或项目。。。

    打开CMakefiles.txt文件来自thermo文件夹的文件。

    选择Qt作为MCU-桌面32bpp套件。

    单击“配置项目”以完成。

    替代文字

    问题

    开发主机环境支持仅限于Windows 10

    C++编译失败,文本大字体.pixelSize.

    文本类型无法正确呈现需要复杂文本布局的unicode序列。对复杂文本使用StaticText

    read more
  • H

    hi 有问题请教你,方便加个联系方式吗

    read more
  • boost.asio是一个很棒的网络库,这回儿我也开始系统地学习起来了。想想当年接触boost,也有八年多了。这次开始接触boost,觉得既熟悉又陌生。熟悉的是小写字母+下划线的命名方式、晦涩的模板、很慢的编译速度以及较大的程序体积,陌生的是asio的各种概念:io服务、接收器、套接字等等:我之前对网络编程不是非常了解。

    于是根据我的理解,参考《Boost.Asio C++网络编程》实现了这样一个简单的客户端和服务端通信的例子,例子非常简单,还不完善,但是幸运的是,可以在本机上互通了。
    下面是客户端的代码:

    #include <iostream> #include <boost/asio.hpp> #include <boost/proto/detail/ignore_unused.hpp> using namespace std; using namespace boost::asio; using namespace boost::system; using namespace boost::proto::detail;// 提供ignore_unused方法 void writeHandler( const boost::system::error_code& ec, size_t bytesTransferred ) { if ( ec ) { cout << "Write data error, code: " << ec.value( ) << "transferred: " << bytesTransferred << endl; } else { cout << "OK! " << bytesTransferred << "bytes written. " << endl; } } int main(int argc, char *argv[]) { ignore_unused( argc ); ignore_unused( argv ); io_service service; ip::tcp::socket sock( service ); ip::tcp::endpoint ep( ip::address::from_string( "127.0.0.1" ), 6545 ); boost::system::error_code ec; sock.connect( ep, ec ); if ( ec ) { cout << "Connect error, code: " << ec.value( ) << ", We will exit." << endl; return ec.value( ); } else { char buf[1024] = "Hello world!"; sock.async_write_some( buffer( buf ), writeHandler ); sock.close( ); } return service.run( ); }

    下面是服务端的代码:

    #include <iostream> #include <boost/asio.hpp> #include <boost/proto/detail/ignore_unused.hpp> using namespace std; using namespace boost::asio; using namespace boost::system; using namespace boost::proto::detail;// 提供ignore_unused方法 void acceptHandle( const boost::system::error_code& code ) { cout << "Accepted." << endl; } int main(int argc, char *argv[]) { ignore_unused( argc ); ignore_unused( argv ); io_service service; ip::tcp::endpoint ep( ip::address::from_string( "127.0.0.1" ), 6545 ); boost::system::error_code ec; ip::tcp::socket sock( service ); ip::tcp::acceptor acceptor( service, ep ); acceptor.async_accept( sock, acceptHandle ); if ( ec ) { cout << "There is an error in server. code: " << ec.value( ) << endl; } return service.run( );// 阻塞运行 }

    运行结果是这样的:
    78448d7b-b3ae-42fc-9e2e-4dd2fbdac2c2-image.png

    我对boost.asio中几个概念的理解:

    io_service,这就是一个类似事件循环的东西,它为io设备提供服务,故名。不管是套接字、文件还是串口设备,都要使用它的服务。它的run()函数相当于启动了一个事件循环。一旦有消息了,即进行响应。这也是实现异步编程的重要基础。 socket,这个类则是套接字,可以处理TCP或者是UDP请求。有同步以及异步的处理方式,也有带异常以及不带异常的处理方式。 acceptor,接收器,仅仅是服务端使用。相当于其余框架中的listener,作接收用的。

    比较浅显,如果有不当之处,敬请指正。

    read more
  • 843143141.jpg
    闲下来了,我又开始大规模地学习了。
    最近开始学习内存模型和无锁结构。因为这个是和操作系统密切相关的,懂得这些对于编写C++服务端应用程序
    有着非常好的帮助。之前我对内存模型以及无锁结构几乎没有什么了解,我就询问群里的大佬看看有没有可以参考的资料。
    大佬很高兴,并且推荐了我一本名为《Memory Model》的电子书。这本电子书虽然页数不多,但是从起源到发展,
    从源码到汇编,都给我们详细地介绍了。看了一遍,不是非常理解,但是依然尝试将自己的理解写下来,以便日后翻阅。
    首先因为多核处理器成为主流,多线程的程序已经非常常见,因此我们不可避免地要处理多线程程序的同步问题。
    然后,因为编译器默认都对源码进行了优化,在单核处理器中这通常不是什么问题,但是在多核处理器中,就会因为编译器
    对其进行了乱序处理而导致程序出现问题。由此深入地探讨内存模型。
    内存模型主要分为:
    载-载 顺序(load-load order)
    载-存 顺序(load-store order)
    存-载 顺序(store-load order)
    存-存 顺序(store-store order)
    依赖载入顺序(dependent loads order)

    通过内存栅栏(memory barrier)能够避免编译器对指令的乱序。Linux中有

    READ_ONCE( x, value ) WRITE_ONCE( x )

    避免这些读写被编译器乱序或者是优化掉。

    这里谈到volatile关键字。在另外一篇博客上说,volatile具有“易变性、不可优化性、顺序性”。简单说,由于
    被volatile声明的变量,指令须从内存读取,并且不能被编译器乱序以及优化。在Java(语言扩展)和MSVC(系统兼容)上,
    还附带了Accquire()和Release()语义,因此可部分用于多线程环境。但多数情况下,还是慎用volatile,
    因为不同架构的处理器,它的内存模型是千变万化的,不能一而概之。

    至于C++11,它提供了std::atomic<T>这个模板类,相当于提供了很多方式来实现不同内存模型的原子操作。
    它的load()和store()方法,第二个参数有以下几个选项:

    std::memory_order_relaxed std::memory_order_seq_cst std::memory_order_acq_rel std::memory_order_acquire std::memory_order_release std::memory_order_consume

    我们最常用来实现RCpc(Release Consistency、Processor Consistency)是使用

    std::memory_order_acquire std::memory_order_release

    这两对。

    作为例子,在实现自旋锁时使用std::atomic<T>是这样的:

    struct SpinLock2 { void lock( ) { for ( ; ; ) { while ( lock_.load( std::memory_order_relaxed ) ); if ( !lock_.exchange( true, std::memory_order_acquire ) ) break; } } void unlock( ) { lock_.store( false, std::memory_order_release ); } std::atomic<bool> lock_ = { false }; };

    read more

关注我们

微博
QQ群