拓冰建站拓冰建站
首页 / 资讯中心 / 正文

SpacetimeDB C SDK 内部架构与开发指南:代码生成、线程模型与客户端缓存深度解析

SpacetimeDB C# SDK 内部架构与开发指南代码生成、线程模型与客户端缓存深度解析【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB本篇技术指南以 SpacetimeDB 仓库中 sdks/csharp/DEVELOP.md 为骨架系统讲解 C# 客户端 SDK 的迁移状态、本地开发联调方法、双层代码生成机制、运行时结构与单线程模型并结合 sdks/csharp/src/SpacetimeDBClient.cs、sdks/csharp/src/Table.cs、sdks/csharp/src/MultiDictionary.cs 等源码深入剖析订阅去重、网络协议与回调分发的底层实现。读完你将掌握如何用spacetime generate生成客户端、理解DbConnection.FrameTick()的正确用法以及为什么 C# SDK 要求连接对象只能在单个线程上访问。一、迁移说明SDK 代码去向C# SDK 正在从独立的com.clockworklabs.spacetimedbsdk仓库迁移到 SpacetimeDB 主仓库的 sdks/csharp 子目录。当前规则如下所有新改动都应提交到sdks/csharp子目录旧的com.clockworklabs.spacetimedbsdk仓库仅在发版时同步更新迁移期间可能存在一些尚未打磨的边角sharp edges开发者应以当前仓库为准。二、基于 SpacetimeDB 本地克隆进行开发当需要针对本地 SpacetimeDB 克隆进行联调时必须保证 C# SDK 项目能拿到最新版本的BSATN.Codegen与BSATN.Runtime包——它们源自 SpacetimeDB 仓库内的 crates/bindings-csharp/BSATN.Runtime 等目录而不是 NuGet 上的旧版本。假设本地 SpacetimeDB 克隆位于../SpacetimeDB运行dotnet pack ../SpacetimeDB/crates/bindings-csharp/BSATN.Runtime ./tools~/write-nuget-config.sh ../SpacetimeDB这条命令会做两件事dotnet pack把BSATN.Runtime打成本地包执行 sdks/csharp/tools~/write-nuget-config.sh 写出一个已被.gitignore忽略的NuGet.Config文件使 SDK 项目优先使用本地构建的包而不是 NuGet 上的发布包。查看该脚本源码可以看到它实际生成了两份NuGet.Config一份写入sdks/csharp/NuGet.Config一份写入 SpacetimeDB 克隆根目录。两份配置的核心都是通过packageSourceMapping将SpacetimeDB.BSATN.Runtime与SpacetimeDB.Runtime两个包显式映射到本地 Release 输出目录add keyLocal SpacetimeDB.BSATN.Runtime value${SPACETIMEDB_REPO_PATH}/crates/bindings-csharp/BSATN.Runtime/bin/Release / add keyLocal SpacetimeDB.Runtime value${SPACETIMEDB_REPO_PATH}/crates/bindings-csharp/Runtime/bin/Release /同时保留nuget.org作为*通配回退源确保测试依赖等其余包仍可从公共源解析。这样做的目的是避免在测试时悄悄拉取到过时的 NuGet 版本。注意每当你更新了BSATN.Codegen或BSATN.Runtime都必须重新运行上述命令让本地包重新打包并刷新 NuGet 配置。三、内部架构两层代码生成SDK 的代码生成分为两层职责截然不同。3.1 编译期序列化生成SpacetimeDB.BSATN.CodegenSpacetimeDB.BSATN.Codegen是 SDK 的依赖库其源码位于 SpacetimeDB 仓库的 crates/bindings-csharp。它提供[SpacetimeDB.Type]注解当 C# 编译器遇到该注解时会调用此库为被注解的类型生成 BSATN 序列化代码。关键特性对任何兼容的 C# 类型都有效不涉及任何非 C# 代码生成的代码不会出现在文件系统里由 Roslyn 编译器在内存中完成。如果需要调试SpacetimeDB.BSATN.Codegen生成的代码可以在.csproj的PropertyGroup中设置EmitCompilerGeneratedFilestrue/EmitCompilerGeneratedFiles然后构建项目进入obj/Debug/.../generated目录即可查看 Roslyn 生成的 C# 代码。3.2 网络层客户端生成spacetimedb-codegen第二层由 SpacetimeDB 仓库中的spacetimedb-codegenRust 库源码位于 crates/codegen/srcC# 生成逻辑在其中的csharp.rs负责由spacetime generateCLI 命令调用。它生成的代码负责与 SpacetimeDB 模块通过网络通信这些代码是用户真实可见的直接落在文件系统里而关联的模块可以用任意语言编写不限于 C#。spacetime generate产出的代码import SpacetimeDB SDK并扩展其各类以构成一个完整的 SpacetimeDB 客户端importSpacetimeDB.BSATN.Codegen以满足序列化需求。完整示例参见 templates/chat-console-cs/module_bindings 目录其中SpacetimeDBClient.g.cs见 templates/chat-console-cs/module_bindings/SpacetimeDBClient.g.cs是客户端类的主文件。该文件顶部标注了 THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB并记录了生成时使用的 CLI 版本例如spacetimedb cli version 2.6.0。3.3 DbConnection 与继承模式使用该 SDK 创建的客户端其根对象是一个DbConnection该类位于生成代码SpacetimeDBClient.g.cs中注SpacetimeDBClient是一个历史遗留命名未来可能被淘汰。生成的DbConnection继承自 SDK 中的DbConnectionBase...位于 sdks/csharp/src/SpacetimeDBClient.cs。这是一种通用模式生成的代码实现尽可能少的逻辑把大部分行为留给 SDK。这样更利于升级——改 SDK 代码通常比改生成代码容易得多SDK 需要引用生成类型时有两种选择一是把 SDK 代码做成泛型在生成代码中实例化泛型参数例如DbConnectionBase...SDK泛型→DbConnection生成非泛型二是干脆把代码整体搬进生成代码例如ReducerEventContext已经完全从 SDK 中移出。从 sdks/csharp/src/SpacetimeDBClient.cs 可以看到DbConnectionBaseDbConnection, Tables, Reducer的签名以及DbConnectionBase.Builder()返回DbConnectionBuilderDbConnection的构建器入口。构建器提供WithUri、WithDatabaseName、WithToken、WithCompression、WithLightMode、WithConfirmedReads等链式配置以及OnConnect、OnConnectError、OnDisconnect回调注册。SDK 中最重要的生成类型有两个RemoteTables即客户端缓存 client cache保存从数据库订阅来的数据的本地视图。对DbConnection conn来说conn.Db就是RemoteTables的一个实例。其基类RemoteTablesBase位于 sdks/csharp/src/RemoteTablesBase.cs内部用Dictionarystring, IRemoteTableHandle按远端表名索引各表句柄conn.Db通过AddTable注册每张表RemoteReducers允许在客户端调用服务端 reducer通过conn.Reducers访问。此外生成代码还会为表/模块引用的所有服务端类型生成对应类型。四、运行时结构DbConnectionBase 的核心职责SDK 的大部分核心逻辑集中在 sdks/csharp/src/SpacetimeDBClient.cs 的DbConnectionBase...中它负责启动后台线程与网络通信并解析消息构造函数中创建名为SpacetimeDB Network Thread的解析线程见 SpacetimeDBClient.cs接收更新、维护客户端缓存、触发回调。用户创建一个DbConnection然后通过SubscriptionBuilder创建若干SubscriptionHandle。每个订阅由若干条 SQL 查询组成由远端服务器跟踪。用户也可以直接用该DbConnection调用 reducer。服务端通过WebSocket周期性推送更新。DbConnection负责在后台线程用ParseMessages从_parseQueue取出原始字节、解压并解码为ServerMessage生成ParsedMessage投入_applyQueue在主线程FrameTick()中从_applyQueue取出消息并ApplyMessage更新本地视图conn.Db并调用用户注册的回调。4.1 表句柄与回调机制Codegen 还会为每张表生成实现ITable接口的代码接口定义见 sdks/csharp/src/Table.cs。DbConnection只把表当作ITable看待不了解每张表的具体实现。RemoteTableHandle...位于 Table.cs结合生成代码实现ITable接口内部维护一个MultiDictionaryobject, Row Entries作为行缓存无主键时以整行作为键。它通过OnInternalInsert/OnInternalDelete内部事件维持索引——即 Table.cs 中的UniqueIndexBaseColumnDictionaryColumn, Row提供Find与BTreeIndexBaseColumnDictionaryColumn, HashSetRow提供Filter索引对象在构造时订阅这两个内部事件把行增删同步到自己的字典。用户可见的回调包括持久表的OnInsert、OnDelete、OnBeforeDelete、OnUpdate以及事件表RemoteEventTableHandle仅有的OnInsert。事件表不把行持久化到客户端缓存只触发插入回调。4.2 三阶段应用PreApply / Apply / PostApply一次数据库更新的应用被拆成三阶段见 Table.cs由 SpacetimeDBClient.cs 的ApplyUpdate统一调度PreApply先对所有受影响表调用触发OnBeforeDelete让用户能在行真正被删除前读到旧值Apply把MultiDictionaryDelta应用到Entries并更新索引此阶段不得触发用户回调因为并非所有表都已更新完毕同时完成索引修复为 PostApply 做准备PostApply所有表都 Apply 完成后才真正触发用户的OnInsert/OnUpdate/OnDelete回调。五、线程模型单线程访问约束与 Rust SDK 不同C# SDK假定一个DbConnection只在单个线程上被访问。这个线程被称为主线程即不断循环调用DbConnection.FrameTick()的那个线程。约束非常严格只能从单个线程调用FrameTick()只能从这个线程访问DbConnection。FrameTick()的实现见 SpacetimeDBClient.cs先调用webSocket.Update()驱动网络层然后循环从_applyQueue取出已解析消息并ApplyMessage。为什么这样设计本质上是用主线程自身充当conn.Db上的锁当FrameTick()运行期间conn.Db的状态是未定义的其余任何时候conn.Db都保证处于单一、良构的状态对应服务器在过去某个时刻的状态严格说是因果过去即 SDK 已收到服务器消息其状态对应服务器发该消息前的某个状态——这一表述成立的前提是服务器事务全序目前成立只有在主线程访问RemoteTables时上述保证才成立。从其他线程访问conn.Db可能读到不一致的数据或抛出ConcurrentModificationException。最重要的推论用户永远观察不到部分应用的事务。事务更新是原子的、一次性发生的。如果一个事务修改了多行/多张表用户永远不会看到只应用了其中一部分更新的conn.Db前提是不在后台线程访问它。此外FrameTick()可能会调用用户回调SDK 同样保证在回调执行期间conn.Db处于良构状态。代价是这种设计让 SDK 在多线程场景下难以使用但换来了相对简单的用户心智模型。六、网络协议WebSocket BSATN客户端与服务器通过 WebSocket 通信消息使用BSATNBinary SATS编码。具体消息类型位于SpacetimeDB.ClientApi命名空间源码存放在 sdks/csharp/src/SpacetimeDB/ClientApi 目录——其中的.g.cs文件如ServerMessage.g.cs、ClientMessage.g.cs、TransactionUpdate.g.cs、ReducerResult.g.cs等都是自动生成的。6.1 重新生成 ClientApi该命名空间由一份用 Rust 编写的规范自动生成。重新生成的入口脚本为 sdks/csharp/tools~/gen-client-api.shWindows 对应 sdks/csharp/tools~/gen-client-api.bat。从脚本源码可以看到完整链路cargo build --manifest-path crates/standalone/Cargo.toml先构建 standalone 服务器运行 crates/client-api-messages 的get_ws_schema_v2示例导出 WebSocket v2 协议 schema即仓库根目录下的ws_schema-2.json将该 schema 通过spacetime generate -l csharp --namespace SpacetimeDB.ClientApi生成 C# 消息类型把生成的Types/*移动到 sdks/csharp/src/SpacetimeDB/ClientApi并清理临时目录。6.2 双重编码问题注意消息实际上是双重编码的SpacetimeDB.ClientApi消息内部存有大量byte[]这些字节必须再解码一次才能得到真实的表行、reducer 参数等。文档明确指出这不可避免地涉及大量拷贝This unfortunately involves a lot of copying这是网络路径上值得关注的性能特征。从解析源码看SpacetimeDBClient.csParseMessage先解压解码外层消息CompressionHelpers.DecompressDecodeMessage再按消息类型分别处理SubscribeApplied、TransactionUpdate、ReducerResult、ProcedureResult、OneOffQueryResult等分支把行数据解析成ParsedDatabaseUpdate。七、重叠订阅客户端去重与 MultiDictionary用户可能以多种方式订阅同一行例如同时执行SELECT * FROM students WHERE student.age 5 SELECT * FROM students WHERE student.class 4如果两个查询都订阅服务器会对班级为 4 且年龄大于 5的所有学生发送多份拷贝。理论上可以在服务器端去重但那是一大块工作量因此出于性能考虑去重放在客户端。7.1 MultiDictionary客户端依赖 sdks/csharp/src/MultiDictionary.cs 中的MultiDictionaryTKey, TValue完成去重。它像一个普通字典但可以存储同一 (key, value) 对的多个拷贝内部用DictionaryTKey, (TValue Value, uint Multiplicity)记录每个键的多重度multiplicity同一个键只能映射到同一个值插入不同值属于逻辑错误调试模式下用Debug.Assert校验。值得注意的实现细节见 MultiDictionary.cs它是 struct性能考虑但必须用带两个比较器的构造函数创建默认构造会处于非法状态Count返回含多重度的总数CountDistinct返回不含多重度的键数对于没有主键的表MultiDictionary的键退化为整行对象——这之所以可行是因为任何[SpacetimeDB.Type]都自动具备正确的Equals与哈希实现。MultiDictionaryTests.cs见 sdks/csharp/tests~/MultiDictionaryTests.cs提供了针对其行为的随机化测试。7.2 MultiDictionaryDelta 与后台预处理MultiDictionaryDelta表示对MultiDictionary的一批预处理过的变更。从 MultiDictionary.cs 的注释可以确认它的关键性质它是无冲突复制数据类型CRDT无论 Add/Remove 的调用顺序如何只要每个 (key, value) 的增减次数一致应用结果就相同每个键在 delta 中最多关联两个值旧值 新值超过两个值视为非法应用时 delta 必须维持每个键恰好映射一个值的不变量。架构上SDK在后台线程准备MultiDictionaryDelta在主线程ApplyMultiDictionary.cs从而在不阻塞主线程的前提下完成尽可能多的工作。7.3 依赖的协议保证当多个订阅指向同一行时若服务器端事务更新了该行网络会恰好发送对应份数的更新封装在单个ServerMessage中。MultiDictionary与MultiDictionaryDelta的正确性依赖这一保证——若该保证未被满足调试模式下会抛出异常。也就是说订阅去重机制与服务器每订阅一份就发一份更新的行为是严格耦合的。八、小结与进一步阅读本文梳理的 C# SDK 设计可以用几句话概括双层代码生成BSATN.Codegen负责编译期序列化spacetimedb-codegen负责网络客户端两者由spacetime generate串联单线程心智模型DbConnection只在主线程持续调用FrameTick()的线程上访问换来事务级原子可见性客户端去重重叠订阅通过MultiDictionary/MultiDictionaryDelta在客户端完成多重度记账与服务器单消息多份更新的协议保证严格对应。如果你要继续深入推荐按以下路径阅读仓库源码客户端核心sdks/csharp/src/SpacetimeDBClient.csDbConnectionBase、FrameTick、消息解析与分发表与索引sdks/csharp/src/Table.csITable、RemoteTableHandle、PreApply/Apply/PostApply去重数据结构sdks/csharp/src/MultiDictionary.cs 及随机化测试 sdks/csharp/tests~/MultiDictionaryTests.cs协议消息类型sdks/csharp/src/SpacetimeDB/ClientApi自动生成勿手改生成代码样例templates/chat-console-cs/module_bindingsSpacetimeDBClient.g.cs、RemoteTables等代码生成器 Rust 实现crates/codegen/src以及协议规范与再生成脚本 crates/client-api-messages 与 sdks/csharp/tools~/gen-client-api.sh。【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门