此内容已使用自动机器翻译服务进行翻译,仅供您参考及阅读便利。其中可能包含错误、遗漏,或与原始英文版本存在理解方面的细微差别。如有疑问,请参考原始英文版本。
社交媒体用户厌倦了在每次平台关闭或转移时就失去自己的身份和数据。在 ATProto 生态系统(“身份验证传输协议”的缩写)中,用户拥有自己的数据和身份。他们发布的所有内容都将成为一个加密签名的全球共享社交网络的一部分。Bluesky 是第一个大型案例,但新一波的去中心化社交网络浪潮才刚刚开始。本篇博客文章将介绍如何开始,在 Cloudflare 的开发人员平台上构建和部署一个完全无服务器的 ATProto 应用程序。
为何采用无服务器?管理虚拟机、扩展数据库、维护 CI 管道、跨可用区分布数据以及保护 API 免受 DDoS 攻击等开销分散了实际构建的注意力。
Cloudflare 因此与之合作。您可以利用我们的开发人员平台,构建在我们的全球网络上运行的应用程序:Workers 以毫秒为单位部署代码,KV 提供快速的全球分布式缓存,D1 提供分布式关系数据库,Durable Objects 管理 WebSocket 并处理协调。最重要的是,我们的免费层提供了构建无服务器 ATProto 应用程序所需的一切,因此您可以不花一分钱开始使用。代码在此 GitHub repo 中。
ATProto 生态系统:简单介绍
让我们先概念性概述一下数据在 ATProto 生态系统中的流动方式:
用户与应用程序交互,后者将更新写入用户的个人存储库。这些更新会触发更改事件,更改事件会发布到中继并通过全局事件流进行广播。任何应用程序都可以订阅这些事件——即使它没有发布原始更新——因为在 ATProto 中,存储库、中继和应用程序都是独立的组件,可以(并且已经)由不同的运营商运行。
身份
用户身份以人类可读的名称,例如alice.example.com
。每个用户名都必须是有效域名,以便协议利用 DNS 来提供谁拥有什么账户的全球视图。句名映射到用户的去中心化标识符(DID),其中包含用户的个人数据服务器(PDS)的位置。
身份验证
用户的 PDS 管理其密钥和存储库。它处理身份验证并通过其存储库提供权威的数据视图。
如果您想了解更多信息,请点击这里阅读:适用于分布式系统工程师的 ATProto 。
这里的不同之处(也很容易被忽略)是,此堆栈的任何部分几乎不依赖于对单一服务的信任。DID 解析是可验证的。PDS 由用户选择。客户端应用只是一个界面。
我们发布或获取数据时,它被签名和自我验证。这意味着任何其他应用都可以使用它或在其上构建,无需征得许可,也无需信任我们的后端。
我们的应用
我们将使用 Statusphere,一个由 ATProto 团队构建的小而完整的演示应用。这是最简单的社交媒体应用:用户发布单一表情符号的状态更新。由于 Statusphere 非常小,因此它成为了一个完美的起点,了解去中心化 ATProto 应用程序如何工作,以及如何调整它们以在 Cloudflare 的无服务器堆栈上运行。
Statusphere 模式
在 ATProto 中,所有存储库数据都使用 Lexicons 进行分类,这是一种类似于 JSON-Schema 的共享模式语言。对于 Statusphere,我们使用 xyz.statusphere.status
记录,该记录最初由 ATProto 团队定义:
{
"type": "record",
"key": "tid", # timestamp-based id
"record": {
"type": "object",
"required": ["status", "createdAt"],
"properties": {
"status": { "type": "string", "maxGraphemes": 1 },
"createdAt": { "type": "string", "format": "datetime" }
}
}
}
词汇表是强类型的,因此应用之间可以轻松进行互操作。
它是如何构建的
在本节中,我们将跟踪 Statusphere 中的数据流动:从身份验证到存储库读取和写入,再到实时更新,并了解我们如何在无服务器基础设施上处理实时事件流。
1. 语言选择
ATProto 的核心库是用 TypeScript 编写的,而 Cloudflare Workers 则提供一流的 TypeScript 支持。这是在 Cloudflare Workers 上构建 ATProto 服务的自然起点。
不过,ATProto TypeScript 库假定一个后端或浏览器上下文。Cloudflare Workers 支持在无服务器环境中使用Node.js API,但 ATProto 库使用的“错误”重定向处理模式与边缘运行时不兼容。
Cloudflare 还通过 WASM 交叉编译在 Workers 中支持 Rust,所以接下来我尝试了这种方法。ATProto Rust 包和代码生成工具充分利用了 Rust 的类型系统和构建工具,但仍在积极开发中。不过,Rust 的 WASM 生态系统很可靠,因此我通过调整 Statusphere 的现有 Rust 实现(最初由 Bailey Townsend 编写),得到了一个快速运行的工作原型。代码在此 GitHub repo 中。
如果您要在 Cloudflare Workers 上构建 ATProto 应用,我建议向 TypeScript 库做出贡献,以更好地支持无服务器运行时。这个应用程序的 TypeScript 版本将是一个不错的下一步——如果您有意构建它,请通过 Cloudflare 开发人员 Discord 服务器 联系 。
2. 持续跟踪
使用此“部署到 Cloudflare”按钮,克隆存储库并设置您自己的 KV 和 D1 实例以及 CI 管道。
按照此链接中的步骤操作,使用默认值或选择自定义名称,它将构建和部署您自己的 Statusphere Worker。
注意:此项目包括一个预定的组件,从公共事件流中读取。完成实验后,您可能希望将其删除,以节省资源。
3. 解析用户的句名
为了与用户的数据进行交互,我们首先使用在 _atproto 子域中注册的记录将用户句了解解析为 DID。例如,我的句重是 inanna.recursion.wtf
,所以我的 DID 记录存储在 _atproto.inanna.recursion.wtf
中。该记录的值为did:plc:p2sm7vlwgcbbdjpfy6qajd4g
。
然后,我们将 DID 解析为其相应的 DID Document,其中包含身份元数据,包括用户个人数据服务器的位置。根据 DID 方法,此解析直接通过 DNS 处理(对于did:web
标识符),或者更常见的是,通过公共凭据分类账(对于did:plc
标识符)。
由于这些值不会经常更改,我们使用Cloudflare KV来缓存它们——它非常适合这种情况:我们有一些不常更新但常读取的键值映射,需要使其能够低延迟地提供给全球。
从 DID 文档中,我们提取了用户个人数据服务器的位置。在我的例子中,属性是 bsky.social
但其他用户可能自行托管自己的 PDS 或使用替代提供商。
OAuth 流程的细节在此并不重要 — 您可以阅读我用于实现它的代码,或者如果感到好奇,可以深入研究 OAuth 规范 — 但简而言之,就是:用户通过他们的 PDS 登录,然后使用其管理的签名密钥向我们的应用授予代表其操作的权限。
我们使用tower-sessions将会话数据保存在安全会话 cookie 中。这意味着,只有一个不透明的会话 ID 存储在客户端,而所有会话/oauth 状态数据都存储在 Cloudflare KV 中。同样,它天生适合该用例。
4. 获取状态和档案数据
使用存储在会话 cookie 中的 DID,我们恢复用户的 OAuth 会话并启动一个经过身份验证的代理:
let agent = state.oauth.restore_session(&did).await?;
代理准备就绪后,我们会获取用户最新的 Statusphere 帖子及其 Bluesky 个人资料。
let current_status = agent.current_status().await?;
let profile = agent.bsky_profile().await?;
有了他们的状态和档案信息后,我们就可以渲染主页了:
Ok(HomeTemplate {
status_options: &STATUS_OPTIONS,
profile: Some(Profile {
did: did.to_string(),
display_name: Some(username),
}),
my_status: current_status,
})
5. 发布更新
当用户发布新的表情符号状态时,我们会在其个人存储库中创建一条新记录——使用我们用于获取其数据的相同认证代理。这一次,我们不执行读取,而是执行创建记录操作:
let uri = agent.create_status(form.status.clone()).await?.uri;
该操作会返回一个 URI —— 新记录的规范标识符。
然后,我们将状态更新写入 D1,以便立即反映在 UI 中。
6. 使用 Durable Objects 来广播更新
每个活动的主页都维护一个到 Durable Object 的 WebSocket 连接,后者充当轻量级实时消息代理。空闲时,Durable Object 进入休眠,以节省资源并维持 WebSocket 连接。我们向 Durable Object 发送消息将其唤醒并广播新的更新:
state.durable_object.broadcast(status).await?;
然后,Durable Object 将这个更新广播到每个连接的主页:
for ws in self.state.get_websockets() {
ws.send(&status);
}
然后,它遍历每一个活动的 WebSocket 并发送更新。
一个实际说明:Durable Objects 在跨实例分片时性能更佳。为简单起见,我描述了所有内容都通过单个 Durable Object运行的情况。
为了扩大规模,下一步将是通过位置提示在每个受支持位置使用多个 Durable Object实例,最大限度地减少全球用户的延迟,并避免在单一位置遇到大量并发用户时出现瓶颈。我最初考虑过实现这种模式,但这与我的目标相冲突,我是创建一个简洁的“hello world”风格示例,供 ATProto 开发人员复制并用作其应用的模板。
7. 监听实时变化
面临的挑战:实时与无服务器对比
在我们自己的应用程序中发布更新很容易,但在 ATProto 生态系统中,其他应用程序也可以为用户发布状态更新。如果我们希望完全集成 Statusphere,我们也需要接收这些事件。
侦听实时事件更新需要与 ATProto Jetstream 服务保持 WebSocket 连接。传统基于服务器的应用程序可以使 WebSocket 客户端套接字无限期地保持打开状态,但无服务器平台做不到这一点——workers 不允许永远运行下去。
我们需要一种无需运行实时服务器即可“监听”的方法。
解决方案:Cloudflare worker Cron Triggers
为解决这个问题,我们将侦听逻辑移入Cron 触发器中 — 不再保持实时套接字打开,而是利用此功能通过定期计划作业小批量读取更新。
当计划的 Worker 调用触发时,它会从持久存储中加载最后看到的游标。然后,它连接到 Jetstream —— ATProto 存储库事件的流式传输服务,按 xyz.statusphere.status 集合进行过滤,并从最后一次看到的游标开始。
let ws = WebSocket::connect("wss://jetstream1.us-east.bsky.network/subscribe?wantedCollections=xyz.statusphere.status&cursor={cursor}").await?;
我们在 Durable Object 的持久存储中存储游标(一种微秒时间戳,标记我们收到的最后一条消息),这样,即使对象重新启动,它也能确切知道从哪里恢复。一旦处理的事件比开始时间更新,我们就会关闭 WebSocket 连接,并让 Durable Object 返回休眠状态。
缺点是:更新可能会滞后一分钟,但系统保持完全无服务器状态。这非常适合处于早期阶段的应用和原型,因为在这些应用和原型中,最小化基础设施的复杂性比实现完美的实时交付更重要。
可选升级:实时事件监听器
如果您想要实时更新,并且愿意在无服务器模型方面稍有偏差,您可以部署一个轻量级侦听器进程来维护与 Jetstream 的实时 WebSocket 连接。
这个过程不是每分钟轮询一次,而是侦听 xyz.statuSphere.status 集合的新事件,并在更新到达时立即推送更新到我们的 Cloudflare Worker。您可以在此处找到该侦听器进程的概述,并在此处找到从中处理更新的端点。
结果依然不是一个传统的服务器:
不会公开暴露在网络上
没有打开的 HTTP 端口
无持久数据库
这只是一个单一用途、无状态的监听器,非常简单,可以在家庭服务器上运行,直到您的应用程序增长到需要更强大的基础设施为止。
之后,您可以使用 Cloudflare Queues 等工具将这种设计替换为更具可扩展性的设计,以提供 批处理和重试——但对于中小型应用程序,这种轻量级 Cloudflare 监听器是一种简单且有效的升级方案。
展望
如今,Durable Objects 可以在持有长时间的 WebSocket服务器连接时进行休眠,但在维持长时间的 WebSocket客户端连接(例如 Jetstream 监听器)时,不支持休眠。这就是为什么 Statusphere 使用变通方法——通过 Cron Trigger 和轻量级外部监听器调用计划的 Worker——与网络保持同步。
未来对 Durable Objects 的改进(例如增加对休眠活动 WebSocket 客户端的支持)可能会完全消除对这些变通方法的需求。
构建您自己的 ATProto 应用
这是一个功能齐全的 atproto 应用,完全在 Cloudflare 上运行,零服务器,操作开销最小。Workers 与大多数用户距离不超过 50 毫秒即可运行您的代码,KV 和 D1 使您的数据保持可用,Durable Objects 则处理 WebSocket 扇出和实时协调。
使用“部署到 Cloudflare”按钮,克隆存储库并设置您的无服务器环境。那么,展示您构建的成果吧。欢迎在我们的 Discord中提交链接,或在 Bluesky 上标记@cloudflare.social,或在 X 上标记@CloudflareDev —— 我们很乐意看到它。