成功迁移到自定义 XMPP 解决方案
已发表: 2019-04-08我将向您介绍我们从第三方聊天迁移到为我们的客户 Forward Health 定制的基于 XMPP 的消息传递解决方案所面临的挑战,Forward Health 是一个位于英国的消息传递医疗保健解决方案。 本文将介绍迁移的原因、我们的期望与实施的现实以及构建附加功能的挑战。
我们开始的地方
我们的客户 Forward Health 想要为英国的医护人员构建一个移动通信应用程序,包括聊天功能。 作为一家初创公司,他们想快速展示他们的工作产品。 同时,消息传递必须可靠、稳健并且能够安全地发送敏感的患者数据。 为了实现这一点,我们决定使用一种可用的第三方解决方案来实现聊天功能。
聊天功能并不是一件小事,尤其是当它旨在支持医疗保健行业时。 随着应用程序的发展,我们在库方面遇到了更多的边缘案例和一些第三方不愿意处理的错误。 此外,Forward Health 希望添加第三方库不支持的新功能。 下一步是切换到自定义解决方案。
那是我们开始使用MongooseIM的时候。 MIM 是基于成熟的 XMPP 协议的开源解决方案。 我们与外部公司 Erlang Solutions Limited 合作建立我们的后端并提供实施定制解决方案的支持。
起初,关于消息传递的一切似乎都不同。 以前,我们通过 SDK 及其 REST API 满足了我们的所有需求。 现在,使用 MongooseIM,我们不得不花一些时间来了解 XMPP 的本质并实现我们自己的 SDK 。 事实证明,“最基本的”XMPP 服务器只在客户端之间实时传递节(XML 消息)。 节可以是不同的类型,即正常的聊天消息、存在、请求和响应。 可以将各种各样的模块添加到服务器中,例如,存储消息并让客户端查询它们。
在客户端(Android、iOS)有一些低级 SDK。 不幸的是,它们只是作为一个层来实现与 MongooseIM 及其一些称为 XEP 的可插入模块的通信(XMPP 扩展协议负责为每条消息发送推送通知等)。 消息处理、存储和查询消息的整个架构必须由我们的团队实现。
拯救我们的是我们之前使用的第三方库。 它有一个经过深思熟虑的 API,所以我们让我们的解决方案以类似的方式工作。 我们将 XMPP 特定代码分离到我们的内部 SDK 中,其接口对应于之前解决方案中的一个。 这导致迁移后我们的应用程序代码只发生了一些变化。
在 MongooseIM 的实施过程中,我们多次对我们认为是标准的元素感到惊讶,但这些元素对我们来说是不可用的,即使是 XEP 也是如此。
实现基于 XMPP 的聊天的关键功能
时间戳
您可能会认为,就像我们所做的那样,时间戳就像“我收到一条消息,我在 UI 上显示它并带有时间戳”一样简单。 不,没那么容易。 默认情况下,消息节没有时间戳字段。 对于我们的团队来说幸运的是,XMPP 是一个易于扩展的协议。 在后端,我们实现了一项自定义功能,为通过 MongooseIM 服务器的每条消息添加时间戳。 然后收件人会将时间戳附加到消息中。
为什么发件人不能自己添加时间戳? 好吧,我们不知道他们是否在手机上设置了正确的时间。
为什么没有任何 XEP 呢? 可能因为 XMPP 是一个实时协议,所以理论上发送的每条消息都会立即收到。
编辑:正如 Florian Schmaus 指出的那样:“实际上有一个,尽管它很容易被忽略,因为它的名称令人困惑:XEP-0203:延迟交付。” 仅当消息的传递延迟时,它才会向消息添加时间戳。 否则,消息是刚刚发送的。
离线消息
当两个用户都登录到应用程序时,他们可以实时互相发送消息。 但是,如果其中一个离线怎么办? 快速回答是:消息必须在后端缓冲。 离线消息功能处理这项工作,并在用户重新登录后将所有缓冲的节发送给用户。
但随后出现了几个问题:
- 这些消息应该缓冲多长时间?
- 他们有多少?
- 是否应该在重新登录后重新发送? 但它会用消息淹没客户端,不是吗?
- 如果用户只登录,但没有与新消息一起进入聊天,该怎么办。 他们都会消失吗?
- 如果用户在多个设备上登录怎么办?
很明显,离线消息功能只能将消息发送到第一个重新在线的设备,然后这些消息对于所有其他设备都会丢失。 我们决定放弃此功能,并以不同的持久方式将消息存储在 XMPP 后端。

消息归档管理 (MAM)
MAM 是消息的服务器上存储。 当客户端登录时,他们可以向服务器查询消息。 可以按页查询,也可以按日期查询。 它很灵活——您甚至可以在具有特定 ID 的消息之前或之后查询页面,为来自确切对话的消息添加过滤器。
但这就是问题所在。 普通聊天消息包装在 MAM 消息中存储,这些消息具有自己的唯一 ID。 当用户在流中收到聊天消息时,它不包含 MAM ID。 他们必须查询 MAM 才能得到它。
从 MAM 检索是一个网络请求,这意味着它可能需要相对较长的时间。 当用户进入聊天时,他们希望立即看到消息。 所以我们还需要一个本地数据库。
当用户在流中收到消息(在线消息)时,我们将其保存到本地数据库并显示给用户。 这样,我们向用户显示实时快速到达的消息。
此外,每次他们进入聊天屏幕时,我们都会将所有消息下载到存储在本地数据库中的最新 MAM 消息中,然后将它们放入数据库中,忽略重复项。
这就是我们处理存储旧消息的方式。 此外,我们确信在数据库中有一组完整的消息,用于来自 MAM 的第一条和最后一条消息之间的特定对话。
为了跟踪从 MAM 下载的消息,我们向对话实体添加了两个属性:
- 数据库中最新 MAM 消息的 MAM id
- 数据库中最早的 MAM 消息的 MAM id
在本地数据库中处理破碎的 MAM 消息集将是非常有问题的。
此外,每个对话都有这两个属性允许我们在数据库中存储正常的聊天消息,同时忽略包装器 - MAM 消息。 当用户进入聊天时,我们可以显示来自数据库的最新消息,并在后台从 MAM 获取丢失的消息。
收件箱
每个基于聊天的应用程序都需要一个带有聊天列表的屏幕——您可以在其中查看姓名、最后一条消息和未读消息计数。 一定有办法解决的!
实际上,没有……有一个叫做名册的东西——它可以保存一个标记为“朋友”的用户列表。 不幸的是,没有最后一条消息,也没有附加到它们的未读消息计数。 当然,您可以从后端获取所需的信息。 起初,我们想那样做,但它工作起来很慢而且做起来很复杂。 就在那时,我们开始与Erlang Solutions合作开发收件箱功能,该功能也正在走向开源。
当用户连接到 XMPP 后端时,应用程序会获取他们的收件箱,其中包含该用户的所有对话——包括一对一和团队聊天。 他们每个人都附有最后一条消息和未读消息的计数。 应用程序将整个收件箱保存到本地数据库。 当用户在应用程序中并且有新消息到达时,我们会在本地更新收件箱状态。 这样,应用程序就不需要为每条新消息获取收件箱。
概括
一些第三方聊天解决方案提供了高级别的抽象。 如果您想创建一个简单的聊天应用程序,这是可以的。 通过在 Forward 应用程序中实现我们自己的基于 XMPP 的解决方案,我们能够获得更好的低级访问,这使得解决问题变得更加容易。 当然,这需要一些时间,但现在我们知道我们可以提供任何自定义功能来帮助英国的医生以 NHS 批准的安全和简单的方式进行沟通。
消息传递是关于高性能的实时通信。 通过切换到 MIM,我们能够优化解决方案的每个部分,以提高速度、可靠性并最终提高信任度。 目前,我们拥有完整的代码,因此很容易追踪它们。 此外,我们正处于稳定阶段之后,与消息传递相关的一些报告已大幅减少。 用户对能够信任该平台感到满意。
设计和编写我们自己的 SDK 是一项具有挑战性的任务,我们喜欢它。 这与需要从服务器获取数据并将其显示在屏幕上的简单应用程序不同。 在实现过程中,我们了解了我们之前使用的第三方库 API 的许多设计选择。 为什么? 因为我们也遇到过同样的问题。