SpacetimeDB 客户端连接实战指南:从 `DbConnection` 建立到生命周期管理
2026/9/13 11:28:04 网站建设 项目流程

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之前,需要确认三件事已经就绪:

  1. 已为你的模块生成客户端绑定:通过spacetime generate生成类型安全接口,具体方法见 生成客户端绑定。绑定会为模块中的表生成类型定义与访问器、为 reducer 生成可调用函数、为订阅提供查询接口,并支持注册数据库变更回调,保证客户端与服务端在编译期就具备类型安全。
  2. 一个已发布并正在运行的数据库:可以是本地 host,也可以是部署在 MainCloud 上的数据库。
  3. 数据库的 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_uriwith_database_name的具体语义:

  • with_uri(第 1060 行):URI 必须不带 scheme 或使用httphttpswswss中的一种,SDK 会将其解析为http::Uri;连接时实际建立的是 WebSocket。
  • with_database_name(第 1067 行):接受数据库的名称或 Identity,在build时与 URI 一起传给WsConnection::connect,用于在 host 上定位目标数据库(第 980-986 行)。
  • build()标注了#[must_use](第 936 行),编译期即提醒开发者:连接建立后必须显式推进连接frame_tickrun_threadedrun_background_taskrun_asyncadvance_one_message之一),否则连接永远不会前进。

在 TypeScript SDK 中(sdks/typescript/src/sdk/db_connection_builder.ts),build()会在缺少urinameOrAddress时直接抛错(第 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)可以确认两条重要行为:

  1. Token 是可选的。如果不调用with_token(或显式传入None),host 会为这次连接生成一个新的匿名 Identity。也就是说,不传 Token 也能连上,只是每次都是"新用户"。
  2. 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还约束:同一个DbConnectionBuilderonDisconnect只能注册一次,重复调用会抛错(第 220-221 行);Rust 同样通过panic!限制每种回调只能注册一个,并建议"注册单个回调来执行多项操作"(第 1144-1151 行)。Unreal 的onConnect回调所接收的IdentityToken参数还必须在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_erroron_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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询