ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

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

SpacetimeDB 客户端连接完全指南:DbConnection 构建器、WebSocket 生命周期与多语言实践 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 的区别。连接前的准备工作在编写任何连接代码之前需要满足以下三个前提条件已为模块生成客户端绑定使用spacetime generate --lang language --out-dir dir --module-path module-dir命令生成类型安全的绑定代码它们镜像了模块的表结构、reducer 与 procedure 签名。生成绑定是连接的前提因为DbConnection的泛型类型来自这些绑定例如 Rust SDK 中DbConnection::builder()的完整签名是DbConnectionBuilderM: SpacetimeModule。一个已发布并运行的数据库可以运行在本地自托管也可以运行在 SpacetimeDB 托管服务 MainCloud 上。注意数据库与模块的区分模块是你编写的代码schema 与业务逻辑数据库是模块的运行实例拥有存储数据和活动连接。数据库的 URI 与名称或 identityURI 指向 SpacetimeDB 主机名称或 identity 用于定位具体数据库。数据库名称必须匹配正则/^[a-z0-9](-[a-z0-9])*$/仅小写 ASCII 字母与数字、以短横线分隔例如my-game-server、chat-app-production、test123每个数据库创建时还会获得唯一的十六进制 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或者使用http、https、ws、wss之一——这是 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作为 URIconst 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其中的email、sub、preferred_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避免凭据混淆。推进连接FrameTickC#/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-L1022public 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_message、run_async、run_background_task或run_threaded之一即可持续推进sdks/rust/src/db_connection.rs#L940-L948TypeScript 则依赖浏览器或 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 1000、CONNECTION_MANAGER_RECONNECT_MAX_DELAY_MS 30_000重连延迟 min(1000 * 2^attempt, 30000)同时管理器在document与window上注册了visibilitychange等监听器页面回到前台时立即推进停滞的重连定时器——这是因为浏览器在后台标签页会暂停定时器仅靠onclosesetTimeout无法可靠恢复连接。连接身份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: SharedCellOptionIdentity、connection_id: SharedCellOptionConnectionId见 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 存入localStorageRust 可使用credentials::File::save重连或重启后用with_token传回。连接意外断开底层DbConnection不会自动重连需自行新建连接若使用 TypeScript 的 React/Solid/Svelte Provider框架的共享连接管理器已内置指数退避自动重连。URI 怎么写支持http、https、ws、wss四种 scheme 或省略 schemeMainCloud 托管库直接用https://maincloud.spacetimedb.com。【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进