SpacetimeDB 客户端连接实战指南:从DbConnection建立到生命周期管理
【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB
本篇技术指南围绕 SpacetimeDB 1.12.0 客户端的核心入口DbConnection展开,讲解如何在生成模块绑定(bindings)之后,通过 TypeScript、C#、Rust、Unreal 四种 SDK 建立与数据库的 WebSocket 长连接,涵盖连接参数构建、MainCloud 与 Token 认证、连接推进(advance)机制、生命周期回调以及 Identity / ConnectionId 的身份模型。读完本文,你将掌握四种语言下完整的连接接入方案,并理解连接在底层是如何工作的,从而在实际项目中正确接入并避免"连上了但收不到数据"这类经典坑。
连接前置条件
在调用DbConnection之前,需要确认三件事已经就绪:
- 已为你的模块生成客户端绑定:通过
spacetime generate生成类型安全接口,具体方法见 生成客户端绑定。绑定会为模块中的表生成类型定义与访问器、为 reducer 生成可调用函数、为订阅提供查询接口,并支持注册数据库变更回调,保证客户端与服务端在编译期就具备类型安全。 - 一个已发布并正在运行的数据库:可以是本地 host,也可以是部署在 MainCloud 上的数据库。
- 数据库的 URI 以及数据库的名称或 Identity:URI 指向 SpacetimeDB host,名称或 Identity 用于标识目标数据库。
关于数据库与模块的关系,可以参考 核心架构 中的说明:host 是承载数据库的服务器;数据库是运行在 host 上的应用,导出表(tables)与 reducer;而模块则是一份用 C#、Rust 或 TypeScript 编写、定义了数据库 schema 与业务逻辑的软件。在连接前理解这条链路,有助于准确填写连接参数。
基本连接:DbConnection构建器模式
四种 SDK 都遵循同一种构建器(builder)模式:先通过一系列withXxx方法设置连接参数,再调用build()(或new)完成连接。最基本的连接只需要两个参数:host 的 URI 与数据库的名称(或 Identity)。
import { DbConnection } from './module_bindings'; const conn = new DbConnection.builder() .withUri("https://maincloud.spacetimedb.com") .withModuleName("my_database");using SpacetimeDB; var conn = DbConnection.Builder() .WithUri(new Uri("https://maincloud.spacetimedb.com")) .WithModuleName("my_database") .Build();use module_bindings::DbConnection; let conn = DbConnection::builder() .with_uri("https://maincloud.spacetimedb.com") .with_module_name("my_database") .build();#include "ModuleBindings/DbConnection.h" UDbConnection* Conn = UDbConnection::Builder() ->WithUri(TEXT("https://maincloud.spacetimedb.com")) ->WithModuleName(TEXT("my_database")) ->Build();使用时应将"https://maincloud.spacetimedb.com"替换为实际的 SpacetimeDB host URI,将"my_database"替换为数据库的名称或 Identity。
构建器参数的底层约束
从 Rust SDK 的源码实现(sdks/rust/src/db_connection.rs)可以看到with_uri与with_database_name的具体语义:
with_uri(第 1060 行):URI 必须不带 scheme 或使用http、https、ws、wss中的一种,SDK 会将其解析为http::Uri;连接时实际建立的是 WebSocket。with_database_name(第 1067 行):接受数据库的名称或 Identity,在build时与 URI 一起传给WsConnection::connect,用于在 host 上定位目标数据库(第 980-986 行)。build()标注了#[must_use](第 936 行),编译期即提醒开发者:连接建立后必须显式推进连接(frame_tick、run_threaded、run_background_task、run_async或advance_one_message之一),否则连接永远不会前进。
在 TypeScript SDK 中(sdks/typescript/src/sdk/db_connection_builder.ts),build()会在缺少uri或nameOrAddress时直接抛错(第 264-272 行),并调用ensureMinimumVersionOrThrow校验模块绑定的 CLI 版本与运行时兼容性。也就是说,"URI + 数据库标识"是连接的最低要求,二者缺一不可。
更多可配置项(进阶)
构建器还提供了一批可选的连接参数,这些在原文档基础上可从源码确认:
| 构建器方法 | 作用 | 源码依据 |
|---|---|---|
with_compression/withCompression | 设置消息压缩算法,TypeScript 支持'gzip' | 'brotli' | 'none'(默认 gzip),host 端对超过 1KiB 阈值的消息启用压缩 | Rust 第 1093 行、TS 第 96 行 |
with_confirmed_reads/withConfirmedReads | 启用"已确认读":服务端仅在事务被确认为持久化后才下发查询结果;会增大 reducer 调用与订阅更新到达客户端之间的延迟 | Rust 第 1113 行、TS 第 143 行 |
with_debug_to_file | 将 SDK 内部日志追加写入指定文件,用于排查 SDK 问题;会产生大量日志并影响性能,不应在生产环境使用,且多连接并行时建议各自使用独立文件 | Rust 第 1130 行 |
连接 MainCloud
MainCloud 是 SpacetimeDB 的托管服务,连接方式与本地完全一致,只需将 URI 指向https://maincloud.spacetimedb.com,并填上部署在该云上的数据库名称(或 Identity)。四语言写法与基本连接相同,此处不再重复代码,直接替换 URI 与模块名即可:
- TypeScript:
new DbConnection.builder().withUri("https://maincloud.spacetimedb.com").withModuleName("my_database"); - C#:
DbConnection.Builder().WithUri(new Uri("https://maincloud.spacetimedb.com")).WithModuleName("my_database").Build(); - Rust:
DbConnection::builder().with_uri("https://maincloud.spacetimedb.com").with_module_name("my_database").build(); - Unreal:
UDbConnection::Builder()->WithUri(TEXT("https://maincloud.spacetimedb.com"))->WithModuleName(TEXT("my_database"))->Build();
关于 MainCloud 的数据库发布与部署流程,详见 MainCloud 部署文档。
使用 Token 认证
SpacetimeDB 的认证基于 OpenID Connect:身份由 JWT(JSON Web Token)中的 issuer 与 subject 字段哈希派生而来(具体算法见 核心架构文档中的 Identity 一节)。你可以通过 SpacetimeAuth 或任何符合 OIDC 规范的提供商获取 JWT,然后在构建连接时通过withToken传入:
const conn = new DbConnection.builder() .withUri("https://maincloud.spacetimedb.com") .withModuleName("my_database") .withToken("your_auth_token_here");var conn = DbConnection.Builder() .WithUri(new Uri("https://maincloud.spacetimedb.com")) .WithModuleName("my_database") .WithToken("your_auth_token_here") .Build();let conn = DbConnection::builder() .with_uri("https://maincloud.spacetimedb.com") .with_module_name("my_database") .with_token("your_auth_token_here") .build();UDbConnection* Conn = UDbConnection::Builder() ->WithUri(TEXT("https://maincloud.spacetimedb.com")) ->WithModuleName(TEXT("my_database")) ->WithToken(TEXT("your_auth_token_here")) ->Build();Token 会在连接握手时发送给服务器,用于验证你的身份。关于如何获取和管理 Token,参考 SpacetimeAuth 文档。
匿名连接与会话令牌
从 Rust SDK 源码的with_token注释(sdks/rust/src/db_connection.rs)可以确认两条重要行为:
- Token 是可选的。如果不调用
with_token(或显式传入None),host 会为这次连接生成一个新的匿名 Identity。也就是说,不传 Token 也能连上,只是每次都是"新用户"。 - Token 拒绝的两种时机:如果握手建立前 Token 就被拒绝,
build()会直接返回错误;如果 WebSocket 已建立、但在收到初始连接消息之前被拒绝,则会触发on_connect_error回调。
另外注意:on_connect回调收到的第三个参数就是"私有访问令牌",它是服务端签发给当前 Identity 的凭证,应保存下来供下次连接复用(TypeScript 的onConnect文档同样强调这一点,见 db_connection_builder.ts)。Token 的完整获取与签发流程,可阅读 SpacetimeAuth 的 项目创建指南。
推进连接(Advance):C# 与 Unreal 的关键一步
Critical:C#、Unity 与 Unreal 用户必读
在 C#(包括 Unity)与 Unreal Engine 中,你必须手动推进连接来消费入站消息——连接不会自动处理消息!
如果你使用 C#(含 Unity)或 Unreal,必须在游戏循环或更新方法中调用DbConnection.FrameTick():
// In Unity, call this in your Update() method void Update() { conn.FrameTick(); } // Or in a console application, call this in your main loop while (running) { conn.FrameTick(); // Your application logic... }// In your Actor's Tick() method void AMyActor::Tick(float DeltaTime) { Super::Tick(DeltaTime); if (Conn) { Conn->FrameTick(); } }如果不推进连接,客户端将收不到任何来自服务器的更新——包括订阅数据、reducer 回调以及连接事件。这是新手最容易踩的坑:连接看起来"建好了",但数据一动不动。
相比之下,Rust 与 TypeScript 不需要手动轮询:TypeScript 通过浏览器的事件循环或 Node.js 的事件循环自动处理消息;Rust 则依赖 Tokio 异步运行时。
底层机制:为什么 Rust/TS 不用手动推进
从 Rust SDK 源码(sdks/rust/src/db_connection.rs)可以看出,frame_tick的实现本质是循环调用advance_one_message,直到没有待处理消息为止(第 656-659 行):
pub fn frame_tick(&self) -> crate::Result<()> { while self.advance_one_message()? {} Ok(()) }整个连接由三部分组成(第 5-17 行、第 992-999 行):
- 一个后台 Tokio worker(
WsConnection)负责收发原始 WebSocket 消息; parse_loop将原始消息解析为领域类型ParsedMessage;- 当用户调用
advance_one_message/frame_tick时,已解析的消息才会被应用:更新客户端缓存、触发回调。
Rust SDK 还提供了另外三种"自动推进"的方式(第 661-705 行):
| 方法 | 适用场景 |
|---|---|
run_threaded() | 非浏览器环境:启动一个独立线程循环推进,正常断连时优雅退出 |
run_async()/advance_one_message_async() | 异步环境(async fn 中await) |
run_background_task() | 浏览器(wasm)环境:通过wasm_bindgen_futures::spawn_local在本地任务队列中循环推进 |
有趣的是,"Rust/TS 自动处理"与"C#/Unreal 手动推进"的差异,本质上是谁在驱动事件循环的问题:浏览器与 Node.js 的事件循环天然持续运转,Tokio runtime 也能在后台调度任务;而 C#/Unreal 这类以帧为驱动模型的宿主,把推进权明确交给了开发者,让你在Update()/Tick()中自行控制消息处理的时机与粒度。
连接生命周期
连接回调
注册回调可以观察连接状态的变化。连接建立成功、连接失败、断开连接三种事件分别对应on_connect/on_connect_error/on_disconnect:
const conn = DbConnection.builder() .withUri("https://maincloud.spacetimedb.com") .withModuleName("my_database") .onConnect((conn, identity, token) => { console.log(`Connected! Identity: ${identity.toHexString()}`); // Save token for reconnection localStorage.setItem('auth_token', token); }) .onConnectError((_ctx, error) => { console.error(`Connection failed:`, error); }) .onDisconnect(() => { console.log('Disconnected from SpacetimeDB'); });var conn = DbConnection.Builder() .WithUri(new Uri("https://maincloud.spacetimedb.com")) .WithModuleName("my_database") .OnConnect((conn, identity, token) => { Console.WriteLine($"Connected! Identity: {identity}"); // Save token for reconnection }) .OnConnectError((error) => { Console.WriteLine($"Connection failed: {error}"); }) .OnDisconnect((conn, error) => { if (error != null) { Console.WriteLine($"Disconnected with error: {error}"); } else { Console.WriteLine("Disconnected normally"); } }) .Build();let conn = DbConnection::builder() .with_uri("https://maincloud.spacetimedb.com") .with_module_name("my_database") .on_connect(|_ctx, _identity, token| { println!("Connected! Saving token..."); // Save token for reconnection }) .on_connect_error(|_ctx, error| { eprintln!("Connection failed: {}", error); }) .on_disconnect(|_ctx, error| { if let Some(err) = error { eprintln!("Disconnected with error: {}", err); } else { println!("Disconnected normally"); } }) .build() .expect("Failed to connect");// Create delegates FOnConnectDelegate ConnectDelegate; ConnectDelegate.BindDynamic(this, &AMyActor::OnConnected); FOnConnectErrorDelegate ErrorDelegate; ErrorDelegate.BindDynamic(this, &AMyActor::OnConnectError); FOnDisconnectDelegate DisconnectDelegate; DisconnectDelegate.BindDynamic(this, &AMyActor::OnDisconnected); // Build connection with callbacks UDbConnection* Conn = UDbConnection::Builder() ->WithUri(TEXT("https://maincloud.spacetimedb.com")) ->WithModuleName(TEXT("my_database")) ->OnConnect(ConnectDelegate) ->OnConnectError(ErrorDelegate) ->OnDisconnect(DisconnectDelegate) ->Build(); // Callback functions (must be UFUNCTION) UFUNCTION() void OnConnected(UDbConnection* Connection, FSpacetimeDBIdentity Identity, const FString& Token) { UE_LOG(LogTemp, Log, TEXT("Connected! Identity: %s"), *Identity.ToHexString()); // Save token for reconnection } UFUNCTION() void OnConnectError(const FString& Error) { UE_LOG(LogTemp, Error, TEXT("Connection failed: %s"), *Error); } UFUNCTION() void OnDisconnected() { UE_LOG(LogTemp, Warning, TEXT("Disconnected from SpacetimeDB")); }回调触发时机的精确语义
结合 Rust SDK 源码(sdks/rust/src/db_connection.rs)与 TypeScript 实现(db_connection_builder.ts),三个回调有明确的边界:
on_connect:在收到 host 的初始连接消息(InitialConnection)后触发。回调携带三个参数:已连接的DbConnection、本次连接的Identity、以及可用于今后以同一身份重新认证的私有访问令牌(若你通过with_token传入了 Token,它就是那个 Token)。这正是持久化 Token 实现"记住我"的最佳时机。on_connect_error:仅在收到初始连接消息之前的异步连接失败时触发;在build()阶段直接抛出的错误不会走这个回调。例如 host 在 WebSocket 建立后、初始消息前拒绝了 Token,就会触发它。on_disconnect:仅在连接成功建立之后被关闭时触发(无论是主动disconnect()还是出错),错误参数null/None表示正常断开。如果build()失败或触发的是on_connect_error,则不会走到这里。
TypeScript 的onDisconnect还约束:同一个DbConnectionBuilder上onDisconnect只能注册一次,重复调用会抛错(第 220-221 行);Rust 同样通过panic!限制每种回调只能注册一个,并建议"注册单个回调来执行多项操作"(第 1144-1151 行)。Unreal 的onConnect回调所接收的Identity与Token参数还必须在UFUNCTION中声明,否则不会被引擎执行。
主动断开连接
当不再需要连接时,应显式关闭:
conn.disconnect();conn.Disconnect();conn.disconnect();Conn->Disconnect();Rust 的disconnect()(第 713 行)实现值得注意:它并非立即关闭 socket,而是向pending_mutations队列投递一条Disconnect消息,由下一次推进连接时真正执行——这种"先入队、后应用"的设计(queue_mutation,第 723-730 行)是为了避免在回调执行过程中直接持有内部锁导致死锁。因此断开操作同样依赖于连接推进:在 C#/Unreal 中调用了Disconnect()后,仍需在循环中调用FrameTick()让断连真正生效。
重连行为:当前限制与建议
注意(当前限制)
自动重连在各 SDK 中的实现并不一致。如果连接被中断,你可能需要重新创建一个
DbConnection来恢复连接。如果你的应用对连接可靠性有硬性要求,建议在应用层自行实现重连逻辑。
这是一条重要的工程实践提示:不要假设 SDK 会替你处理断线重连。推荐的模式是把build()封装成一个可重复调用的函数,配合on_connect_error与on_disconnect回调触发退避重试(backoff),并结合on_connect中保存的 Token 以同一身份重连。
连接身份:Identity 与 ConnectionId
每次连接都会从服务器获得一个唯一的 Identity,通过on_connect回调即可访问:
.onConnect((conn, identity, token) => { console.log(`Identity: ${identity.toHexString()}, ConnectionId: ${conn.connectionId}`); }).OnConnect((conn, identity, token) => { var connectionId = conn.ConnectionId; Console.WriteLine($"Identity: {identity}, ConnectionId: {connectionId}"); }).on_connect(|ctx, identity, token| { let connection_id = ctx.connection_id(); println!("Identity: {:?}, ConnectionId: {:?}", identity, connection_id); })UFUNCTION() void OnConnected(UDbConnection* Connection, FSpacetimeDBIdentity Identity, const FString& Token) { FSpacetimeDBConnectionId ConnectionId = Connection->GetConnectionId(); UE_LOG(LogTemp, Log, TEXT("Identity: %s, ConnectionId: %s"), *Identity.ToHexString(), *ConnectionId.ToHexString()); }两者的区别是理解整个鉴权模型的关键(核心架构文档):
- Identity:长期有效、全局有效的公开标识符,跨连接保持不变,始终指向同一个终端用户。用户的 Identity 会附加在其发起的每次 reducer 调用上,可用于权限判定。模块自身也有 Identity——
spacetime publish时会被自动签发一个,客户端连接 host 时需要提供它。 - ConnectionId:唯一标识一次连接会话。同一个用户可以在你的数据库上打开多条连接,每条都会获得不同的
ConnectionId。
在 Rust SDK 中,Identity 与 ConnectionId 在收到InitialConnection消息后才填充(identity/connection_id字段初始为None,见 db_connection.rs),并分别通过try_identity()与try_connection_id()提供可选访问、connection_id()提供直接访问(第 764-778 行)。在on_connect回调中读取它们,可以保证这两个值一定已经就绪。
连接建立之后:下一步做什么
连接就绪后,你可以:
- 通过 SDK API 与表交互、调用 reducer、订阅数据;
- 注册回调以观察数据库变更(表的插入、更新、删除);
- 调用服务端的 reducer 与 procedure。
各语言专属的详细参考见:
- Rust SDK 参考
- C# SDK 参考
- TypeScript SDK 参考
- Unreal SDK 参考
常见问题速查
- 连上了但收不到任何数据?检查你是否在 C#/Unity/Unreal 中调用了
FrameTick()——这是这些宿主上消息处理的唯一入口;Rust 端则确认build()返回的DbConnection没有被编译器警告(#[must_use])忽略,务必调用frame_tick/run_*之一。 - Token 被拒?区分两种时机:
build()直接报错(握手前拒绝)与on_connect_error回调触发(WebSocket 建立后、初始消息前拒绝)。Token 应为 OIDC 兼容的 JWT,获取方式见 SpacetimeAuth 文档。 - 连接中断后怎么办?当前 SDK 的自动重连实现并不一致,请自行封装重连逻辑,并结合
on_connect回调保存的 Token 恢复同一身份的会话。 - 如何在多端保持同一用户身份?在
on_connect中持久化第三个参数(Token),下次连接时通过with_token传入即可;Identity 跨连接不变,而每次连接的 ConnectionId 均不同。
【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考