Qt D-Bus概述
一、概述
D-Bus是一种进程间通信(IPC)和远程过程调用(RPC)机制,最初是为Linux开发的,用一个统一的协议取代现有的和竞争的IPC解决方案。它还被设计为允许系统级进程(如打印机和硬件驱动程序服务)和普通用户进程之间的通信。
它使用快速的二进制消息传递协议,由于其低延迟和低开销,适合于同机通信。它的规范目前由 freedesktop.org 项目定义,所有各方都可以使用。
通信通常通过称为“总线”(因此得名)的中央服务器应用程序进行,但也可以直接进行应用程序到应用程序的通信。当在总线上通信时,应用程序可以查询哪些其他应用程序和服务可用,以及按需激活其中一个。
二、总线
D-Bus总线用于需要多对多通信的情况。为了实现这一点,在任何应用程序可以连接到总线之前启动一个中央服务器:该服务器负责跟踪连接的应用程序,并负责将消息从源路由到目的地。
此外,D-Bus还定义了两个众所周知的总线,称为系统总线和会话总线。这些总线的特殊之处在于它们具有良好定义的语义:一些服务被定义为可以在这些总线中的一个或两个中找到。
例如,希望查询附加到计算机上的硬件设备列表的应用程序可能会与系统总线上可用的服务通信,而提供打开用户web浏览器的服务可能会在会话总线上找到。
在系统总线上,还可以找到关于允许每个应用程序提供哪些服务的限制。因此,可以合理地确定,如果存在某个服务,它是由受信任的应用程序提供的。
三、相关概念
1. 消息
在底层,应用程序通过D-Bus相互发送消息进行通信。消息用于转发远程过程调用以及与之相关的应答和错误。当在总线上使用时,消息有目的地,这意味着它们只被路由到感兴趣的各方,避免了由于“蜂群”或广播造成的拥塞。
然而,有一种特殊类型的消息称为“信号消息”(基于Qt的信号和槽机制的概念),它没有预定义的目的地。由于其目的是在一对多上下文中使用,因此信号消息被设计为通过“选择加入”机制工作。
Qt D-Bus模块将消息的低级概念完全封装为Qt开发人员熟悉的更简单的面向对象方法。在大多数情况下,开发人员无需担心发送或接收消息。
2. 服务名称
当通过总线进行通信时,应用程序获得所谓的 “服务名称” :这是该应用程序选择被同一总线上的其他应用程序所知道的方式。通信总得知道是谁收谁发嘛。服务名称由D-Bus总线守护进程代理,用于将消息从一个应用程序路由到另一个应用程序。与服务名称类似的概念是IP地址和主机名:一台计算机通常有一个IP地址,并可能有一个或多个与之关联的主机名,这取决于它向网络提供的服务。
另一方面,如果不使用总线,也不会使用服务名称。如果我们再次将其与计算机网络进行比较,这将等同于点对点网络:由于对等点是已知的,因此不需要使用主机名来查找它或它的IP地址。
D-Bus服务名称的格式实际上与主机名非常相似:它是由点分隔的字母和数字序列。常见的做法甚至是根据定义该服务的组织的域名来命名服务名称。
例如,D-Bus服务是由freedesktop.org定义的,可以在总线上的服务名称下找到它:
org.freedesktop.DBus
就像 deepin中的部分代码用到的这种服务名称,也是用的域名表示方式,java中的很多包名也用的域名表示
QDBusConnection sessionBus = QDBusConnection::sessionBus();
if (!sessionBus.interface()->isServiceRegistered("com.deepin.daemon.InputDevices")) {
qDebug() << "DBusError" << "com.deepin.daemon.InputDevices";
return;
}
// 监控全局鼠标信号。
sessionBus.connect("com.deepin.daemon.InputDevices",
"/com/deepin/api/XEventMonitor", "com.deepin.api.XEventMonitor", "ButtonPress",
this, SLOT(ButtonPressEvent(int, int, int, QString)));
3. 对象的路径
与网络主机一样,应用程序通过导出对象向其他应用程序提供特定的服务。这些对象是分层组织的,很像从QObject派生的类所拥有的父子关系。然而,一个不同之处在于存在 “根对象” 的概念,所有对象都有作为最终父对象的根对象。
如果我们继续对Web服务进行类比,对象路径等同于URL的路径部分:
与它们类似,D-Bus中的对象路径的形式类似于文件系统中的路径名称:它们是由斜杠分隔的标签,每个标签由字母、数字和下划线(“_”)组成。它们必须总是以斜杠开始,不能以斜杠结束。
4. 接口
接口类似于c++抽象类和Java的interface关键字,并声明了在调用者和被调用者之间建立的“契约”。也就是说,它们建立了可用的方法、信号和属性的名称,以及在建立通信时期望的任何一方的行为。
Qt在它的插件系统中使用了一个非常相似的机制:c++中的基类通过Q_DECLARE_INTERFACE()宏与一个唯一标识符相关联。
实际上,D-Bus接口名称的命名方式类似于Qt插件系统所建议的:一个标识符通常由定义该接口的实体的域名构造而成。
5. 备忘单
为方便记忆命名格式及其用途,可使用下表:
d - bus概念 | 类比 | 名字的格式 |
---|---|---|
Service name 服务名称 | 网络主机名 | 点分隔(“看起来像主机名”) |
Object path 对象路径 | URL路径组件 | 斜杠分隔(“看起来像一条路径”) |
Interface 接口 | 插件标识符 | 以圆点分隔 |
四、调试
在开发使用D-Bus的应用程序时,有时能够查看每个应用程序通过总线发送和接收的消息的相关信息是很有用的。
通过在运行每个应用程序之前设置QDBUS_DEBUG环境变量,可以在每个应用程序的基础上启用该特性。例如,在D-Bus遥控汽车示例中,我们可以通过以下方式运行控制器和汽车,使能仅对汽车进行调试:
examples/dbus/remotecontrolledcar/controller/controller &
QDBUS_DEBUG=1 examples/dbus/remotecontrolledcar/car/car &
有关消息的信息将写入启动应用程序的控制台中。
五、使用Qt D-Bus 适配器
适配器是附加到任何qobject派生类的特殊类,并使用D-Bus提供与外部世界的接口。适配器旨在成为轻量级类,其主要目的是中继对真实对象的调用和来自真实对象的调用,可能会验证或转换来自外部世界的输入,从而保护真实对象。
与多重继承不同,适配器可以随时添加到任何对象(但不能删除),这在导出现有类时提供了更大的灵活性。适配器的另一个优点是在不同接口中的同名方法中提供类似但不相同的功能,在向对象添加新版本的标准接口时,这种情况非常常见。
为了使用适配器,必须创建继承QDBusAbstractAdaptor的类。由于这是一个标准的qobject派生类,Q_OBJECT宏必须出现在声明中,源文件必须用moc工具处理。类还必须包含一个带有“D-Bus Interface”名称的Q_CLASSINFO条目,声明它要导出的接口。每个类只支持一个条目。
类中的任何公共槽都可以通过MethodCall类型的消息通过总线访问。(更多信息请参见声明D-Bus适配器的槽函数)。课堂上的信号将通过D-Bus自动传递。然而,并非所有类型都允许信号或槽的参数列表:参见Qt D-Bus类型系统了解更多信息。
此外,使用Q_PROPERTY声明的任何属性都将通过D-Bus上的Properties接口自动公开。由于QObject属性系统不允许非可读属性,因此不可能使用适配器声明只写属性。
1. 在 D-Bus 适配器中声明槽函数
D-Bus适配器中的槽函数就像普通的公共槽函数一样被声明,但是它们的参数必须遵循一定的规则(参见Qt D-Bus类型系统了解更多信息)。如果槽函数的参数不符合这些规则,或者槽函数不是公共的,那么D-Bus将无法访问这些槽函数。
槽可以有一个const QDBusMessage &类型的参数,它必须出现在输入参数列表的末尾,在任何输出参数之前。如果存在此参数,将使用正在处理的当前消息的副本初始化,这允许被调用方获取有关调用方的信息,例如其连接名称。
槽有三种类型:
- 异步槽
- 只输入槽
- 输入和输出槽
1. 异步槽
异步槽通常不向调用者返回任何应答。因此,它们不能接受任何输出参数。在大多数情况下,到运行槽的第一行时,调用方函数已经恢复工作。
然而,槽函数不能依赖于这种行为。调度和消息调度问题可能会改变槽函数运行的顺序。打算与调用者同步的代码应该提供自己的同步方法。
异步槽由方法签名中的关键字Q_NOREPLY标记,位于void返回类型和槽名之前。D-Bus Complex乒乓示例中的quit()槽函数就是这样的一个例子。
2. 只输入槽
仅输入槽是普通槽,它接受按值或常量引用传递的参数。然而,与异步槽不同,调用方通常在恢复操作之前等待被调用方完成。因此,非异步槽函数不应该阻塞,或者应该在文档中说明它们可能会阻塞。
只有输入的槽在它们的签名中没有特殊的标记,除了它们只接受通过值或常量引用传递的参数。槽可以选择将QDBusMessage参数作为最后一个参数,该参数可用于执行对方法调用消息的附加分析。
3. 输入输出槽
与仅输入槽一样,输入-输出槽是调用者等待回复的槽。但是,与只输入的应答不同,此应答将包含数据。输出数据的槽可能包含非常量引用,也可能返回一个值。但是,输出参数必须全部出现在参数列表的末尾,并且输入参数不能交错。可选地,QDBusMessage参数可能出现在输入和输出参数之间。
4. 自动回复
方法应答是由Qt D-Bus实现与输出参数(如果有的话)的内容一起自动生成的。槽函数不需要考虑构造适当的QDBusMessage对象并通过连接发送它们。
然而,这样做的可能性仍然存在。如果槽发现它需要发送一个特殊的回复,甚至是一个错误,它可以在QDBusMessage参数上使用QDBusMessage::createReply()或QDBusMessage::createErrorReply(),并使用QDBusConnection::send()发送它。如果槽函数这样做了,Qt D-Bus实现将不会生成任何应答。
警告:当调用者调用方法并等待应答时,它将只等待有限的时间。打算花费很长时间来完成的槽函数应该在文档中明确这一事实,以便调用者适当地设置更高的超时。
5. 延迟回复
在某些情况下,被调用的槽可能无法立即处理请求。当请求涉及可能阻塞的I/O或网络操作时,这种情况经常发生。
如果是这种情况,slot应该将控制返回给应用程序的主循环,以避免冻结用户界面,并在以后恢复该进程。要实现这一点,它应该利用输入参数列表末尾的额外QDBusMessage参数,并请求延迟回复。
为此,我们编写一个将请求数据存储在持久结构中的槽,并使用QDBusMessage::setDelayedReply(true)向调用者指示稍后将发送响应。
struct RequestData
{
QString request;
QString processedData;
QDBusMessage reply;
};
QString processRequest(const QString &request, const QDBusMessage &message)
{
RequestData *data = new RequestData;
data->request = request;
message.setDelayedReply(true);
data->reply = message.createReply();
QDBusConnection::sessionBus().send(data->reply);
appendRequest(data);
return QString();
}
需要使用QDBusConnection::sessionBus().send(data->reply)来显式地通知调用者响应将被延迟。在这种情况下,返回值并不重要;我们返回一个任意值来满足编译器。
当请求被处理并且应答可用时,应该使用获得的QDBusMessage对象发送应答。在我们的示例中,回复代码可能如下所示:
void sendReply(RequestData *data)
{
// data->processedData has been initialized with the request's reply
QDBusMessage &reply = &data->reply;
// send the reply over D-Bus:
reply << data->processedData;
QDBusConnection::sessionBus().send(reply);
// dispose of the transaction data
delete data;
}
在这个例子中可以看到,当延迟应答到位时,从槽返回的值将被Qt D-Bus忽略。它们仅用于在与远程应用程序通信适配器描述时确定槽函数的签名,或者在槽函数中的代码决定不使用延迟应答的情况下。
延迟的回复本身是通过调用QDBusMessage::reply()从Qt D-Bus请求的。然后,被调用的代码负责最终向调用方发送应答。
警告-当调用者调用方法并等待应答时,它将只等待有限的时间。打算花费很长时间来完成的槽函数应该在文档中明确这一事实,以便调用者适当地设置更高的超时。