
ET 框架 FiberId 统一编码设计解析32 bit Zone 32 bit LocalSlot 的 long 编码与测试验证【免费下载链接】ETUnity3D Client And C# Server Framework项目地址: https://gitcode.com/GitHub_Trending/et/ET导读本文围绕cn.etetet.core包中的 FiberId 统一编码测试计划Test.md展开系统讲解 ET 框架如何将「Zone区 LocalSlot区内槽位」压缩进一个 64 位long的 FiberId以及Fiber.Zone、Fiber.LocalSlot、FiberIdHelper编解码、localSlot 分配区间与内网消息头FiberInstanceId的落地实现。读完本文你将掌握 FiberId 的编码格式、分配策略、边界约束以及三个核心测试用例Core_Fiber_IdEncoding_Test、Core_Fiber_IdEncoding32_Test、Core_FiberInstanceIdHeader_Test的验证目标与运行方式可直接对照源码验证 ET 的纤程身份体系。为什么需要统一编码从 52 bit 旧编码到 long 新编码ET 的纤程Fiber是轻量级调度单元每个纤程都需要一个全局唯一、可路由的身份标识FiberId。在旧设计中FiberId 使用12 bit zone 20 bit localSlot的紧凑布局总共 32 bit天然落在int范围内但也因此存在两个明显瓶颈zone 最多支持2^12 - 1 4095个区每个 zone 内的 localSlot 最多支持2^20 - 1 1048575个槽位。测试计划中明确给出的目标见 Test.md是Fiber.Zone必须从Fiber.Id解码得到显式 zone 创建入口统一为CreateFiber(zone, ...)Fiber.LocalSlot必须从Fiber.Id解码得到FiberIdHelper必须按32 bit zone 32 bit localSlot编码到long同一 zone 下 Fiber 销毁后再次创建不能复用旧FiberId。也就是说新的统一编码把 zone 与 localSlot 各分配 32 bit共 64 bit编码结果存放在long中从而把 zone 上限提升到int.MaxValuelocalSlot 上限同样提升到int.MaxValue彻底打破旧编码 4095 区 / 100 万槽位的限制。这正是Core_Fiber_IdEncoding32_Test要使用「大于旧 12 bit zone 上限」与「大于旧 20 bit localSlot 上限」的输入做验证的原因。FiberIdHelper编解码核心与常量约束编码与解码的完整实现在 FiberIdHelper.cs这是一个静态工具类定义了整套位布局常量值含义ZoneBits32zone 占用的位数高 32 位LocalSlotBits32localSlot 占用的位数低 32 位MaxZoneint.MaxValuezone 合法最大值MaxLocalSlotint.MaxValuelocalSlot 合法最大值ConfigLocalSlotMax99999配置型固定localSlot 上限DynamicLocalSlotStart100000动态分配 localSlot 起点ReservedLocalSlotStartMaxLocalSlot - 15保留 localSlot 区间起点Encode32 32 位打包public static long Encode(int zone, int localSlot) { ValidateZone(zone); ValidateLocalSlot(localSlot); ulong encoded ((ulong)(uint)zone LocalSlotBits) | (uint)localSlot; return unchecked((long)encoded); }编码公式为zone 32 | localSlot先把 zone 无符号化后左移 32 位放入高 32 位再把 localSlot 无符号化后放进低 32 位最后以unchecked方式整体解释为long。注意这里的关键点只要zone 1编码结果必然 int.MaxValue这就是Core_Fiber_IdEncoding32_Test中「编码结果超过int.MaxValue」的数学依据低 32 位用(uint)localSlot转换因此int.MaxValue作为 localSlot 时仍能完整表达不会因符号位丢失。Decode位运算还原public static int DecodeZone(long fiberId) { return (int)((ulong)fiberId LocalSlotBits); } public static int DecodeLocalSlot(long fiberId) { return (int)((ulong)fiberId uint.MaxValue); }解码是编码的逆运算fiberId 32取高 32 位得到 zonefiberId 0xFFFFFFFF取低 32 位得到 localSlot。由于全部按无符号ulong运算后再转回int即便编码结果超过int.MaxValue也能无损往返这正是测试中「int.MaxValue边界值可以完整往返」的保证。校验规则三类入参约束FiberIdHelper提供了三个校验方法控制着 FiberId 编码的合法输入域ValidateZone(zone)(uint)zone必须 MaxZone即 zone 合法范围为[0, int.MaxValue]负数会被(uint)转换放大而抛异常ValidateLocalSlot(localSlot)localSlot 合法范围为[1, int.MaxValue]0 是非法的——因为 0 通常被用作「无纤程 / 空引用」的哨兵值ValidateFixedLocalSlot(localSlot)在通过ValidateLocalSlot之后额外禁止落入动态区间[DynamicLocalSlotStart, ReservedLocalSlotStart - 1]用于约束配置型固定槽位只能取[1, ConfigLocalSlotMax]或保留区间。IsReservedLocalSlot(localSlot)用于判断某个槽位是否位于保留区间[MaxLocalSlot - 15, MaxLocalSlot]供框架识别特殊用途槽位。Fiber.Zone 与 Fiber.LocalSlot从 Id 实时解码在 Fiber.cs 中Fiber只保存一个不可变的long Id而Zone与LocalSlot都是派生属性每次访问都从Id解码public long Id { get; } public int Zone FiberIdHelper.DecodeZone(this.Id); public int LocalSlot FiberIdHelper.DecodeLocalSlot(this.Id);这种「单一数据源 按需解码」的设计保证了两点一是任何时刻Zone/LocalSlot都不可能与Id不一致杜绝冗余字段漂移二是测试目标「Fiber.Zone必须从Fiber.Id解码得到」「Fiber.LocalSlot必须从Fiber.Id解码得到」在属性层直接成立测试只需验证DecodeZone(child.Id) child.Zone、DecodeLocalSlot(child.Id) child.LocalSlot即可。Fiber同时提供了四个CreateFiber重载其中带int zone参数的版本就是测试计划要求的显式 zone 创建入口public async ETTaskFiber CreateFiber(int zone, long rootId, int sceneType, string name) { Fiber fiber await FiberManager.Instance.CreateFiber(SchedulerType.Parent, zone, rootId, sceneType, name, this); this.children.Add(fiber.Id, fiber); return fiber; }以及返回long版本CreateFiber(SchedulerType schedulerType, int zone, long rootId, int sceneType, string name)。销毁则通过RemoveFiber(long fiberId)/RemoveFibers()完成子纤程销毁时会递归销毁其子纤程并发布FiberDestroyEvent见 Fiber.cs 的RemoveFiber实现。LocalSlot 分配策略配置区间、动态区间与保留区间真正的 localSlot 分配发生在 FiberManager.cs 的AllocateLocalSlot(int zone)中其分配逻辑与FiberIdHelper的常量完全对应private int AllocateLocalSlot(int zone) { FiberIdHelper.ValidateZone(zone); lock (this.localSlotLock) { int localSlot this.nextLocalSlotByZone.TryGetValue(zone, out int nextLocalSlot) ? nextLocalSlot : FiberIdHelper.DynamicLocalSlotStart; // 该 zone 首次分配从 100000 起 if (localSlot FiberIdHelper.ReservedLocalSlotStart) { throw new Exception( $fiber local slot exhausted, zone: {zone}, nextLocalSlot: {localSlot}, dynamic range: [{FiberIdHelper.DynamicLocalSlotStart}, {FiberIdHelper.ReservedLocalSlotStart - 1}]); } this.nextLocalSlotByZone[zone] localSlot 1; // 单调递增绝不回退 return localSlot; } }这套分配策略有四个要点按 zone 隔离的单调递增计数器nextLocalSlotByZone字典为每个 zone 单独维护「下一个槽位」同一 zone 下后创建的纤程 localSlot 一定大于先创建的——这正是测试步骤 7「新LocalSlot大于旧LocalSlot」的实现基础起点为动态区间首次分配从DynamicLocalSlotStart 100000起步把[1, ConfigLocalSlotMax]即[1, 99999]留给配置型固定槽位两类槽位互不冲突永不回收复用分配器不维护空闲池nextLocalSlotByZone只增不减。纤程销毁后其槽位不再分配因此「销毁后再次创建」必然得到全新的 FiberId——这就是测试步骤 5/6/7「新 FiberId 与旧 FiberId 不相同」的实现基础保留区间兜底当 localSlot 达到ReservedLocalSlotStartint.MaxValue - 15时抛出fiber local slot exhausted异常为框架保留的最后 16 个槽位[MaxLocalSlot-15, MaxLocalSlot]留出空间。分配完成后CreateFiber内部调用链为AllocateLocalSlot(zone)→FiberIdHelper.Encode(zone, localSlot)→CreateFiberInternal(fiberId, ...)见 FiberManager.cs 的CreateFiber方法主纤程则通过CreateMainFiber(sceneType, sceneName)以 zone 0 创建。三个核心测试用例解读测试计划Test.md规划了三个用例其中Core_Fiber_IdEncoding_Test与Core_FiberInstanceIdHeader_Test已在仓库落地实现Core_Fiber_IdEncoding32_Test作为边界用例在计划中明确了验证步骤。Core_Fiber_IdEncoding_Test动态分配与不复用验证实现位于 Core_Fiber_IdEncoding_Test.cs它通过TestFiberScope.Create创建一个独立的测试纤程作为父纤程随后按 7 步执行验证每步失败返回不同的非零错误码便于定位创建独立测试 FiberTestFiberScope.Create(context.Fiber, SceneType.TestEmpty, ...)使用parent.CreateFiber(zone, IdGenerater.Instance.GenerateId(), SceneType.TestEmpty, FiberIdEncodingFirst)创建子 Fiber其中zone parent.Zone断言first.Zone zone且FiberIdHelper.DecodeZone(first.Id) zone验证Zone从Id解码一致断言FiberIdHelper.DecodeLocalSlot(first.Id) first.LocalSlot且 localSlot 位于动态区间[DynamicLocalSlotStart, ReservedLocalSlotStart)验证动态分配区间await parent.RemoveFiber(firstId)销毁该子 Fiber在同一 zone 再次创建子 FiberFiberIdEncodingSecond断言新 FiberId 不等于旧 FiberId不复用且secondLocalSlot firstLocalSlot单调递增。该测试完整覆盖了测试计划中「Zone 解码」「LocalSlot 解码」「显式 zone 入口」「销毁后不复用」四个目标并把「动态区间」约束一并纳入验证。Core_Fiber_IdEncoding32_Test32 位宽边界验证这是测试计划中专门针对新 3232 编码的边界用例验证目标为使用大于旧 12 bit zone 上限4095的 zone 编码使用大于旧 20 bit localSlot 上限1048575的 localSlot 编码验证编码结果超过int.MaxValue——因为zone 1时高 32 位非零long编码必然超出 32 位有符号上限验证解码后的 zone 与 localSlot 与输入一致32 位高 / 低位无损往返验证int.MaxValue边界值可以完整往返——即Encode(0, int.MaxValue)恰好等于int.MaxValue且DecodeZone/DecodeLocalSlot能原样还原。该用例的存在意义在于旧编码 1220 布局下 zone 与 localSlot 天然不超过int.MaxValue而新编码必须显式证明「编码结果可以合法地大于int.MaxValue、并且解码无损」这直接守护了 FiberIdHelper.cs 中位运算实现的正确性。从当前仓库文件结构看Test/Fiber 目录 已落地Core_Fiber_IdEncoding_Test.cs与Core_FiberInstanceIdHeader_Test.cs该边界用例属于测试计划中规划的待执行项。Core_FiberInstanceIdHeader_Test内网消息头 16 字节布局验证实现位于 Core_FiberInstanceIdHeader_Test.cs验证内网消息头的FiberInstanceId二进制布局int headerLength Packet.FiberInstanceIdLength; // 期望 16 if (headerLength ! 16) { return 1; } long fiberId FiberIdHelper.Encode(100000, 1500000); // 超过 int.MaxValue 的 FiberId long instanceId (long)int.MaxValue 12345; // 超过 int.MaxValue 的 InstanceId FiberInstanceId fiberInstanceId new(fiberId, instanceId); byte[] bytes new byte[Packet.FiberInstanceIdLength]; bytes.WriteTo(0, fiberInstanceId); long decodedFiberId BitConverter.ToInt64(bytes, 0); // 前 8 字节 long decodedInstanceId BitConverter.ToInt64(bytes, 8); // 后 8 字节三个验证点FiberInstanceIdLength必须为 16 字节使用超过int.MaxValue的 Fiber 与 InstanceId 写入字节数组按两个long读回的值与输入一致。这背后对应 AChannel.cs 中的Packet常量定义public const int MinPacketSize 2; public const int OpcodeLength 2; public const int FiberInstanceIdIndex 0; public const int FiberInstanceIdLength 16;FiberInstanceId 在内网消息中的落地16 字节如何参与路由FiberInstanceId的结构定义位于 ActorId.cs由两个long组成[MemoryPackable] [StructLayout(LayoutKind.Sequential, Pack 1)] public partial struct FiberInstanceId : IEquatableFiberInstanceId { [MemoryPackOrder(0)] public long Fiber; // 8 字节 [MemoryPackOrder(1)] public long InstanceId; // 8 字节 }Fiber字段即本文讨论的 FiberId32 bit zone 32 bit localSlot 编码InstanceId由Fiber.NewInstanceId()提供单调递增实例号两者结合可以区分「同一个槽位在不同生命周期中的不同纤程实例」。它同时是ActorId的组成部分Address FiberInstanceId构成 ET 消息路由的目标地址。内网消息ServiceType.Inner在序列化时会写入该 16 字节头见 MessageSerializeHelper.cscase ServiceType.Inner: opcode MessageToStream(memoryBuffer, (MessageObject)message, Packet.FiberInstanceIdLength); memoryBuffer.GetBuffer().WriteTo(0, fiberInstanceId); // 头部 16 字节 break;对应的消息头布局为[0..16) FiberInstanceIdFiber 8 字节 InstanceId 8 字节→[16..18) Opcode2 字节→ 消息体。接收端反向解析ToMessagefiberInstanceId.Fiber BitConverter.ToInt64(buffer, Packet.FiberInstanceIdIndex); // 偏移 0 fiberInstanceId.InstanceId BitConverter.ToInt64(buffer, Packet.FiberInstanceIdIndex 8); // 偏移 8 ushort opcode BitConverter.ToUInt16(buffer, Packet.FiberInstanceIdLength); // 偏移 16正因为 FiberId 被编码进 64 位long超过int.MaxValue的取值也能在 8 字节内无损承载Core_FiberInstanceIdHeader_Test正是在序列化层面守护这条数据通路的正确性。如何运行这些测试FiberId 编码测试遵循cn.etetet.test包的测试框架见 README.md测试处理器继承抽象基类 ATestHandler.cs实现public abstract ETTaskint Handle(TestContext context)通过 TestFiberScope.cs 创建隔离的TestCase纤程每个用例都是全新服务器环境zone 由TestZoneAllocatorComponent单调分配并校验不超过FiberIdHelper.MaxZone返回ErrorCode.ERR_Success0视为通过返回非零或抛异常视为失败。启动测试场景并执行用例pwsh -Command dotnet ./Bin/ET.App.dll --Console1 --SceneNameTest Test # 运行全部用例 Test --NameIdEncoding # 按正则过滤例如 Fiber 编码相关用例--Name接受正则表达式例如--NameCore_Fiber_IdEncoding可精确匹配Core_Fiber_IdEncoding_Test无匹配时输出not found test!。成功输出形如Test.Core_Fiber_IdEncoding_Test start→... success。注意测试执行完成会退出进程。测试用例类名需遵守{PackageType}_{TestName}_Test命名规范cn.etetet.core包中即以Core_为前缀由分析器TestCaseNamingAnalyzerET0036在编译期强制校验。小结FiberId 统一编码是 ET 纤程身份体系的地基FiberIdHelper以「32 bit zone 32 bit localSlot」把区与槽位打包进 64 位longFiber.Zone/Fiber.LocalSlot从Id实时解码保证单一数据源FiberManager.AllocateLocalSlot以「按 zone 单调递增、永不复用、动态区间 100000 起步、保留区间兜底」的策略保证 FiberId 唯一性与无冲突FiberInstanceId再以 16 字节头部把 FiberId 与实例号接入内网消息路由。测试计划中的三个用例分别从「动态分配与不复用」「32 位边界往返」「消息头二进制布局」三个角度为这套设计提供了可执行、可回归的验证保障。对照 Test.md 与 FiberIdHelper.cs即可完整还原从编码公式到分配策略、再到网络消息落地的全链路。【免费下载链接】ETUnity3D Client And C# Server Framework项目地址: https://gitcode.com/GitHub_Trending/et/ET创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考