开源应用程序架构(卷 1)
Telepathy

Danielle Madeley

Telepathy1 是一个用于实时通信的模块化框架,它处理语音、视频、文本、文件传输等。Telepathy 的独特之处不在于它抽象了各种即时消息协议的细节,而在于它提供了通信即服务的理念,就像打印是服务一样,可供多个应用程序同时使用。为了实现这一点,Telepathy 大量使用了 D-Bus 消息总线和模块化设计。

通信即服务非常有用,因为它允许我们将通信从单个应用程序中分离出来。这使许多有趣的用例成为可能:能够在您的电子邮件应用程序中看到联系人的状态;开始与她交流;直接从您的文件浏览器启动向联系人的文件传输;或在应用程序内提供联系人之间的协作,在 Telepathy 中称为Tubes

Telepathy 由 Robert McQueen 于 2005 年创建,从那时起,它一直由包括 McQueen 共同创办的公司 Collabora 在内的几家公司和个人贡献者开发和维护。

D-Bus 消息总线

D-Bus 是一种用于进程间通信的异步消息总线,是大多数 GNU/Linux 系统(包括 GNOME 和 KDE 桌面环境)的支柱。D-Bus 主要是一种共享总线架构:应用程序连接到总线(由套接字地址标识)并可以向总线上的另一个应用程序发送目标消息,也可以向所有总线成员广播信号。总线上的应用程序具有类似于 IP 地址的总线地址,并且可以声明一些众所周知的名称,例如 DNS 名称,例如 org.freedesktop.Telepathy.AccountManager。所有进程都通过 D-Bus 守护进程进行通信,该守护进程处理消息传递和名称注册。

从用户的角度来看,每个系统上都有两条总线可用。系统总线允许用户与系统范围内的组件(打印机、蓝牙、硬件管理等)通信,并且由系统上的所有用户共享。会话总线对该用户来说是唯一的,即每个登录用户都有一个会话总线,用于用户的应用程序相互通信。当需要通过总线传输大量数据时,应用程序也可以创建自己的私有总线,或创建没有 dbus-daemon 的对等、非仲裁总线。

几个库实现了 D-Bus 协议,可以与 D-Bus 守护进程通信,包括 libdbus、GDBus、QtDBus 和 python-dbus。这些库负责发送和接收 D-Bus 消息,将语言类型系统中的类型编组到 D-Bus 的类型格式中,并在总线上发布对象。通常,这些库还提供方便的 API 用于列出连接的应用程序和可激活的应用程序,以及请求总线上众所周知的名称。在 D-Bus 级别,所有这些都通过对 dbus-daemon 本身发布的对象进行方法调用来完成。

有关 D-Bus 的更多信息,请参见 http://www.freedesktop.org/wiki/Software/dbus

20.1. Telepathy 框架的组件

Telepathy 是模块化的,每个模块通过 D-Bus 消息总线与其他模块通信。通常通过用户的会话总线。这种通信在 Telepathy 规范中详细说明了2。Telepathy 框架的组件如 图 20.1 所示

在 Telepathy 的当前实现中,帐户管理器和通道调度器都由一个称为 Mission Control 的进程提供。

[Example Telepathy Components]

图 20.1:示例 Telepathy 组件

这种模块化设计基于 Doug McIlroy 的哲学,“编写只做一件事并做好这件事的程序”,具有几个重要的优势

连接管理器管理多个连接,其中每个连接代表与通信服务的逻辑连接。每个配置的帐户都有一个连接。连接将包含多个通道。通道是执行通信的机制。通道可以是 IM 对话、语音或视频通话、文件传输或其他一些有状态操作。连接和通道将在 第 20.3 节 中详细讨论。

20.2. Telepathy 如何使用 D-Bus

Telepathy 组件通过 D-Bus 消息总线进行通信,该总线通常是用户的会话总线。D-Bus 提供许多 IPC 系统共有的功能:每个服务发布对象,这些对象具有严格命名空间的对象路径,例如 /org/freedesktop/Telepathy/AccountManager3。每个对象都实现了一些接口。这些接口也是严格命名空间的,形式类似于 org.freedesktop.DBus.PropertiesofdT.Connection。每个接口都提供您可以调用、监听或请求的方法、信号和属性。

[Conceptual Representation of Objects Published by a D-Bus Service]

图 20.2:D-Bus 服务发布的对象的概念表示

发布 D-Bus 对象

发布 D-Bus 对象完全由正在使用的 D-Bus 库处理。实际上,它将 D-Bus 对象路径映射到实现这些接口的软件对象。服务发布的对象的路径由可选的 org.freedesktop.DBus.Introspectable 接口公开。

当服务接收到具有给定目标路径(例如,/ofdT/AccountManager)的传入方法调用时,D-Bus 库负责定位提供该 D-Bus 对象的软件对象,然后对该对象进行适当的方法调用。

Telepathy 提供的接口、方法、信号和属性在基于 XML 的 D-Bus IDL 中详细说明,该 IDL 已扩展以包含更多信息。该规范可以解析以生成文档和语言绑定。

Telepathy 服务在总线上发布了许多对象。Mission Control 发布帐户管理器和通道调度器的对象,以便可以访问其服务。客户端发布一个客户端对象,通道调度器可以访问该对象。最后,连接管理器发布了许多对象:一个服务对象,帐户管理器可以使用它来请求新的连接,一个用于每个打开连接的对象,以及一个用于每个打开通道的对象。

虽然 D-Bus 对象没有类型(只有接口),但 Telepathy 以多种方式模拟类型。对象的路径告诉我们该对象是连接、通道、客户端等等,但通常您在请求它的代理时已经知道这一点。每个对象都实现了该类型的基接口,例如,ofdT.ConnectionofdT.Channel。对于通道,这有点像抽象基类。通道对象然后有一个定义其通道类型的具体类。同样,这也由 D-Bus 接口表示。可以通过读取通道接口上的 ChannelType 属性来了解通道类型。

最后,每个对象都实现了一些可选的接口(不出所料,也作为 D-Bus 接口表示),这些接口取决于协议和连接管理器的功能。给定对象上可用的接口可以通过对象基类上的 Interfaces 属性获得。

对于类型为 ofdT.Connection 的连接对象,可选接口的名称类似于 ofdT.Connection.Interface.Avatars(如果协议有头像的概念)、odfT.Connection.Interface.ContactList(如果协议提供联系人列表——并非所有协议都提供)和 odfT.Connection.Interface.Location(如果协议提供地理位置信息)。对于类型为 ofdT.Channel 的通道对象,具体类的接口名称采用 ofdT.Channel.Type.TextofdT.Channel.Type.CallofdT.Channel.Type.FileTransfer 的形式。与连接一样,可选接口的名称类似于 odfT.Channel.Interface.Messages(如果此通道可以发送和接收文本消息)和 odfT.Channel.Interface.Group(如果此通道是到包含多个联系人的组,例如多用户聊天)。因此,例如,文本通道至少实现了 ofdT.ChannelofdT.Channel.Type.TextChannel.Interface.Messages 接口。如果是多用户聊天,它也会实现 odfT.Channel.Interface.Group

为什么是 Interfaces 属性而不是 D-Bus 自省?

您可能想知道为什么每个基类都实现了一个Interfaces 属性,而不是依赖于 D-Bus 的内省功能来告诉我们有哪些接口可用。答案是,不同的通道和连接对象可能会根据通道或连接的功能,彼此提供不同的接口,但大多数 D-Bus 内省的实现假设同一个对象类的所有对象都具有相同的接口。例如,在telepathy-glib 中,由 D-Bus 内省列出的 D-Bus 接口是从类实现的对象接口中获取的,这些接口是在编译时静态定义的。我们通过让 D-Bus 内省提供可能存在于对象上的所有接口的数据来解决这个问题,并使用Interfaces 属性来指示哪些接口实际上存在。

虽然 D-Bus 本身没有提供任何健全性检查,以确保连接对象只具有与连接相关的接口等等(因为 D-Bus 没有类型概念,只有任意命名的接口),但我们可以在 Telepathy 语言绑定中使用 Telepathy 规范中包含的信息来提供健全性检查。

为什么以及如何扩展规范 IDL

现有的 D-Bus 规范 IDL 定义了方法、属性和信号的名称、参数、访问限制和 D-Bus 类型签名。它不提供对文档、绑定提示或命名类型的支持。

为了解决这些限制,添加了一个新的 XML 命名空间来提供所需的信息。此命名空间被设计为通用的,以便其他 D-Bus API 可以使用它。添加了新的元素,包括内联文档、理由、介绍和弃用版本,以及方法的潜在异常。

D-Bus 类型签名是通过总线序列化内容的低级类型表示法。D-Bus 类型签名可能看起来像(ii)(这是一个包含两个 int32 的结构),也可能更复杂。例如,a{sa(usuu)} 是一个从字符串到包含 uint32、字符串、uint32、uint32 的结构数组的映射 (图 20.3)。这些类型虽然描述了数据格式,但对类型中包含的信息没有提供语义意义。

为了为程序员提供语义清晰度并加强语言绑定的类型化,添加了新的元素来命名简单类型、结构体、映射、枚举和标志,并提供它们的类型签名以及文档。还添加了元素,以便为 D-Bus 对象模拟对象继承。

[D-Bus Types (ii) and a{sa(usuu)}]

图 20.3:D-Bus 类型 (ii) 和 a{sa(usuu)}

20.2.1. 句柄

句柄在 Telepathy 中用于表示标识符(例如,联系人姓名和房间名称)。它们是由连接管理器分配的无符号整数值,使得元组(连接、句柄类型、句柄)唯一地引用给定的联系人或房间。

由于不同的通信协议以不同的方式规范化标识符(例如,大小写敏感性、资源),因此句柄为客户端提供了一种方法来确定两个标识符是否相同。它们可以请求两个不同标识符的句柄,如果句柄号码匹配,那么这些标识符引用相同的联系人或房间。

每个协议的标识符规范化规则都不同,因此客户端比较标识符字符串来比较标识符是一个错误。例如,escher@tuxedo.cat/bedescher@tuxedo.cat/litterbox 是 XMPP 协议中同一个联系人 (escher@tuxedo.cat) 的两个实例,因此具有相同的句柄。客户端可以按标识符或句柄请求通道,但他们应该只使用句柄进行比较。

20.2.2. 发现 Telepathy 服务

某些服务(例如,帐户管理器和通道调度程序)始终存在,具有在 Telepathy 规范中定义的知名名称。但是,连接管理器和客户端的名称并不为人所知,必须发现它们。

Telepathy 中没有负责注册正在运行的连接管理器和客户端的服务。相反,感兴趣的各方会监听 D-Bus 上的新的服务公告。每当 D-Bus 总线守护进程在总线上出现新的命名 D-Bus 服务时,它都会发出信号。客户端和连接管理器的名称以规范中定义的已知前缀开头,新的名称可以与这些前缀匹配。

这种设计的好处在于它完全是无状态的。当 Telepathy 组件启动时,它可以询问总线守护进程(它有一个基于其打开的连接的规范列表)哪些服务当前正在运行。例如,如果帐户管理器崩溃,它可以查看哪些连接正在运行,并将这些连接与其帐户对象重新关联。

连接也是服务

除了连接管理器本身之外,连接也作为 D-Bus 服务进行广告。理论上,这允许连接管理器将每个连接作为一个单独的进程分离出来,但到目前为止,还没有实现这样的连接管理器。实际上,它允许通过查询 D-Bus 总线守护进程来发现所有以ofdT.Connection 开头的服务的运行连接。

通道调度程序也使用此方法来发现 Telepathy 客户端。这些客户端以ofdT.Client 开头,例如,ofdT.Client.Logger

20.2.3. 减少 D-Bus 流量

Telepathy 规范的原始版本在总线上产生了大量 D-Bus 流量,形式为请求总线上许多消费者所需的信息的方法调用。Telepathy 的后续版本通过一些优化解决了这个问题。

单个方法调用被 D-Bus 属性替换。原始规范包括用于对象属性的单独方法调用:GetInterfacesGetChannelType 等。请求对象的全部属性需要进行多次方法调用,每次调用都有自己的调用开销。通过使用 D-Bus 属性,可以使用标准的GetAll 方法一次性请求所有内容。

此外,通道上的许多属性在通道的整个生命周期内都是不可变的。这些包括通道类型、接口、连接对象以及请求者。例如,对于文件传输通道,它还包括文件大小和内容类型。

添加了一个新的信号来预示通道的创建(传入和响应传出请求),其中包含一个不可变属性的哈希表。这可以直接传递给通道代理构造函数(参见 第 20.4 节),这可以节省有兴趣的客户端单独请求此信息的麻烦。

用户头像作为字节数组跨总线传输。虽然 Telepathy 已经使用令牌来引用头像,允许客户端知道何时需要新的头像并保存下载不需要的头像,但每个客户端都必须通过RequestAvatar 方法单独请求头像,该方法以其回复的形式返回头像。因此,当连接管理器发出信号表明联系人已更新其头像时,将发出多个单独的头像请求,要求头像通过消息总线传输多次。

这通过添加一种不返回值量(它不返回值)的新方法来解决。相反,它将头像放在一个请求队列中。从网络获取头像将导致信号AvatarRetrieved 发出,所有感兴趣的客户端都可以监听该信号。这意味着头像数据只需要通过总线传输一次,并将对所有感兴趣的客户端可用。一旦客户端的请求在队列中,所有进一步的客户端请求将被忽略,直到AvatarRetrieved 发出。

每当需要加载大量联系人(即加载联系人列表)时,需要请求大量信息:它们的别名、头像、功能和群组成员资格,以及可能的位置、地址和电话号码。以前在 Telepathy 中,这需要对每个信息组进行一次方法调用(大多数 API 调用,例如GetAliases 已经接受联系人列表),导致六次或更多次方法调用。

为了解决这个问题,引入了Contacts 接口。它允许通过单个方法调用返回来自多个接口的信息。Telepathy 规范已扩展为包括联系人属性:由GetContactAttributes 方法返回的命名空间属性,该方法隐藏用于检索联系人信息的方法调用。客户端使用它感兴趣的联系人列表和接口调用GetContactAttributes,并获得从联系人到联系人属性到值的映射的映射。

一些代码将使这一点更加清楚。请求看起来像这样

connection[CONNECTION_INTERFACE_CONTACTS].GetContactAttributes(
  [ 1, 2, 3 ], # contact handles
  [ "ofdT.Connection.Interface.Aliasing",
    "ofdT.Connection.Interface.Avatars",
    "ofdT.Connection.Interface.ContactGroups",
    "ofdT.Connection.Interface.Location"
  ],
  False # don't hold a reference to these contacts
)

回复可能看起来像这样

{ 1: { 'ofdT.Connection.Interface.Aliasing/alias': 'Harvey Cat',
       'ofdT.Connection.Interface.Avatars/token': hex string,
       'ofdT.Connection.Interface.Location/location': location,
       'ofdT.Connection.Interface.ContactGroups/groups': [ 'Squid House' ],
       'ofdT.Connection/contact-id': 'harvey@nom.cat'
     },
  2: { 'ofdT.Connection.Interface.Aliasing/alias': 'Escher Cat',
       'ofdT.Connection.Interface.Avatars/token': hex string,
       'ofdT.Connection.Interface.Location/location': location,
       'ofdT.Connection.Interface.ContactGroups/groups': [],
       'ofdT.Connection/contact-id': 'escher@tuxedo.cat'
     },
  3: { 'ofdT.Connection.Interface.Aliasing/alias': 'Cami Cat',
        ⋮    ⋮    ⋮
     }
}

20.3. 连接、通道和客户端

20.3.1. 连接

连接由连接管理器创建,以建立与单个协议/帐户的连接。例如,连接到 XMPP 帐户escher@tuxedo.catcami@egg.cat 将导致两个连接,每个连接由一个 D-Bus 对象表示。连接通常由帐户管理器为当前启用的帐户设置。

连接为管理和监控连接状态以及请求通道提供了一些强制功能。然后,它还可以根据协议的功能提供一些可选功能。这些作为可选的 D-Bus 接口提供(如上一节所述),并由连接的Interfaces 属性列出。

通常,连接由帐户管理器管理,使用相应帐户的属性创建。帐户管理器还将用户的当前状态与每个帐户同步到其相应的连接,并且可以被要求提供给定帐户的连接路径。

20.3.2. 通道

通道是进行通信的机制。通道通常是即时聊天对话、语音或视频通话或文件传输,但通道也用于提供与服务器本身的某些有状态通信(例如,搜索聊天室或联系人)。每个通道都由一个 D-Bus 对象表示。

通道通常位于两个或多个用户之间,其中一个是您自己。它们通常具有目标标识符,对于一对一通信,该标识符是另一个联系人;对于多用户通信(例如,聊天室),则为房间标识符。多用户通道公开Group 接口,允许您跟踪当前在通道中的联系人。

通道属于连接,并从连接管理器请求,通常通过通道调度程序;或者它们是由连接响应网络事件(例如,传入聊天)创建的,并传递给通道调度程序以进行调度。

通道类型由通道的ChannelType 属性定义。此通道类型所需的核心功能、方法、属性和信号(例如,发送和接收文本消息)在相应的Channel.Type D-Bus 接口中定义,例如,Channel.Type.Text。某些通道类型可能会实现可选的附加功能(例如,加密),这些功能显示为通道的Interfaces 属性列出的附加接口。一个将用户连接到多用户聊天室的示例文本通道可能具有 表 20.1 中显示的接口。

属性 用途
odfT.Channel 所有渠道的共同功能
odfT.Channel.Type.Text 频道类型,包括文本频道共有的功能
odfT.Channel.Interface.Messages 富文本消息
odfT.Channel.Interface.Group 在该频道中列出、跟踪、邀请和批准成员
odfT.Channel.Interface.Room 读取和设置属性,例如聊天室的主题

表 20.1:文本频道示例

联系人列表频道:错误

在 Telepathy 规范的早期版本中,联系人列表被视为一种类型的频道。有几个服务器定义的联系人列表(已订阅的用户、发布到用户、被阻止的用户),可以从每个连接请求这些列表。然后使用Group 接口发现列表中的成员,就像多人聊天一样。

最初,这将允许仅在检索到联系人列表后才创建频道,这在某些协议上需要时间。客户端可以随时请求频道,并在准备好后进行传送,但对于联系人众多的用户而言,这意味着请求有时会超时。确定客户端的订阅/发布/阻止状态需要检查三个频道。

联系人组(例如,朋友)也被公开为频道,每个组一个频道。这对客户端开发人员来说非常难用。诸如获取联系人所属的组列表之类的操作需要在客户端中编写大量代码。此外,由于信息只能通过频道获得,因此诸如联系人的组或订阅状态之类的属性无法通过 Contacts 接口发布。

两种频道类型都已由连接本身的接口取代,这些接口以对客户端作者更有用的方式公开联系人列表信息,包括联系人的订阅状态(枚举)、联系人所属的组以及组中的联系人。信号指示何时准备好了联系人列表。

20.3.3. 请求频道、频道属性和调度

使用您希望目标频道拥有的属性映射来请求频道。通常,频道请求将包括频道类型、目标句柄类型(联系人或房间)和目标。但是,频道请求还可以包括诸如文件传输的文件名和文件大小、是否最初包含呼叫的音频和视频、将哪些现有频道合并到会议呼叫中,或者要在哪个联系人服务器上进行联系人搜索之类的属性。

频道请求中的属性是 Telepathy 规范接口定义的属性,例如ChannelType 属性(表 20.2)。它们与它们所属的接口的命名空间限定。可以包含在频道请求中的属性在 Telepathy 规范中被标记为可请求

属性
ofdT.Channel.ChannelType odfT.Channel.Type.Text
ofdT.Channel.TargetHandleType Handle_Type_Contact (1)
ofdT.Channel.TargetID escher@tuxedo.cat

表 20.2:频道请求示例

表 20.3中的更复杂示例请求文件传输频道。请注意,请求的属性是如何通过它们所属的接口进行限定的。(为了简洁,并非所有必需属性都显示出来。)

属性
ofdT.Channel.ChannelType odfT.Channel.Type.FileTransfer
ofdT.Channel.TargetHandleType Handle_Type_Contact (1)
ofdT.Channel.TargetID escher@tuxedo.cat
odfT.Channel.Type.FileTransfer.Filename meow.jpg
odfT.Channel.Type.FileTransfer.ContentType image/jpeg

表 20.3:文件传输频道请求

频道可以创建确保。确保频道意味着仅在它不存在的情况下创建它。请求创建频道将导致创建一个全新的独立频道,或者如果多个此类频道的副本不能共存,则会生成错误。通常您希望确保文本频道和呼叫(即,您只需要与某人打开一个对话,事实上,许多协议不支持与同一联系人进行多个单独的对话),并希望创建文件传输和有状态频道。

新创建的频道(请求的或其他)由连接发出的信号宣布。此信号包含一个频道不可变属性的映射。这些是保证在频道整个生命周期中不会改变的属性。被认为不可变的属性在 Telepathy 规范中被标记为不可变,但通常包括频道的类型、目标句柄类型、目标、发起者(创建频道的人)和接口。诸如频道状态之类的属性显然不包括在内。

老式频道请求

最初,频道仅通过类型、句柄类型和目标句柄进行请求。这不够灵活,因为并非所有频道都有目标(例如,联系人搜索频道),而某些频道需要在初始频道请求中包含其他信息(例如,文件传输、请求语音邮件和用于发送短信的频道)。

还发现,在请求频道时,可能希望获得两种不同的行为(要么创建一个保证唯一的频道,要么仅仅确保一个频道存在),而直到现在,连接一直负责决定将发生哪种行为。因此,旧方法被更新、更灵活、更明确的方法所取代。

当您创建或确保频道时返回频道的不可变属性,这使得为频道创建代理对象的速度快得多。这是我们现在不必请求的信息。 表 20.4中的映射显示了我们在请求文本频道时可能包含的不可变属性(即,使用表 20.3中的频道请求)。出于简洁起见,已排除某些属性(包括TargetHandleInitiatorHandle)。

属性
ofdT.Channel.ChannelType Channel.Type.Text
ofdT.Channel.Interfaces {[} Channel.Interface.Messages, Channel.Interface.Destroyable, Channel.Interface.ChatState {]}
ofdT.Channel.TargetHandleType Handle_Type_Contact (1)
ofdT.Channel.TargetID escher@tuxedo.cat
ofdT.Channel.InitiatorID danielle.madeley@collabora.co.uk
ofdT.Channel.Requested True
ofdT.Channel.Interface.Messages.SupportedContentTypes {[} text/html, text/plain {]}

表 20.4:新频道返回的不可变属性示例

请求程序通常向频道调度程序发出频道请求,提供请求所针对的帐户、频道请求,以及可选的所需处理程序的名称(如果程序希望自行处理频道,这很有用)。传递帐户名称而不是连接意味着频道调度程序可以要求帐户管理器在线连接帐户(如果需要)。

请求完成后,频道调度程序要么将频道传递给命名处理程序,要么定位合适的处理程序(有关处理程序和其他客户端的讨论,请参见下文)。使所需处理程序的名称可选,使得对于对初始请求之外的通信频道不感兴趣的程序来说,可以请求频道并让最佳程序处理它们(例如,从电子邮件客户端启动文本聊天)。

[Channel Request and Dispatching]

图 20.4:频道请求和调度

请求程序向频道调度程序发出频道请求,频道调度程序反过来将请求转发到相应的连接。连接发出 NewChannels 信号,该信号被频道调度程序接收,然后找到合适的客户端来处理该频道。传入的、未请求的频道以几乎相同的方式调度,从连接发出信号,该信号被频道调度程序接收,但显然没有来自程序的初始请求。

20.3.4. 客户端

客户端处理或观察传入和传出的通信频道。客户端是任何注册到频道调度程序的内容。有三种类型的客户端(尽管单个客户端可以是两种,或所有三种类型,如果开发人员愿意)。

客户端提供 D-Bus 服务,最多具有三个接口:Client.ObserverClient.ApproverClient.Handler。每个接口提供一个方法,频道调度程序可以调用该方法来告知客户端有关要观察、批准或处理的频道的信息。

频道调度程序依次将频道调度到每个客户端组。首先,将频道调度到所有相应的观察者。一旦它们都返回,频道将调度到所有相应的批准者。一旦第一个批准者批准或拒绝了频道,所有其他批准者都会收到通知,最终频道将调度到处理程序。频道调度是分阶段进行的,因为观察者可能需要一些时间才能做好准备,然后再由处理程序开始更改频道。

客户端公开一个频道筛选器属性,这是一个由频道调度程序读取的筛选器列表,以便它知道客户端对哪些类型的频道感兴趣。筛选器必须至少包括客户端感兴趣的频道类型和目标句柄类型(例如,联系人或房间),但它可以包含更多属性。匹配是使用简单的相等比较,针对频道的不可变属性进行的。 表 20.5中的筛选器匹配所有一对一的文本频道。

属性
ofdT.Channel.ChannelType Channel.Type.Text
ofdT.Channel.TargetHandleType Handle_Type_Contact (1)

表 20.5:频道筛选器示例

客户端可以通过 D-Bus 发现,因为它们发布以众所周知名称ofdT.Client 开头的服务(例如ofdT.Client.Empathy.Chat)。它们还可以选择安装一个文件,频道调度程序将读取该文件以指定频道筛选器。这允许频道调度程序在客户端未运行时启动它。以这种方式使客户端可发现,使用户界面选择可配置,并且可以在任何时候更改,而无需替换 Telepathy 的任何其他部分。

全部或无

可以提供一个筛选器来表明您对所有频道感兴趣,但在实践中,这只有作为观察频道的示例才有用。真正的客户端包含特定于频道类型的代码。

空筛选器表明处理程序对任何频道类型都不感兴趣。但是,如果您按名称进行调度,则仍然可以将频道调度到此处理程序。按需创建的临时处理程序,用于处理特定频道,使用此类筛选器。

20.4. 语言绑定角色

由于 Telepathy 是一个 D-Bus API,因此可以使用任何支持 D-Bus 的编程语言驱动它。语言绑定对于 Telepathy 来说不是必需的,但可以使用它们提供一种方便的方式来使用它。

语言绑定可以分为两组:低级绑定,包括从规范生成的代码、常量、方法名称等等;以及高级绑定,这是手写的代码,使程序员更容易使用 Telepathy 完成事情。GLib 和 Qt4 绑定是高级绑定的示例。Python 绑定和原始 libtelepathy C 绑定是低级绑定的示例,尽管 GLib 和 Qt4 绑定包含一个低级绑定。

20.4.1. 异步编程

在语言绑定中,所有通过 D-Bus 进行请求的方法调用都是异步的:发出请求,并在回调中给出回复。这是必需的,因为 D-Bus 本身是异步的。

与大多数网络和用户界面编程一样,D-Bus 需要使用事件循环来调度传入信号和方法返回的回调。D-Bus 与 GTK+ 和 Qt 工具包使用的 GLib 主循环很好地集成在一起。

一些 D-Bus 语言绑定(例如 dbus-glib)提供了一个伪同步 API,其中主循环会阻塞,直到方法回复返回。曾经,这通过 telepathy-glib API 绑定暴露出来。不幸的是,使用伪同步 API 充满了问题,最终从 telepathy-glib 中移除。

为什么伪同步 D-Bus 调用不起作用

dbus-glib 和其他 D-Bus 绑定提供的伪同步接口使用请求和阻塞技术实现。在阻塞期间,只有 D-Bus 套接字被轮询以获取新的 I/O,并且任何不是对请求的响应的 D-Bus 消息都会被排队以供稍后处理。

这会导致一些主要且不可避免的问题
  • 调用者在等待请求被回答时被阻塞。它(以及它的用户界面,如果有的话)将完全无响应。如果请求需要访问网络,这需要时间;如果被调用者已经锁定,调用者将无响应,直到调用超时。
    线程在这里不是解决方案,因为线程只是让你的调用异步的另一种方式。相反,你也可以进行异步调用,其中响应通过现有的事件循环传入。
  • 消息可能会被重新排序。在被观察的回复之前收到的任何消息将被放置在一个队列中,并在回复之后传递给客户端。
    这会导致问题,例如,指示状态更改的信号(即对象已被销毁)现在在对该对象的调用方法失败(即,带有异常 `UnknownMethod`)之后接收。在这种情况下,很难知道向用户显示什么错误。而如果我们先收到信号,我们可以取消挂起的 D-Bus 方法调用,或忽略它们的响应。
  • 两个进程互相进行伪阻塞调用会导致死锁,每个进程都在等待另一个进程响应其查询。这种情况可能发生在既是 D-Bus 服务又是调用其他 D-Bus 服务的进程中(例如,Telepathy 客户端)。通道调度器调用客户端上的方法来调度通道,但客户端也会调用通道调度器上的方法来请求打开新通道(或者同样地,它们会调用帐户管理器,它是同一个进程的一部分)。

第一个 Telepathy 绑定中的方法调用,在 C 中生成,只是使用 typedef 回调函数。你的回调函数只需要实现相同的类型签名。

typedef void (*tp_conn_get_self_handle_reply) (
    DBusGProxy *proxy,
    guint handle,
    GError *error,
    gpointer userdata
);

这个想法很简单,对 C 来说有效,因此在下一代绑定中继续使用。

近年来,人们已经开发出一种方法来使用脚本语言,例如 Javascript 和 Python,以及一个名为 Vala 的类似 C# 的语言,这些语言通过名为 GObject-Introspection 的工具使用基于 GLib/GObject 的 API。不幸的是,将这些类型的回调重新绑定到其他语言非常困难,因此较新的绑定被设计为利用语言和 GLib 提供的异步回调功能。

20.4.2. 对象就绪

在简单的 D-Bus API 中,例如低级 Telepathy 绑定,你可以通过为它创建代理对象来开始对 D-Bus 对象进行方法调用或接收信号。它就像提供一个对象路径和接口名称一样简单,然后开始使用。

但是,在 Telepathy 的高级 API 中,我们希望我们的对象代理知道哪些接口可用,我们希望检索对象类型的通用属性(例如,通道类型、目标、发起者),并且我们希望确定和跟踪对象的 state 或状态(例如,连接状态)。

因此,就绪的概念存在于所有代理对象中。通过对代理对象进行方法调用,你能够异步地检索该对象的 state,并在 state 被检索到且对象可以使用时收到通知。

由于并非所有客户端都实现或感兴趣于给定对象的所有功能,因此对象类型的就绪被分成许多可能的功能。每个对象都实现了一个核心功能,这将准备有关对象的关键信息(即它的 `Interfaces` 属性和基本 state),以及一些可选功能以获取其他 state,这可能包括额外的属性或 state 跟踪。你可以准备各种代理上的其他功能的具体示例包括联系信息、功能、地理位置信息、聊天 state(例如,“Escher 正在输入……”)和用户头像。

例如,连接对象代理具有

程序员请求对象就绪,提供他们感兴趣的功能列表以及在所有这些功能都就绪时要调用的回调。如果所有功能都已经就绪,则可以立即调用回调,否则,一旦检索到所有这些功能的信息,就会调用回调。

20.5. 鲁棒性

Telepathy 的主要优势之一是其鲁棒性。组件是模块化的,因此一个组件的崩溃不应导致整个系统崩溃。以下是一些使 Telepathy 强大的功能

20.6. 扩展 Telepathy:侧车

虽然 Telepathy 规范试图涵盖通信协议导出的各种功能,但有些协议本身是可扩展的4。Telepathy 的开发者希望能够扩展 Telepathy 连接以利用这些扩展,而无需扩展 Telepathy 规范本身。这通过使用侧车来实现。

侧车通常由连接管理器中的插件实现。客户端调用一个方法,请求实现给定 D-Bus 接口的侧车。例如,有人对 XEP-0016 隐私列表的实现可能会实现一个名为 `com.example.PrivacyLists` 的接口。然后,该方法返回由插件提供的 D-Bus 对象,该对象应该实现该接口(以及可能的其他接口)。该对象与主连接对象并存(因此得名侧车,就像摩托车上的侧车一样)。

侧车的历史

在 Telepathy 的早期,每个孩子一台笔记本电脑项目需要支持自定义 XMPP 扩展(XEP)来在设备之间共享信息。这些直接添加到 Telepathy-Gabble(XMPP 连接管理器)中,并通过连接对象上的未公开接口暴露。最终,随着越来越多的开发者希望支持在其他通信协议中没有对应物的特定 XEP,人们达成一致,需要一个更通用的插件接口。

20.7. 简要了解连接管理器内部

大多数连接管理器使用 C/GLib 语言绑定编写,并且开发了大量高级基类,使编写连接管理器变得更容易。如前所述,D-Bus 对象从实现许多软件接口的软件对象发布,这些软件接口映射到 D-Bus 接口。Telepathy-GLib 提供基对象来实现连接管理器、连接和通道对象。它还提供了一个接口来实现通道管理器。通道管理器是工厂,`BaseConnection` 可以使用它们来实例化和管理要在总线上发布的通道对象。

绑定还提供所谓的mixin。这些可以添加到类中以提供额外的功能,抽象规范 API 并通过一种机制为 API 的新旧版本提供向后兼容性。最常用的 mixin 是一个将 D-Bus 属性接口添加到对象的 mixin。还有一些 mixin 用于实现 `ofdT.Connection.Interface.Contacts` 和 `ofdT.Channel.Interface.Group` 接口,以及通过一组方法实现旧的和新的 presence 接口,以及旧的和新的文本消息接口的 mixin。

[Example Connection Manager Architecture]

图 20.5:示例连接管理器架构

使用 Mixin 解决 API 错误

mixin 用于解决 Telepathy 规范中的错误的一个地方是 `TpPresenceMixin`。Telepathy 暴露的原始接口(`odfT.Connection.Interface.Presence`)非常复杂,对连接和客户端来说都很难实现,并且暴露的功能既在大多数通信协议中不存在,又在其他协议中很少使用。该接口被一个更简单的接口(`odfT.Connection.Interface.SimplePresence`)取代,该接口暴露了用户关心的所有功能,这些功能也是连接管理器中唯一真正实现的功能。

presence mixin 在连接上实现了这两个接口,以便旧的客户端可以继续工作,但只有在更简单接口的功能级别上工作。

20.8. 经验教训

Telepathy 是如何在 D-Bus 之上构建模块化、灵活的 API 的一个很好的例子。它展示了如何在 D-Bus 之上开发一个可扩展的、解耦的框架。它不需要中央管理守护程序,并且允许组件重新启动,而不会在其他组件中丢失数据。Telepathy 还展示了如何有效地使用 D-Bus,从而最大限度地减少在总线上发送的流量。

Telepathy 的开发是迭代的,随着时间的推移,它对 D-Bus 的使用不断改进。犯了一些错误,也汲取了一些教训。以下是在设计 Telepathy 架构时学到的一些重要内容

脚注

  1. http://telepathy.freedesktop.org/,或查看开发人员手册 http://telepathy.freedesktop.org/doc/book/
  2. http://telepathy.freedesktop.org/spec/
  3. 从这里开始,/org/freedesktop/Telepathy/org.freedesktop.Telepathy 将缩写为 ofdT 以节省空间。
  4. 例如,可扩展消息和状态协议 (XMPP)。