建器、WebSocket 生命周期與多語(yǔ)言實(shí)踐)
SpacetimeDB 客戶端連接完全指南DbConnection 構(gòu)建器、WebSocket 生命周期與多語(yǔ)言實(shí)踐【免費(fèi)下載鏈接】SpacetimeDBDevelopment at the speed of light項(xiàng)目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB本篇技術(shù)指南系統(tǒng)講解 SpacetimeDB 客戶端 SDK 的數(shù)據(jù)庫(kù)連接機(jī)制。在完成模塊客戶端綁定生成之后客戶端應(yīng)用通過DbConnection類型建立一條持久化的 WebSocket 連接從而與服務(wù)器進(jìn)行實(shí)時(shí)通信。讀完本文你將掌握如何用各語(yǔ)言 SDK 的構(gòu)建器模式建立連接、如何通過令牌完成身份認(rèn)證、C#/Unreal 客戶端為何必須手動(dòng)推進(jìn)連接FrameTick、如何注冊(cè)連接生命周期回調(diào)、如何優(yōu)雅地?cái)嚅_與重連以及 Identity 與 ConnectionId 的區(qū)別。連接前的準(zhǔn)備工作在編寫任何連接代碼之前需要滿足以下三個(gè)前提條件已為模塊生成客戶端綁定使用spacetime generate --lang language --out-dir dir --module-path module-dir命令生成類型安全的綁定代碼它們鏡像了模塊的表結(jié)構(gòu)、reducer 與 procedure 簽名。生成綁定是連接的前提因?yàn)镈bConnection的泛型類型來自這些綁定例如 Rust SDK 中DbConnection::builder()的完整簽名是DbConnectionBuilderM: SpacetimeModule。一個(gè)已發(fā)布并運(yùn)行的數(shù)據(jù)庫(kù)可以運(yùn)行在本地自托管也可以運(yùn)行在 SpacetimeDB 托管服務(wù) MainCloud 上。注意數(shù)據(jù)庫(kù)與模塊的區(qū)分模塊是你編寫的代碼schema 與業(yè)務(wù)邏輯數(shù)據(jù)庫(kù)是模塊的運(yùn)行實(shí)例擁有存儲(chǔ)數(shù)據(jù)和活動(dòng)連接。數(shù)據(jù)庫(kù)的 URI 與名稱或 identityURI 指向 SpacetimeDB 主機(jī)名稱或 identity 用于定位具體數(shù)據(jù)庫(kù)。數(shù)據(jù)庫(kù)名稱必須匹配正則/^[a-z0-9](-[a-z0-9])*$/僅小寫 ASCII 字母與數(shù)字、以短橫線分隔例如my-game-server、chat-app-production、test123每個(gè)數(shù)據(jù)庫(kù)創(chuàng)建時(shí)還會(huì)獲得唯一的十六進(jìn)制 identity客戶端可以用名稱或identity 連接?;具B接構(gòu)建器模式所有語(yǔ)言 SDK 都采用同構(gòu)的構(gòu)建器builder模式來創(chuàng)建連接。核心構(gòu)建步驟是設(shè)置 URI 和數(shù)據(jù)庫(kù)名然后調(diào)用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();使用時(shí)將https://maincloud.spacetimedb.com替換為你自己的 SpacetimeDB 主機(jī) URI將my_database替換為你的數(shù)據(jù)庫(kù)名稱或 identity。構(gòu)建器背后的實(shí)現(xiàn)細(xì)節(jié)從源碼角度Rust SDK 的構(gòu)建器在sdks/rust/src/db_connection.rs中提供了語(yǔ)義明確的鏈?zhǔn)椒椒╳ith_uri(...)sdks/rust/src/db_connection.rs#L1060-L1064設(shè)置遠(yuǎn)端數(shù)據(jù)庫(kù)所在主機(jī)的 URI。源碼注釋明確規(guī)定URI必須不帶 scheme或者使用http、https、ws、wss之一——這是 WebSocket 協(xié)議升級(jí)的合法來源傳非法 URI 時(shí)build會(huì)直接 panic。with_database_name(...)sdks/rust/src/db_connection.rs#L1067-L1070接收數(shù)據(jù)庫(kù)的名稱或 identity字符串。with_token(...)sdks/rust/src/db_connection.rs#L1083-L1086攜帶 OIDC 兼容的 JWT 令牌若不調(diào)用或傳None主機(jī)將生成一個(gè)全新的匿名 Identity詳見下文“令牌認(rèn)證”一節(jié)。with_compression(...)sdks/rust/src/db_connection.rs#L1093-L1096設(shè)置消息壓縮策略。當(dāng)前主機(jī)在整條服務(wù)器消息或單個(gè)查詢更新超過1KiB閾值時(shí)啟用壓縮注意該閾值不保證不變。with_confirmed_reads(...)開啟確認(rèn)讀后服務(wù)器只在查詢結(jié)果確認(rèn)持久化后才下發(fā)——單節(jié)點(diǎn)服務(wù)器以fsync落盤為持久化標(biāo)準(zhǔn)集群則以足夠數(shù)量副本確認(rèn)存儲(chǔ)為標(biāo)準(zhǔn)代價(jià)是 reducer 調(diào)用到訂閱更新到達(dá)之間的延遲增加。在底層build()sdks/rust/src/db_connection.rs#L951-L954會(huì)完成解析 URI → 建立 WebSocket 連接WsConnection::connect將 URI、數(shù)據(jù)庫(kù)名、令牌一起傳給握手階段→ 啟動(dòng)消息循環(huán)線程 → 啟動(dòng)parse_loop解析線程 → 創(chuàng)建空客戶端緩存 → 組裝DbContextImpl連接上下文。在瀏覽器wasm目標(biāo)下build是異步方法sdks/rust/src/db_connection.rs#L956-L960因?yàn)?WebSocket 握手需要異步等待。連接 MainCloud 托管數(shù)據(jù)庫(kù)如果你的數(shù)據(jù)庫(kù)托管在 SpacetimeDB 官方托管服務(wù) MainCloud只需使用官方主機(jī)地址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(...)。將模塊發(fā)布到 MainCloud 使用spacetime publish my-database --server maincloud詳見 MainCloud 部署指南發(fā)布后同樣用https://maincloud.spacetimedb.com作為客戶端連接主機(jī)。使用令牌進(jìn)行身份認(rèn)證SpacetimeDB 的身份體系基于 OpenID Connect。當(dāng)應(yīng)用需要區(qū)分用戶身份例如權(quán)限控制、跨連接保持同一用戶時(shí)可以在構(gòu)建連接時(shí)通過令牌認(rèn)證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();令牌在連接握手期間發(fā)送給服務(wù)器用于校驗(yàn)?zāi)愕纳矸荨+@取和管理令牌的完整流程參見 SpacetimeAuth 文檔SpacetimeAuth 是官方身份提供方認(rèn)證流程結(jié)束時(shí)應(yīng)用會(huì)收到一個(gè)ID token一個(gè) OIDC 兼容的 JWT其中的email、sub、preferred_username等 claims 描述了用戶信息應(yīng)用即可用該 token 配合任意 SpacetimeDB SDK 進(jìn)行認(rèn)證。也可以使用任何其他 OIDC 兼容的身份提供方簽發(fā)的令牌。關(guān)于令牌的底層行為Rust SDK 源碼給出了三條明確語(yǔ)義若不調(diào)用with_token或傳入None主機(jī)將為本次連接生成一個(gè)匿名 Identity若令牌在連接上下文創(chuàng)建之前就被服務(wù)器拒絕build()直接返回錯(cuò)誤若拒絕發(fā)生在 WebSocket 已建立、但初始連接消息尚未到達(dá)之間則會(huì)觸發(fā)on_connect_error回調(diào)。另外Rust SDK 提供了現(xiàn)成的憑據(jù)落盤工具sdks/rust/src/credentials.rscredentials::File::new(my_app)可以在用戶主目錄的.spacetimedb_client_credentials目錄下用 BSATN 序列化保存 JWT。典型用法是在on_connect回調(diào)中調(diào)用credentials::File::new(my_app).save(token)保存服務(wù)端下發(fā)的令牌下次啟動(dòng)時(shí)用File::load()取回——官方推薦的持久化身份路徑。若連接多個(gè)集群建議為每個(gè)集群使用獨(dú)立的 key避免憑據(jù)混淆。推進(jìn)連接FrameTickC#/Unreal 的必修課?? 關(guān)鍵提示C#含 Unity與 Unreal 用戶必讀在 C#包括 Unity中你必須手動(dòng)推進(jìn)連接才能處理入站消息在 Unreal Engine 中必須手動(dòng)推進(jìn)連接或者開啟自動(dòng) tick。如果不推進(jìn)連接客戶端將收不到任何消息——包括訂閱數(shù)據(jù)、reducer 回調(diào)、連接事件全部不會(huì)觸發(fā)。在游戲循環(huán)或 update 方法中調(diào)用FrameTick()// Unity 中在 Update() 里調(diào)用 void Update() { conn.FrameTick(); } // 控制臺(tái)應(yīng)用則在主循環(huán)中調(diào)用 while (running) { conn.FrameTick(); // 你的應(yīng)用邏輯... }// 方案 1在 Actor 的 Tick() 中調(diào)用 FrameTick() void AMyActor::Tick(float DeltaTime) { Super::Tick(DeltaTime); if (Conn) { Conn-FrameTick(); } } // 方案 2構(gòu)建連接后開啟自動(dòng) tick只需一次 Conn Builder-Build(); Conn-SetAutoTicking(true);FrameTick的底層實(shí)現(xiàn)可以從 C# SDK 源碼中直接印證sdks/csharp/src/SpacetimeDBClient.cs#L1015-L1022public void FrameTick() { webSocket.Update(); // 1. 推進(jìn)底層 WebSocket收發(fā)消息 while (_applyQueue.TryTake(out var parsedMessage)) { ApplyMessage(parsedMessage); // 2. 將隊(duì)列中的解析消息應(yīng)用到客戶端緩存并觸發(fā)回調(diào) } }可以看到FrameTick做兩件事驅(qū)動(dòng)底層 WebSocket 的收發(fā)狀態(tài)機(jī)然后逐一取出已解析的消息隊(duì)列并應(yīng)用——即把服務(wù)器下發(fā)的 diff 寫入客戶端緩存、觸發(fā)訂閱/行/連接相關(guān)回調(diào)。因此漏調(diào)FrameTick等同于整個(gè)消息處理管線停擺。Rust 與 TypeScript 則完全不需要手動(dòng)輪詢Rust SDK 在build時(shí)于后臺(tái) Tokio 運(yùn)行時(shí)中啟動(dòng) WebSocket 消息循環(huán)與parse_loop解析線程應(yīng)用只需在業(yè)務(wù)層調(diào)用advance_one_message、run_async、run_background_task或run_threaded之一即可持續(xù)推進(jìn)sdks/rust/src/db_connection.rs#L940-L948TypeScript 則依賴瀏覽器或 Node.js 的事件循環(huán)自動(dòng)處理消息。兩種語(yǔ)言的事件驅(qū)動(dòng)模型天然承擔(dān)了“推進(jìn)連接”的職責(zé)。連接生命周期連接回調(diào)觀察連接狀態(tài)變化通過構(gòu)建器注冊(cè)回調(diào)可以觀察連接的建立、失敗與斷開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 用于重連——按 服務(wù)器/數(shù)據(jù)庫(kù) 分別存儲(chǔ) 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);// 創(chuàng)建委托 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); // 帶回調(diào)構(gòu)建連接 UDbConnection* Conn UDbConnection::Builder() -WithUri(TEXT(https://maincloud.spacetimedb.com)) -WithDatabaseName(TEXT(my_database)) -OnConnect(ConnectDelegate) -OnConnectError(ErrorDelegate) -OnDisconnect(DisconnectDelegate) -Build(); // 回調(diào)函數(shù)必須是 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); }這些回調(diào)在 SDK 內(nèi)部有精確的觸發(fā)時(shí)機(jī)。以 Rust 實(shí)現(xiàn)為例sdks/rust/src/db_connection.rs#L147-L185服務(wù)器在握手后下發(fā)InitialConnectionIdentityToken消息SDK 校驗(yàn)并保存 identity 與 connection id 后將生命周期從Connecting切換為Connected隨后調(diào)用on_connect回調(diào)sdks/rust/src/db_connection.rs#L319-L356若連接在收到初始消息之前就失敗觸發(fā)on_connect_error若在Connected之后中斷則觸發(fā)on_disconnect并依次對(duì)當(dāng)前所有訂閱調(diào)用其on_disconnect最后把send_chan置為None標(biāo)記連接結(jié)束。斷開連接使用完畢后顯式關(guān)閉連接conn.disconnect();conn.Disconnect();conn.disconnect();Conn-Disconnect();重連行為 重連行為說明底層的DbConnection對(duì)象不會(huì)自行重連。如果你直接創(chuàng)建了DbConnection且連接中斷需要新建一個(gè)DbConnection來重新建立連接。如果你的應(yīng)用對(duì)連接可靠性有要求官方建議在應(yīng)用層自行實(shí)現(xiàn)重連邏輯。不過TypeScript 的 React、Solid 與 Svelte 框架 Provider是個(gè)例外它們通過 SDK 的共享連接管理器來管理連接。在 Provider 掛載期間該管理器會(huì)自動(dòng)以**指數(shù)退避exponential backoff**重建意外關(guān)閉的連接并且在頁(yè)面重新可見、重新獲得焦點(diǎn)、網(wǎng)絡(luò)恢復(fù)、或從往返緩存back-forward cache恢復(fù)時(shí)會(huì)重新檢查連接存活狀態(tài)。這段行為的源碼依據(jù)在sdks/typescript/src/sdk/connection_manager.ts指數(shù)退避以1000ms 為基數(shù)每次連續(xù)失敗翻倍封頂30000msCONNECTION_MANAGER_RECONNECT_BASE_DELAY_MS 1000、CONNECTION_MANAGER_RECONNECT_MAX_DELAY_MS 30_000重連延遲 min(1000 * 2^attempt, 30000)同時(shí)管理器在document與window上注冊(cè)了visibilitychange等監(jiān)聽器頁(yè)面回到前臺(tái)時(shí)立即推進(jìn)停滯的重連定時(shí)器——這是因?yàn)闉g覽器在后臺(tái)標(biāo)簽頁(yè)會(huì)暫停定時(shí)器僅靠onclosesetTimeout無法可靠恢復(fù)連接。連接身份Identity 與 ConnectionId每條連接都會(huì)從服務(wù)器獲得一個(gè)唯一的Identity通過on_connect回調(diào)訪問.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()); }兩者的區(qū)別詳見核心架構(gòu)文檔中的 Identity 與 ConnectionId 章節(jié)Identity標(biāo)識(shí)與數(shù)據(jù)庫(kù)交互的用戶是長(zhǎng)期有效、公開、全局有效的標(biāo)識(shí)符跨連接始終指向同一個(gè)終端用戶。用戶的每個(gè) reducer 調(diào)用都會(huì)附帶其 Identity可用于權(quán)限判斷。Identity 由 JWT 的 issuer 與 subject 字段哈希派生具體偽代碼見關(guān)鍵架構(gòu)文檔。模塊自身也擁有 Identity——spacetime publish發(fā)布時(shí)自動(dòng)簽發(fā)。ConnectionId標(biāo)識(shí)客戶端到數(shù)據(jù)庫(kù)的單條連接。一個(gè)用戶只有一個(gè) Identity但可以對(duì)同一數(shù)據(jù)庫(kù)打開多條連接每條連接各獲得一個(gè)唯一的 ConnectionId。在 Rust SDK 中identity 與 connection_id 都存儲(chǔ)在DbContextImpl的共享單元中identity: SharedCellOptionIdentity、connection_id: SharedCellOptionConnectionId見 sdks/rust/src/db_connection.rs#L95-L104初始為None匿名連接尚未收到初始連接消息時(shí)收到InitialConnection后才被填充并在on_connect中暴露給用戶。SDK 還會(huì)斷言若此前已存在 identity/connection id服務(wù)器下發(fā)的值必須與之一致sdks/rust/src/db_connection.rs#L163-L179。連接建立后的下一步連接建立成功之后就可以開始與數(shù)據(jù)庫(kù)交互了閱讀 SDK API 使用指南操作表、調(diào)用 reducer、訂閱數(shù)據(jù)注冊(cè)回調(diào)觀察數(shù)據(jù)庫(kù)變更訂閱更新、行插入/更新/刪除、reducer 調(diào)用、procedure 結(jié)果調(diào)用服務(wù)器端的 reducer 與 procedure。各語(yǔ)言的具體 API 細(xì)節(jié)可查閱對(duì)應(yīng)語(yǔ)言參考Rust SDK 參考C# SDK 參考TypeScript SDK 參考Unreal SDK 參考常見問題速查客戶端收不到任何訂閱數(shù)據(jù)/reducer 回調(diào)C#/Unreal 用戶請(qǐng)檢查是否在循環(huán)或Tick中調(diào)用了FrameTick()或 Unreal 中開啟了SetAutoTicking(true)Rust 用戶請(qǐng)確認(rèn)調(diào)用了advance_one_message系列方法或run_*系列運(yùn)行器之一。連接建立后需要保持用戶身份將on_connect回調(diào)收到的 token 持久化TypeScript 按HOST/DB_NAME為 key 存入localStorageRust 可使用credentials::File::save重連或重啟后用with_token傳回。連接意外斷開底層DbConnection不會(huì)自動(dòng)重連需自行新建連接若使用 TypeScript 的 React/Solid/Svelte Provider框架的共享連接管理器已內(nèi)置指數(shù)退避自動(dòng)重連。URI 怎么寫支持http、https、ws、wss四種 scheme 或省略 schemeMainCloud 托管庫(kù)直接用https://maincloud.spacetimedb.com。【免費(fèi)下載鏈接】SpacetimeDBDevelopment at the speed of light項(xiàng)目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考