ET 框架中的 MongoBson 序列化库:一套方案打通配置、日志、网络消息与数据库存储
ET 框架中的 MongoBson 序列化库一套方案打通配置、日志、网络消息与数据库存储【免费下载链接】ETUnity3D Client And C# Server Framework项目地址: https://gitcode.com/GitHub_Trending/et/ET导读本文围绕 ET 框架官方文档《强大的MongoBson库》展开系统讲解 MongoDB 官方 C# 驱动自带的 BSON/JSON 序列化能力以及它在 ET 服务端五大典型场景对象克隆、数据库存储、跨进程消息、后端日志、配置文件中的统一运用。读完本文你将掌握 MongoBson 的字段控制、版本兼容、复杂继承反序列化与 ISupportInitialize 扩展机制并能结合 MongoHelper.cs 与 MongoRegister.cs 的源码理解 ET 如何把这一库落成可直接复用的基础设施。一、为什么服务端需要一个库满足所有序列化场景后端开发中序列化几乎无处不在。归纳起来主要有五类场景对象克隆通过序列化再反序列化实现深拷贝服务端数据库存储数据需要二进制格式分布式服务端多进程间消息需要二进制格式后端日志需要可读的文本格式服务端各种配置文件需要可读的文本格式。C# 序列化库虽多却各有短板protobuf不支持复杂对象结构无法使用继承适合做消息但不适合数据库存储和日志格式json适合日志格式但作为网络消息和数据存储体积太大。而 MongoBson 恰恰能同时满足以上所有场景某天配置文件需要放进数据库无需格式转换后端直接把前端发来的配置消息存入数据库显著减少转换引入的错误某天服务端配置文件不再用文件格式、改存数据库只需几行代码即可完成迁移某天后端服务器 crash需要扫描日志做数据恢复时把日志反序列化成 C# 对象逐条处理后转成对象保存进数据库即可存入数据库的对象可以直接看到文本内容可执行各类类 SQL 操作一个配置文本对象反序列化进内存、经网络消息发送、再存进数据库整个过程一气呵成。一句话总结减少各种数据转换、减少代码、提高开发效率、提高可维护性。MongoBson 既支持序列化成可读文本MongoDB 扩展 JSON也支持 BSON 二进制格式而 MongoDB 本身又是游戏行业使用极多的数据库天然契合游戏后端技术栈。二、MongoBson 的能力清单MongoBson 库非常完善以下能力是它在 ET 中得以担当统一序列化方案的根基支持复杂的继承结构支持忽略某些字段序列化支持字段默认值结构多出多余字段照样可以反序列化对多版本协议非常有用支持ISupportInitialize接口反序列化时堪称神器支持文本 json 和二进制 bson 两种序列化原生支持 MongoDB 数据库读写。三、基础用法序列化 / 反序列化成 json 或 bson3.1 一个普通的 Player 类public sealed class Player { public long Id; public string Account { get; private set; } public long UnitId { get; set; } } Player player1 new Player() { Id 1 }; string json player1.ToJson(); Console.WriteLine($player1 to json: {json}); Console.WriteLine($player to bson: {player1.ToBson().ToHex()});输出结果player to json: { _id : NumberLong(1), C : [], Account : null, UnitId : NumberLong(0) } player to bson: B000000125F69640001000000000000000A4163636F756E740012556E6974496400000000000000000000两点说明BSON 的十六进制表示ToHex()便于排查与调试实际传输与存储可直接使用ToBson()返回的byte[]默认输出的是 MongoDB 扩展 JSON与标准 JSON 略有差异如NumberLong(1)、_id字段。若需要标准 JSON可传入JsonWriterSettings并指定JsonOutputMode.StrictPlayer player2 new Player() { Id 1 }; Console.WriteLine($player to json: {player2.ToJson(new JsonWriterSettings() { OutputMode JsonOutputMode.Strict })}); // player to json: { _id : 1, C : [], Account : null, UnitId : 0 }ET 的 MongoHelper.cs 正是围绕这一机制做了封装默认使用JsonOutputMode.RelaxedExtendedJson同时提供面向配置文件的ConfigSettings带\t缩进、\n换行、JsonOutputMode.Shell模式保证配置文件导出后具备良好的可读性。3.2 反序列化 jsonPlayer player11 BsonSerializer.DeserializePlayer(json); Console.WriteLine($player11 to json: {player11.ToJson()});3.3 反序列化 bsonusing (MemoryStream memoryStream new MemoryStream(bson)) { Player player12 (Player) BsonSerializer.Deserialize(memoryStream, typeof(Player)); Console.WriteLine($player12 to json: {player12.ToJson()}); }3.4 ET 对 BsonSerializer 的统一封装在 MongoHelper.cs 中ET 将上述 API 收敛为若干静态方法并统一附加错误信息与ISupportInitialize前置处理ToJson(object)/ToJson(object, JsonWriterSettings)序列化为文本序列化前若对象实现了ISupportInitialize会先调用BeginInit()见 MongoHelper.csFromJsonT(string)/FromJson(Type, string)从文本反序列化Serialize(object)/Serialize(object, MemoryStream)序列化为 BSON 字节流式版本使用BsonBinaryWriterDeserialize(Type, byte[], ...)系列从字节或流反序列化CloneT(T)序列化 反序列化一步完成深拷贝见 MongoHelper.cs这正是文档对象通过序列化反序列化 clone场景的直接落点。所有方法都包裹了try/catch失败时抛出带类型名与上下文信息的异常极大方便了线上问题定位。四、字段级控制忽略字段与别名4.1 [BsonIgnore]禁止字段序列化某些字段只在内存中使用如缓存、计算属性不应写入数据库或日志可用[BsonIgnore]跳过public sealed class Player { public long Id; [BsonIgnore] public string Account { get; private set; } public long UnitId { get; set; } } Player player new Player() { Id 2, UnitId 3, Account panda }; Console.WriteLine($player to json: {player.ToJson()}); // player to json: { _id : 2, UnitId : 3 }4.2 [BsonElement]私有字段序列化与字段别名默认情况下只有 public 字段会被序列化。给字段加上[BsonElement]后即使是 private 字段也会参与序列化该标签还可以带一个 string 参数为字段指定序列化别名便于与既有数据格式兼容或缩短字段名public sealed class Player { public long Id; public string Account { get; private set; } [BsonElement(UId)] public long UnitId { get; set; } } Player player new Player() { Id 2, UnitId 3, Account panda }; Console.WriteLine($player to json: {player.ToJson()}); // player to json: { _id : 2, Account : panda, UId : 3 }注意上面两个例子的差别带[BsonIgnore]时Account被剔除带[BsonElement(UId)]时字段名被改写为UId。两者组合使用可以精确控制每个字段的参与与否与落盘命名。4.3 默认值处理MongoBson 支持字段默认值public long UnitId { get; set; }未赋值时序列化输出为NumberLong(0)严格模式为0string 类型默认为null。这保证了未初始化对象也能正常序列化不会因字段缺失而出错。五、版本升级兼容多余字段照样反序列化服务端协议迭代很快新版本客户端/服务端会不断给消息和配置增加字段。若旧版本结构遇到多出来的字段直接抛异常将导致部署与热更困难。[BsonIgnoreExtraElements]用在 class 上反序列化时忽略多余字段保证低版本结构也能反序列化高版本数据[BsonIgnoreExtraElements] public sealed class Player { public long Id; public string Account { get; private set; } [BsonElement(UId)] public long UnitId { get; set; } }ET 在 MongoRegister.cs 中更进一步通过 Convention 机制为所有类型全局注册了IgnoreExtraElementsConvention(true)ConventionPack conventionPack new() { new IgnoreExtraElementsConvention(true) }; ConventionRegistry.Register(IgnoreExtraElements, conventionPack, type true);这意味着 ET 项目中所有参与 MongoBson 序列化的类都自动获得容忍多余字段的能力开发者无需逐个类打标签为多版本协议兼容提供了全局兜底。六、复杂继承结构反序列化时的多态支持MongoBson 最强大的地方在于完全支持继承结构的序列化与反序列化。父类引用如Object、Component反序列化时能根据数据中的判别符还原成真实的子类实例。但注意继承反序列化要求所有父类/子类都被注册到驱动中有两种注册方式。6.1 方式一[BsonKnownTypes] 声明式注册在父类上用[BsonKnownTypes(typeof(...))]显式声明其子类mongo 驱动会自动完成注册[BsonKnownTypes(typeof(Entity))] public class Component { } [BsonKnownTypes(typeof(Player))] public class Entity : Component { } public sealed class Player : Entity { public long Id; public string Account { get; set; } public long UnitId { get; set; } }这种方式有明显缺陷框架并不知道一个类未来会有哪些子类每增加一个子类都要回头修改父类对框架代码有侵入性也破坏了模块间解耦。6.2 方式二程序集扫描自动化注册ET 采用更优雅的做法是扫描程序集中所有子类将它们的类型注册到 mongo 驱动Type[] types typeof(Game).Assembly.GetTypes(); foreach (Type type in types) { if (!type.IsSubclassOf(typeof(Component))) { continue; } BsonClassMap.LookupClassMap(type); } BsonSerializer.RegisterSerializer(new EnumSerializerNumericType(BsonType.String));这样完全自动化注册使用者无需关心类是否注册。最后一行还演示了枚举的序列化策略将NumericType这类枚举以字符串而非数字形式序列化增强数据可读性与前后兼容性。ET 在 MongoRegister.cs 中实现了同类逻辑RegisterClass遍历当前域内所有程序集UNITY_EDITOR下遍历AppDomain.CurrentDomain.GetAssemblies()运行时则通过CodeTypes.Instance.GetTypes()对所有继承自Object的非泛型类执行BsonClassMap.AutoMap()并注册。值得注意的两点实现细节通过cm.SetDiscriminator(type.FullName)设置全限定名判别符从源码结构看这是为了在多层继承如Player : Entity : Component场景下反序列化时能精确区分具体子类使用BsonClassMap.IsClassMapRegistered(type)做幂等保护避免重复注册同时为ObjectSerializer、ComponentsCollection、ChildrenCollection等 ET 核心类型注册了专用序列化器见 BsonComponentsCollectionSerializer.cs支撑 Entity-Component 体系的序列化。七、ISupportInitialize反序列化的钩子神器7.1 接口语义MongoBson 反序列化时支持ISupportInitialize接口它只有两个方法public interface ISupportInitialize { void BeginInit(); void EndInit(); }BeginInit在反序列化前调用EndInit在反序列化后调用。这意味着你可以在反序列化完成后立即执行派生字段计算、格式转换、数据校验等操作无需在业务代码里到处补救。7.2 实战字符串地址转 IPEndPointET 中进程内网地址配置InnerConfig是典型用例。IPEndPoint不适合直接写在配置里于是配置成字符串反序列化时在EndInit中转换[BsonIgnoreExtraElements] public class InnerConfig : AConfigComponent { [BsonIgnore] public IPEndPoint IPEndPoint { get; private set; } public string Address { get; set; } public override void EndInit() { this.IPEndPoint NetworkHelper.ToIPEndPoint(this.Address); } }要点IPEndPoint字段打[BsonIgnore]不参与序列化它是派生数据Address字符串可配置、可读性强是唯一落盘字段EndInit在反序列化结束后自动把Address解析为IPEndPoint业务侧拿到的永远是已就绪的对象。类似的模式在 StartProcessConfig.cs 中同样可见Luban 生成的配置类通过partial void EndInit()预留钩子StartProcessConfigExtensions中的GetInnerIPInnerPort再借助NetworkHelper.ToIPEndPoint组合出内网通信地址见 StartProcessConfig.cs可见字符串配置 EndInit 转换是 ET 配置体系里反复出现的设计。7.3 MongoHelper 中的呼应ET 在序列化端也保持了对称处理MongoHelper.ToJson与Serialize在真正序列化前会先调用BeginInit()见 MongoHelper.cs确保对象状态在写出前已初始化到位读写两端的钩子语义完整闭环。八、扩展思路给 protobuf 消息也加上 EndInitET 的 protobuf 由于要支持 ilruntimeHybridCLR 热更去掉了 map 支持。假如网络消息里需要一个Dictionary映射怎么办ET 的做法是把 proto 生成的消息类改为 partial class让开发者自行扩展。以进图消息为例proto 定义如下message UnitInfo { int64 UnitId 1; float X 2; float Y 3; float Z 4; } // protobuf message G2C_EnterMap // IResponse { int32 RpcId 90; int32 Error 91; string Message 92; // 自己的unit id int64 UnitId 1; // 所有的unit repeated UnitInfo Units 2; }repeated UnitInfo Units在 protobuf 中实际是数组按 UnitId 查某个 Unit 时需要遍历。借助ISupportInitialize在 partial class 中扩展一个字典反序列化完成后自动构建索引public partial class G2C_EnterMap : ISupportInitialize { public DictionaryInt64, UnitInfo unitsDict new Dictionarylong, UnitInfo(); public void BeginInit() { } public void EndInit() { foreach (var unit in this.Units) { this.unitsDict.Add(unit.UnitId, unit); } } }通过这样一段代码把消息扩展一下反序列化出来后即自动得到一个Dictionarylong, UnitInfo业务代码可以直接unitsDict[unitId]查询而无需每次都线性遍历。这正是文档提到的同样给 protobuf 反序列化方法也加上了这个调用的工程实践——把ISupportInitialize这一通用模式从 MongoBson 平移到 protobuf让所有反序列化路径拥有统一的后处理钩子。九、使用注意事项与总结结合上文与仓库源码使用 MongoBson 时有几点值得留意JSON 模式选择默认输出为 MongoDB 扩展 JSONRelaxedExtendedJson面向标准 JSON 消费方请显式指定JsonOutputMode.Strict面向人类可读的配置文件可使用带缩进的Shell模式对应 ET 的ConfigSettings继承多态必须注册继承结构反序列化依赖类映射注册ET 已通过MongoRegister.Init自动完成业务侧只需保证类型继承自Object或Component且非泛型自定义扩展类型时可参照 MongoRegister.cs 的RegisterClass逻辑补注册版本兼容有全局兜底IgnoreExtraElementsConvention(true)让所有类型默认容忍多余字段老结构读新数据不会崩善用 ISupportInitialize凡是反序列化后需要派生出新字段/索引/转换的场景优先考虑BeginInit/EndInit钩子而不是在业务层补初始化统一入口走 MongoHelperET 建议通过 MongoHelper.cs 的静态方法做序列化统一获得异常包装与BeginInit预处理避免各处直接散用BsonSerializer导致行为不一致。总而言之MongoBson 用一个库同时覆盖了 ET 后端的对象克隆、数据库存储、跨进程消息、日志与配置文件五大场景二进制BSON保证体积与性能文本扩展 JSON保证可读与运维继承与多版本兼容保证协议演进安全ISupportInitialize则提供了强大的反序列化后处理能力。理解并善用这套基础设施是开发 ET 服务端模块时写出少转换、少代码、高可维护代码的关键一步。【免费下载链接】ETUnity3D Client And C# Server Framework项目地址: https://gitcode.com/GitHub_Trending/et/ET创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考