Qt Remote Objects
Remote Object Concepts
Qt远程对象 (QtRO) 是为Qt开发的进程间通信(IPC)模块. 这个模块扩展Qt现有功能, 使进程或计算机之间可以轻松地进行信息交换.
为了实现信息交换, Qt的关键功能之一是区分对象的API(由属性, 信号和槽函数定义)和实现. QtRO是为了满足预期API, 即真正的 QObject 处于不同的进程. 调用对象副本的槽函数 (QtRO的 Replica) 转到真正的对象 (QtRO的 Source) 处理. 每个Replica都接收Source的更新, 无论是属性更改还是发送信号.
Replica 是 Source 对象的轻量级代理, Replica支持对象的相同连接和行为,这使得Replica可以像Qt的其他 QObject 对象一样使用. 在后台, QtRO处理 Replica 的所有事情, 看起来就跟 Source 一样.
注意: 远程对象的行为跟传统的远程过程调用(RPC)实现不同, 例如: 在RPC中, 客户端发送请求, 等待响应. 在RPC中, 除对客户端的响应, 服务器不会向客户端推送任何数据. 通常, RPC的设计使不同客户端相互独立. 如, 两个客户端向服务端请求方向, 得到不同的结果. 在QtRO中, Qt虽然以无属性的 Source 和具有返回值的槽函数实现这种功能, 但是QtRO隐藏远程处理这一事实. 你可以让一个节点提供 Replica 而不是自己创建它, 你可能用到状态信号 (isReplicaValid()), 随后, 你可以像普通 QObject对象一样交互.
案例: GPS
考虑全球定位系统(GPS)的接收传感器. 在QtRO中: Source 之间与GPS硬件交互, 并计算你当前位置. 这个位置作为 QObject 属性公布, 并周期性更新位置属性, 发出属性变更信号. Replica在其他进程创建, 一直获取你的当前位置, 且不需要处理传感器数据. 在 Replica 中连接的位置改变信号按照预期工作; Source 发出的信号将触发每个 Replica的信号.
案例: 访问打印机
另一个QtRO的示例是打印服务. Source 是直接控制打印机的进程. 墨水量和打印机状态是QObject属性. 这些属性更改时会发出属性改变信号. 关键功能-打印文件-传递给打印机. 顺便说一句, 这与Qt信号和槽函数机制一致, QtRO将通过 Replica 调用 Source. 实际上, 属性和信号从 Source 到 Replica, 槽函数从 Replica 转到 Source. 打印请求被接受时, 打印机状态改变, 触发属性状态改变. 向所有 Replica报告此状态.
Nodes
QtRO needs a helper class to make this work: the QRemoteObjectNode. QRemoteObjectNodes (let's shorten the name to Node for now) are what enables the passing of information between processes. All of the QtRO functionality is enabled by a small number of distinct packets passing the necessary data between nodes.
Each process that participates in QtRO's IPC will instantiate a Node-based type (QRemoteObjectNode, QRemoteObjectHost, or QRemoteObjectRegistryHost). The latter types of Nodes provide additional functionality. Both QRemoteObjectHost and QRemoteObjectRegistryHost support the enableRemoting() (and the corresponding disableRemoting()) methods, which are the key methods to expose Source objects to the network. In order to use the Registry functionality, there should be one QRemoteObjectRegistryHost on the network. All other nodes can then pass the RegistryHost's URL to the Node's registryAddress constructor parameter, or pass the URL to the setRegistryUrl() method.
QtRO works as a peer-to-peer network. That is, in order to acquire() a valid Replica, the Replica node needs a connection to the node that hosts its Source. A host node is a node that allows other nodes to connect to it, which is accomplished by giving hosts unique addresses (the address is provided to the QRemoteObjectHost constructor or set by the setHostUrl method). The node that a Replica is requested from must establish the connection to the host node in order to initialize the Replica and keep it up to date.
Connecting Nodes using QtRO URLs
Host Nodes use custom URLs to simplify connections. While the list will likely be extended, QtRO currently supports two types of connections. A "tcp" connection (using the standard tcp/ip protocol) supports connections between devices as well as between processes on the same device. The 2nd option is a "local" connection - which can have less overhead, depending on the underlying OS features - but does not support connectivity between devices.
When using a local connection, a unique name must be used. For tcp connections, a unique address and port number combination much be used.
There is currently no zeroconf facility included in QtRO. All processes or devices must therefore know beforehand how to connect to each other. A QRemoteObjectRegistry (see below) can be used to simplify the connection process for a network with multiple Host Nodes.
Connection types are summarized in the following table.
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) |
Nodes have a couple of enableRemoting() methods that are used to share objects on the network (this will produce an error if the Node is not a Host Node however). Other processes/devices that want to interact with a shared object use one of the node's acquire() methods to instantiate a replica.
Source objects
A Remote Object Source is the QObject that is responsible for the implementation of the exposed API.
At a high level, you have a choice of using a QObject type directly as a source or defining the desired API in a .rep template for use with the repc compiler.
If you already have a fully defined QObject, it can become a Source simply by passing it to QRemoteObjectHostBase::enableRemoting(). This lets other processes/devices create a Replica of the object to interact with (see Remote Object Interaction). You can then instantiate QRemoteObjectDynamicReplicas of your object, or use the QOBJECT_REPLICA macro in your project file, which will use repc to create a header file describing the Replica for use in that process/on that device (and provides compile-time checks).
Letting repc generate a Source header file for your project (using the REPC_SOURCE macro) provides three options of implementing the desired API. If your class name was Foo, the options would be the following (and see The rep file format for help in creating a rep file)
- FooSimpleSource inheritance
- FooSource inheritance
- FooSourceAPI usage with your own QObject
There is a <Type>SimpleSource class defined in the header, which provides basic getter/setter methods for each property and implements data members of the correct property type in the header. Here "<Type>" represents the class name from the .rep file, so if your class is of type "MyType" in the .rep file, there will be a MyTypeSimpleSource class declared in the produced header file. This is a fast way to get started using the API. To use this class, you need to inherit from this class and implement any defined slots (which are pure virtual in the generated header file). Whatever logic is needed to manage the exposed properties and define when Signals need to be emitted would be added to the overriding class as well.
If you need to hide the implementation details, you can use the <Type>Source class instead, which is the 2nd class declared in the same resulting header file. This class definition does not provide data members, and makes the getter/setter functions pure virtual as well. You have more flexibility in how you implement the class, although you need to write more code. This defines the API for both the source and replica side from a single .rep template file.
Finally, there is the <Type>SourceAPI class generated in the header. This is a templated class, for use specifically by the templated version of QRemoteObjectHostBase::enableRemoting() function overload, which allows you to use any QObject that supports the desired API as the source. You will get compile-time warnings if the class does not provide the correct API, and using this class allows you to hide or convert properties or signal/slot parameters.
Note: The QObject API is never exposed. For instance, while a Replica will have a destroyed signal, the destroyed signal of the source is not propagated. The Source and each Replica are unique QObjects with their own connections. The API that is exposed is defined by the .rep template used by repc, or in the case of raw QObjects, all API elements defined in the inheritance chain from a specific ancestor. Unless you define Q_CLASSINFO("RemoteObject Type") in an ancestor, the QObject's parent is used. If Q_CLASSINFO("RemoteObject Type") is used, that class's API is the lowest level of API used.
Identifying Sources
Since more than one Source can be shared by a host node, each Source requires a name. All repc generated headers include a way for the node to determine the class name (Q_CLASSINFO for replica/simplesource/source types, or a static name() function for the SourceAPI type). If you pass your own QObject type to QRemoteObjectHostBase::enableRemoting(), the name will be determined using the following logic:
- If the object or any of its ancestors has Q_CLASSINFO of type "RemoteObject Type" defined, the provided name will be used.
- Otherwise, the QObject's objectName (if set) will be used.
- If neither is available, the call to QRemoteObjectHostBase::enableRemoting() will fail, returning False.
Replica objects
A remote object replica is a proxy object that has (approximately) the same API as the Source QObject it is replicating. There are a few additional properties and signals to make it possible to detect when the Replica is initialized or if it loses the connectivity to the Source object. There are a few other differences: a constant property on the source cannot be constant on the replica. The value will not be known at the time the replica is instantiated, it will only be known once the replica is initialized (see Remote Object Interaction).
A compiled replica is a QRemoteObjectReplica based type, where the derived class definition is automatically generated by the repc compiler. Only a header file is generated (and using the REPC_REPLICA macro in your .pro file can make generation part of the build process), but it is a complete type. There is no public constructor, you need to use the QRemoteObjectNode::acquire template function to create the Replica instance.
A QRemoteObjectDynamicReplica can be generated at runtime. To do so, you call the non-templated version of QRemoteObjectNode::acquire(), passing in as an argument the Source name (a QString). Dynamic replicas are a bit more verbose to use from C++, but do not require compilation and can be used easily in QML or (potentially) exposed to scripting languages such as Python. Dynamic replicas do not support initial property values, and do not support introspection until they have been initialized.
An important difference between these two ways of creating replicas is the behavior before the replica is initialized. Since a Dynamic replica only gets a metaObject after initialization, it basically has no API before initialization. No properties, and no Signals to connect slots to. Due to the compile-time creation of the metaObject for compiled replicas, their API is available when the replica is instantiated. You can even provide default values for Properties in the template file, which will be used until the replica is initialized with current values from the Source.
See QRemoteObjectReplica and QRemoteObjectDynamicReplica
Replica Initialization
A host node will share the list of sources it hosts and every other node that connects to it. It will send updates when sources are added or removed from the list. In this way, a connected node will always know what sources it can attach to. Changes to a specific Source are only propagated to nodes that have a replica of that source. This avoids unnecessary network traffic.
When a node acquires a replica for a known source, the replica node sends a request for that source to the host node. Upon receipt of this request, the host will create a reply packet with the current values of all properties of the source. If the requested replica is dynamic, it will include the API definition for the source. The replica node will be included in the list of connections that receive changes to that source from then on.
If a replica is instantiated but the node is not connected to the node that hosts the requested source (or that object lives in a host node process, but sharing/remoting has not been enabled for the QObject), the Replica will still be created, it will just remain uninitialized.
If, at a later time, the replica node gets notified that the requested source is available from a connected node, it will at that point request the source and start the initialization process.
If the connection to a host node is lost, the replica will transition to the invalid state. It will attempt to reconnect and will re-initialize if the connection is restored (this making sure all Properties are current).
The Registry
When you QRemoteObjectNode::acquire() a replica, the node URL is not passed as an argument. This means you do not need to specify the host node, but it does require you to have some other means of connecting to that host. Without the registry, it is necessary to manually call QRemoteObjectNode::connect(), from each node, to every host node that has Source objects it should replicate. This is fine for small or static networks, but does not scale.
The registry provides a simpler way to establish these connections. Every node that wants to be part of the registry's network connects to the registry. The registry is itself a specialized source object, and thus is hosted by a node. Connecting to the registry is simply a matter of passing the registry's URL to the QRemoteObjectNode or QRemoteObjectHost constructor, or passing the URL to the setRegistryUrl method.
The registry is tightly integrated with QtRO. Whenever a Source is added or removed, the name/URL is updated in the registry automatically. So once your node is connected to the registry, it is not necessary to connect to any other nodes manually. If you request an object on the network and you aren't connected to the hosting node, the registry will know what URL to connect to and will initiate the connection. Once connected (and the list of available objects is passed along, including the desired Source), the initialization process for the requested Replica will start automatically.
Remote Object Interaction
Source/replica interaction is directional. Property changes and signal emission happen on the source, and are propagated to all replicas. If a property is writable, you can call the setter function on a replica. This will be forwarded to the source, and if a change is made, it will be made on the source and subsequently forwarded to all replicas. To the replica, it is then an asynchronous call, with latency before the change takes effect.
Whereas you can emit a signal on a replica, this may have unexpected results and is discouraged for that reason. It will only trigger slots connected to the replica itself, no slots connected to the source or other replicas. Like property setters, slot invocations on a replica are forwarded to the Source to run.
The behavior above is implemented automatically by QtRO, there is no need to write any replica implementation code. It will be handled automatically at runtime for dynamic replicas, or at compile time for repc generated headers.
Replica Ownership
The acquire methods return a pointer to the replica QObject instantiated by the node. The node has no way of knowing the intended lifetime of the replica, so it is the responsibility of the calling program to delete the replica when it is no longer needed.
You can instantiate multiple copies of the same replica (this may be necessary in QML for instance). All replicas of the same source from a single node will share a private data member which handles the network communication. This means multiple instances of a Replica do not introduce additional network traffic, although there will be some additional processing overhead. Failing to delete replicas will prevent the reference count on this private object to be invalid, and cause unnecessary network communication until the calling process exits. For this reason, it is recommended that QScopedPointer or QSharedPointer be used to help track a replica lifetime.
Remote Object Public Classes
The following classes make up the public interface for Qt Remote Objects:
A class holding information about client backends available on the Qt Remote Objects network | |
A class holding information about server backends available on the Qt Remote Objects network | |
A dynamically instantiated Replica | |
A (Host) Node on a Qt Remote Objects network | |
Base functionality common to Host and RegistryHost classes | |
A node on a Qt Remote Objects network | |
Virtual class provides the methods for setting PROP values of a replica to value they had the last time the replica was used | |
A (Host/Registry) node on a Qt Remote Objects network | |
A class holding information about Source objects available on the Qt Remote Objects network | |
A class interacting with (but not implementing) a Qt API on the Remote Object network |