SpacetimeDB 客户端连接完全指南:DbConnection 构建器、WebSocket 生命周期与多语言实践
2026/9/13 0:58:38 网站建设 项目流程

SpacetimeDB 客户端连接完全指南:DbConnection 构建器、WebSocket 生命周期与多语言实践

【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB

本篇技术指南系统讲解 SpacetimeDB 客户端 SDK 的数据库连接机制。在完成模块客户端绑定生成之后,客户端应用通过DbConnection类型建立一条持久化的 WebSocket 连接,从而与服务器进行实时通信。读完本文,你将掌握:如何用各语言 SDK 的构建器模式建立连接、如何通过令牌完成身份认证、C#/Unreal 客户端为何必须手动推进连接(FrameTick)、如何注册连接生命周期回调、如何优雅地断开与重连,以及 Identity 与 ConnectionId 的区别。

连接前的准备工作

在编写任何连接代码之前,需要满足以下三个前提条件:

  1. 已为模块生成客户端绑定:使用spacetime generate --lang <language> --out-dir <dir> --module-path <module-dir>命令生成类型安全的绑定代码,它们镜像了模块的表结构、reducer 与 procedure 签名。生成绑定是连接的前提,因为DbConnection的泛型类型来自这些绑定(例如 Rust SDK 中DbConnection::builder()的完整签名是DbConnectionBuilder<M: SpacetimeModule>)。
  2. 一个已发布并运行的数据库:可以运行在本地(自托管),也可以运行在 SpacetimeDB 托管服务 MainCloud 上。注意数据库与模块的区分:模块是你编写的代码(schema 与业务逻辑),数据库是模块的运行实例,拥有存储数据和活动连接。
  3. 数据库的 URI 与名称或 identity:URI 指向 SpacetimeDB 主机,名称或 identity 用于定位具体数据库。数据库名称必须匹配正则/^[a-z0-9]+(-[a-z0-9]+)*$/(仅小写 ASCII 字母与数字、以短横线分隔),例如my-game-serverchat-app-productiontest123;每个数据库创建时还会获得唯一的十六进制 identity,客户端可以用名称identity 连接。

基本连接:构建器模式

所有语言 SDK 都采用同构的构建器(builder)模式来创建连接。核心构建步骤是设置 URI 和数据库名,然后调用build

// TypeScript import { DbConnection } from './module_bindings'; const conn = DbConnection.builder() .withUri("https://maincloud.spacetimedb.com") .withDatabaseName("my_database") .build();
// C# using SpacetimeDB; var conn = DbConnection.Builder() .WithUri("https://maincloud.spacetimedb.com") .WithDatabaseName("my_database") .Build();
// Rust use module_bindings::DbConnection; let conn = DbConnection::builder() .with_uri("https://maincloud.spacetimedb.com") .with_database_name("my_database") .build();
// Unreal #include "ModuleBindings/DbConnection.h" UDbConnection* Conn = UDbConnection::Builder() ->WithUri(TEXT("https://maincloud.spacetimedb.com")) ->WithDatabaseName(TEXT("my_database")) ->Build();

使用时将"https://maincloud.spacetimedb.com"替换为你自己的 SpacetimeDB 主机 URI,将"my_database"替换为你的数据库名称或 identity。

构建器背后的实现细节

从源码角度,Rust SDK 的构建器在sdks/rust/src/db_connection.rs中提供了语义明确的链式方法:

  • with_uri(...)(sdks/rust/src/db_connection.rs#L1060-L1064):设置远端数据库所在主机的 URI。源码注释明确规定,URI必须不带 scheme,或者使用httphttpswswss之一——这是 WebSocket 协议升级的合法来源;传非法 URI 时build会直接 panic。
  • with_database_name(...)(sdks/rust/src/db_connection.rs#L1067-L1070):接收数据库的名称或 identity字符串。
  • with_token(...)(sdks/rust/src/db_connection.rs#L1083-L1086):携带 OIDC 兼容的 JWT 令牌;若不调用或传None,主机将生成一个全新的匿名 Identity(详见下文“令牌认证”一节)。
  • with_compression(...)(sdks/rust/src/db_connection.rs#L1093-L1096):设置消息压缩策略。当前主机在整条服务器消息或单个查询更新超过1KiB阈值时启用压缩,注意该阈值不保证不变。
  • with_confirmed_reads(...):开启确认读后,服务器只在查询结果确认持久化后才下发——单节点服务器以fsync落盘为持久化标准,集群则以足够数量副本确认存储为标准;代价是 reducer 调用到订阅更新到达之间的延迟增加。

在底层,build()(sdks/rust/src/db_connection.rs#L951-L954)会完成:解析 URI → 建立 WebSocket 连接(WsConnection::connect,将 URI、数据库名、令牌一起传给握手阶段)→ 启动消息循环线程 → 启动parse_loop解析线程 → 创建空客户端缓存 → 组装DbContextImpl连接上下文。在浏览器(wasm)目标下,build是异步方法(sdks/rust/src/db_connection.rs#L956-L960),因为 WebSocket 握手需要异步等待。

连接 MainCloud 托管数据库

如果你的数据库托管在 SpacetimeDB 官方托管服务 MainCloud,只需使用官方主机地址https://maincloud.spacetimedb.com作为 URI:

const conn = DbConnection.builder() .withUri("https://maincloud.spacetimedb.com") .withDatabaseName("my_database") .build();
let conn = DbConnection::builder() .with_uri("https://maincloud.spacetimedb.com") .with_database_name("my_database") .build();

C# 与 Unreal 的写法与此完全一致(分别用WithUri/WithDatabaseName->WithUri(...)/->WithDatabaseName(...))。将模块发布到 MainCloud 使用spacetime publish my-database --server maincloud(详见 MainCloud 部署指南),发布后同样用https://maincloud.spacetimedb.com作为客户端连接主机。

使用令牌进行身份认证

SpacetimeDB 的身份体系基于 OpenID Connect。当应用需要区分用户身份(例如权限控制、跨连接保持同一用户)时,可以在构建连接时通过令牌认证:

const conn = DbConnection.builder() .withUri("https://maincloud.spacetimedb.com") .withDatabaseName("my_database") .withToken("your_auth_token_here") .build();
var conn = DbConnection.Builder() .WithUri("https://maincloud.spacetimedb.com") .WithDatabaseName("my_database") .WithToken("your_auth_token_here") .Build();
let conn = DbConnection::builder() .with_uri("https://maincloud.spacetimedb.com") .with_database_name("my_database") .with_token("your_auth_token_here") .build();
UDbConnection* Conn = UDbConnection::Builder() ->WithUri(TEXT("https://maincloud.spacetimedb.com")) ->WithDatabaseName(TEXT("my_database")) ->WithToken(TEXT("your_auth_token_here")) ->Build();

令牌在连接握手期间发送给服务器,用于校验你的身份。获取和管理令牌的完整流程参见 SpacetimeAuth 文档:SpacetimeAuth 是官方身份提供方,认证流程结束时应用会收到一个ID token(一个 OIDC 兼容的 JWT),其中的emailsubpreferred_username等 claims 描述了用户信息,应用即可用该 token 配合任意 SpacetimeDB SDK 进行认证。也可以使用任何其他 OIDC 兼容的身份提供方签发的令牌。

关于令牌的底层行为,Rust SDK 源码给出了三条明确语义:

  • 不调用with_token,或传入None,主机将为本次连接生成一个匿名 Identity
  • 若令牌在连接上下文创建之前就被服务器拒绝,build()直接返回错误;
  • 若拒绝发生在 WebSocket 已建立、但初始连接消息尚未到达之间,则会触发on_connect_error回调。

另外,Rust SDK 提供了现成的凭据落盘工具sdks/rust/src/credentials.rscredentials::File::new("my_app")可以在用户主目录的.spacetimedb_client_credentials目录下用 BSATN 序列化保存 JWT。典型用法是在on_connect回调中调用credentials::File::new("my_app").save(token)保存服务端下发的令牌,下次启动时用File::load()取回——官方推荐的持久化身份路径。若连接多个集群,建议为每个集群使用独立的 key,避免凭据混淆。

推进连接(FrameTick):C#/Unreal 的必修课

⚠️ 关键提示:C#(含 Unity)与 Unreal 用户必读

在 C#(包括 Unity)中,你必须手动推进连接才能处理入站消息;在 Unreal Engine 中,必须手动推进连接,或者开启自动 tick。如果不推进连接,客户端将收不到任何消息——包括订阅数据、reducer 回调、连接事件,全部不会触发。

在游戏循环或 update 方法中调用FrameTick()

// Unity 中在 Update() 里调用 void Update() { conn.FrameTick(); } // 控制台应用则在主循环中调用 while (running) { conn.FrameTick(); // 你的应用逻辑... }
// 方案 1:在 Actor 的 Tick() 中调用 FrameTick() void AMyActor::Tick(float DeltaTime) { Super::Tick(DeltaTime); if (Conn) { Conn->FrameTick(); } } // 方案 2:构建连接后开启自动 tick(只需一次) Conn = Builder->Build(); Conn->SetAutoTicking(true);

FrameTick的底层实现可以从 C# SDK 源码中直接印证(sdks/csharp/src/SpacetimeDBClient.cs#L1015-L1022):

public void FrameTick() { webSocket.Update(); // 1. 推进底层 WebSocket,收发消息 while (_applyQueue.TryTake(out var parsedMessage)) { ApplyMessage(parsedMessage); // 2. 将队列中的解析消息应用到客户端缓存并触发回调 } }

可以看到FrameTick做两件事:驱动底层 WebSocket 的收发状态机,然后逐一取出已解析的消息队列并应用——即把服务器下发的 diff 写入客户端缓存、触发订阅/行/连接相关回调。因此漏调FrameTick等同于整个消息处理管线停摆。

Rust 与 TypeScript 则完全不需要手动轮询:Rust SDK 在build时于后台 Tokio 运行时中启动 WebSocket 消息循环与parse_loop解析线程,应用只需在业务层调用advance_one_messagerun_asyncrun_background_taskrun_threaded之一即可持续推进(sdks/rust/src/db_connection.rs#L940-L948);TypeScript 则依赖浏览器或 Node.js 的事件循环自动处理消息。两种语言的事件驱动模型天然承担了“推进连接”的职责。

连接生命周期

连接回调:观察连接状态变化

通过构建器注册回调,可以观察连接的建立、失败与断开:

const HOST = "https://maincloud.spacetimedb.com"; const DB_NAME = "my_database"; const TOKEN_KEY = `${HOST}/${DB_NAME}/auth_token`; const conn = DbConnection.builder() .withUri(HOST) .withDatabaseName(DB_NAME) .onConnect((conn, identity, token) => { console.log(`Connected! Identity: ${identity.toHexString()}`); // 保存 token 用于重连——按 服务器/数据库 分别存储 localStorage.setItem(TOKEN_KEY, token); }) .onConnectError((_ctx, error) => { console.error(`Connection failed:`, error); }) .onDisconnect(() => { console.log('Disconnected from SpacetimeDB'); });
var conn = DbConnection.Builder() .WithUri("https://maincloud.spacetimedb.com") .WithDatabaseName("my_database") .OnConnect((conn, identity, token) => { Console.WriteLine($"Connected! Identity: {identity}"); // 保存 token 用于重连 }) .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_database_name("my_database") .on_connect(|_ctx, _identity, token| { println!("Connected! Saving token..."); // 保存 token 用于重连 }) .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");
// 创建委托 FOnConnectDelegate ConnectDelegate; BIND_DELEGATE_SAFE(ConnectDelegate, this, AMyActor, OnConnected); FOnConnectErrorDelegate ErrorDelegate; BIND_DELEGATE_SAFE(ErrorDelegate, this, AMyActor, OnConnectError); FOnDisconnectDelegate DisconnectDelegate; BIND_DELEGATE_SAFE(DisconnectDelegate, this, AMyActor, OnDisconnected); // 带回调构建连接 UDbConnection* Conn = UDbConnection::Builder() ->WithUri(TEXT("https://maincloud.spacetimedb.com")) ->WithDatabaseName(TEXT("my_database")) ->OnConnect(ConnectDelegate) ->OnConnectError(ErrorDelegate) ->OnDisconnect(DisconnectDelegate) ->Build(); // 回调函数(必须是 UFUNCTION) UFUNCTION() void OnConnected(UDbConnection* Connection, FSpacetimeDBIdentity Identity, const FString& Token) { UE_LOG(LogTemp, Log, TEXT("Connected! Identity: %s"), *Identity.ToHexString()); // 保存 token 用于重连 } UFUNCTION() void OnConnectError(const FString& Error) { UE_LOG(LogTemp, Error, TEXT("Connection failed: %s"), *Error); } UFUNCTION() void OnDisconnected(UDbConnection* Connection, const FString& Error) { UE_LOG(LogTemp, Warning, TEXT("Disconnected from SpacetimeDB: %s"), *Error); }

这些回调在 SDK 内部有精确的触发时机。以 Rust 实现为例(sdks/rust/src/db_connection.rs#L147-L185):

  • 服务器在握手后下发InitialConnectionIdentityToken)消息,SDK 校验并保存 identity 与 connection id 后,将生命周期从Connecting切换为Connected,随后调用on_connect回调(sdks/rust/src/db_connection.rs#L319-L356);
  • 若连接在收到初始消息之前就失败,触发on_connect_error;若在Connected之后中断,则触发on_disconnect,并依次对当前所有订阅调用其on_disconnect,最后把send_chan置为None标记连接结束。

断开连接

使用完毕后显式关闭连接:

conn.disconnect();
conn.Disconnect();
conn.disconnect();
Conn->Disconnect();

重连行为

📌 重连行为说明

底层的DbConnection对象不会自行重连。如果你直接创建了DbConnection且连接中断,需要新建一个DbConnection来重新建立连接。如果你的应用对连接可靠性有要求,官方建议在应用层自行实现重连逻辑。

不过,TypeScript 的 React、Solid 与 Svelte 框架 Provider是个例外:它们通过 SDK 的共享连接管理器来管理连接。在 Provider 挂载期间,该管理器会自动以**指数退避(exponential backoff)**重建意外关闭的连接;并且在页面重新可见、重新获得焦点、网络恢复、或从往返缓存(back-forward cache)恢复时,会重新检查连接存活状态。

这段行为的源码依据在sdks/typescript/src/sdk/connection_manager.ts:指数退避以1000ms 为基数,每次连续失败翻倍,封顶30000msCONNECTION_MANAGER_RECONNECT_BASE_DELAY_MS = 1000CONNECTION_MANAGER_RECONNECT_MAX_DELAY_MS = 30_000,重连延迟 =min(1000 * 2^attempt, 30000));同时管理器在documentwindow上注册了visibilitychange等监听器,页面回到前台时立即推进停滞的重连定时器——这是因为浏览器在后台标签页会暂停定时器,仅靠onclose+setTimeout无法可靠恢复连接。

连接身份: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 与 ConnectionId 章节):

  • Identity:标识与数据库交互的用户,是长期有效、公开、全局有效的标识符,跨连接始终指向同一个终端用户。用户的每个 reducer 调用都会附带其 Identity,可用于权限判断。Identity 由 JWT 的 issuer 与 subject 字段哈希派生(具体伪代码见关键架构文档)。模块自身也拥有 Identity——spacetime publish发布时自动签发。
  • ConnectionId:标识客户端到数据库的单条连接。一个用户只有一个 Identity,但可以对同一数据库打开多条连接,每条连接各获得一个唯一的 ConnectionId。

在 Rust SDK 中,identity 与 connection_id 都存储在DbContextImpl的共享单元中(identity: SharedCell<Option<Identity>>connection_id: SharedCell<Option<ConnectionId>>,见 sdks/rust/src/db_connection.rs#L95-L104),初始为None(匿名连接尚未收到初始连接消息时),收到InitialConnection后才被填充并在on_connect中暴露给用户。SDK 还会断言:若此前已存在 identity/connection id,服务器下发的值必须与之一致(sdks/rust/src/db_connection.rs#L163-L179)。

连接建立后的下一步

连接建立成功之后,就可以开始与数据库交互了:

  • 阅读 SDK API 使用指南,操作表、调用 reducer、订阅数据;
  • 注册回调观察数据库变更(订阅更新、行插入/更新/删除、reducer 调用、procedure 结果);
  • 调用服务器端的 reducer 与 procedure。

各语言的具体 API 细节,可查阅对应语言参考:

  • Rust SDK 参考
  • C# SDK 参考
  • TypeScript SDK 参考
  • Unreal SDK 参考

常见问题速查

  • 客户端收不到任何订阅数据/reducer 回调:C#/Unreal 用户请检查是否在循环或Tick中调用了FrameTick()(或 Unreal 中开启了SetAutoTicking(true));Rust 用户请确认调用了advance_one_message系列方法或run_*系列运行器之一。
  • 连接建立后需要保持用户身份:将on_connect回调收到的 token 持久化(TypeScript 按HOST/DB_NAME为 key 存入localStorage;Rust 可使用credentials::File::save),重连或重启后用with_token传回。
  • 连接意外断开:底层DbConnection不会自动重连,需自行新建连接;若使用 TypeScript 的 React/Solid/Svelte Provider,框架的共享连接管理器已内置指数退避自动重连。
  • URI 怎么写:支持httphttpswswss四种 scheme 或省略 scheme;MainCloud 托管库直接用https://maincloud.spacetimedb.com

【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

立即咨询