Tutanota 如何用自己的通知系统取代 Google 的 FCM
发布于 2018-09-03,发布者为正如 F-Droid 每周新闻 17 中提到的,Tutanota 现在已进入 F-Droid。
在这篇特别的帖子中,来自 Tutanota 的 Ivan 向我们讲述了这个故事。
嗨,我是 Ivan,我正在开发 Tutanota,以帮助建立未来的网络,让我们的隐私权得到尊重。我认为隐私不应该是富人和精通技术的人的奢侈品,而应该是一项基本人权。
GCM(或现在称为 FCM,Firebase Cloud Messaging)是 Google 拥有的一项服务。我们 Tutanota 将 FCM 用于我们的旧 Android 应用。不幸的是,FCM 包含用于分析的 Google 跟踪代码,我们不想使用它。而且,更重要的是:为了能够使用 FCM,你必须将所有通知数据发送给 Google。你还必须使用他们的专有库。由于随之而来的隐私和安全问题,我们没有在旧应用的通知消息中发送任何信息(可以理解,这导致了我们用户的抱怨)。因此,旧应用中的推送通知仅提及你收到了一条新消息,而没有提及电子邮件本身或该消息所在的邮箱。
FCM 使用起来非常方便,多年来 Google 对 Android 进行了更改,这使得不使用他们的通知服务变得更加困难。另一方面,放弃 Google 的通知服务将使我们免于要求我们的用户在他们的手机上安装 Google Play 服务。
更换 Google FCM 的挑战
Tutanota 应用是自由软件,我们想在 F-Droid 上发布我们的 Android 应用。我们希望我们的用户能够在每个 ROM 和每个设备上使用 Tutanota,而不需要像 Google 这样的第三方的控制。我们决定接受挑战并构建我们自己的推送通知服务。
当我们开始设计推送系统时,我们有几个目标:
- 它必须是安全的
- 它必须很快
- 它必须是节能的
我们研究了其他人 (Signal, Wire, Conversations, Riot, Facebook, Mastodon) 如何解决类似问题。我们想到了几个选项,包括 WebSockets、MQTT、服务器发送事件和 HTTP/2 服务器推送。
用 SSE 替代 FCM
我们选择了 SSE(服务器发送事件),因为它看起来是一个简单的解决方案。我的意思是“易于实现,易于调试”。调试这些类型的东西可能是一件令人头疼的事情,因此不应低估这一因素。支持 SSE 的另一个论据是相对的功率效率:我们不需要上游消息,并且持续连接不是我们的目标。
那么,什么是 SSE?
SSE 是一个 Web API,它允许服务器向连接的客户端发送事件。这是一个相对较旧的 API,在我看来,它未被充分利用。在查看联邦网络 Mastodon 之前,我从未听说过 SSE:他们使用 SSE 进行实时时间线更新,而且效果很好。
协议本身非常简单,类似于老式的轮询:客户端打开连接,服务器保持打开状态。与经典轮询的不同之处在于,我们为多个事件保持此连接打开。服务器可以发送事件和数据消息;它们只是由新行分隔。所以客户端唯一需要做的就是打开一个大超时的连接并循环读取流。
SSE 比 WebSocket 更适合我们的需求(它成本更低且收敛速度更快,因为它不是双工的)。我们已经看到多个聊天应用尝试使用 WebSocket 进行推送通知,但它似乎并不高效。
我们已经有一些使用 WebSocket 的经验,而且我们知道防火墙不喜欢 keepalive 连接。为了解决这个问题,我们对 SSE 使用了与 WebSocket 相同的解决方法:我们每隔几分钟发送一次“心跳”空消息。我们使这个间隔可以从服务器端调整,并随机化以免服务器不堪重负。
多账户支持带来额外挑战
应该注意的是,Tutanota 应用支持多帐户,这对我们提出了挑战:我们希望每台设备只打开一个连接。经过几次迭代,我们找到了令我们满意的设计。每个设备只有一个标识符。打开连接时,客户端会发送它想要接收通知的用户列表。服务器根据用户记录验证此列表并过滤掉无效记录。
用户可以从他们的设置中删除通知令牌,但不会影响此设备上的其他登录。除此之外,我们还必须在收到通知时建立一个交付跟踪机制。不幸的是,我们发现我们的服务器无法检测到连接何时断开,因此我们不得不从客户端发送确认。
为了接收通知,我们利用了 Android 功能。我们运行一个后台服务来保持与服务器的连接打开,类似于 FCM 进程所做的。另一个困难是由 Android M 中引入的打盹模式引起的。打盹模式会在一段时间不活动后打开,除其他外,它会阻止后台进程访问网络。可以想象,这会阻止我们的应用接收通知。
我们通过要求用户免除我们应用的电池优化来缓解这个问题。它工作得相当好。与 Doze 无关的类似问题是特定于供应商的电池优化。为了延长手机制造商(如小米)的电池寿命,默认启用严格的电池优化。幸运的是,用户可以禁用它们,但我们必须更好地传达这一点。
另一个问题是由 Android O 更改引起的。其中之一是后台进程限制:除非你的应用对用户可见,否则你的后台进程将被停止并且你无法启动新进程。
最初我们认为我们可以通过显示具有最低优先级的持久通知来解决这个问题,该通知在通知栏可见,但在状态栏中不可见。这对 Oreo 不起作用:如果你尝试启动后台服务并为其通知使用最低优先级,则通知优先级会升级到更高的优先级(始终可见),除此之外,系统还会显示另一个持续通知:“应用 X 正在使用电池”。
我们最初计划向用户解释他们如何隐藏这些持久通知,但这并不是一个很好的用户体验,所以我们必须找到一个更好的解决方案。我们利用 Android Job 机制定期启动我们的服务(至少每 15 分钟一次),之后我们也尝试让它保持活跃。我们不会手动持有唤醒锁——系统会为我们做这件事。我们能够完全放弃持久通知。即使通知有时会稍有延迟,但始终会收到通知,并且电子邮件会立即出现。
最后,我们不得不做一些工作,但这是完全值得的。我们的新应用仍处于测试阶段,但由于非阻塞 IO,我们已经能够毫无问题地保持数千个同时连接。我们将用户从 Google Play 服务要求中解放出来。最后,每个人都可以在 F-Droid 上获取 Tutanota 应用。该系统现在结合了两者:良好的电源效率和速度。
最后的想法:每个用户都应该能够为每个应用选择一个“通知提供者”
如果用户可以在手机设置中选择一个“推送通知提供程序”并且操作系统自己管理所有这些硬细节,那不是很好吗?所以每个不想被平台所有者监管的应用不就不必重新发明系统了吗?它可以在应用和应用服务器之间进行端到端加密。这并没有真正的技术难题,但只要我们的系统被不允许这样做的大玩家控制,我们就必须自己解决。
