Skip to content

架构决策记录 ​

本文档冻结 Shield 当前阶段的架构决策,作为架构设计与实现推进的对齐基准。决策一旦写入本文,后续实现必须围绕该契约收敛。

若本文与 架构总纲、核心设计理念 或 Skynet 对比 冲突,以本文为准。


背景:设计目标 ​

Shield 的设计约定是:

参考 skynet 的 service/call/send/coroutine 语义,用 CAF 承接 actor runtime,Service 是唯一的可寻址 RPC 单元。

见 核心设计理念 §3、Skynet 对比 §CAF 与 Skynet 语义的关系。

本文冻结的决策,意在把 CAF actor runtime、客户端交互契约和玩家承载模型收口,避免后续引入独立 service runtime 或额外可寻址对象模型。


AD-01:CAF 是唯一 actor runtime ​

决策:Service 必须建立在 CAF actor 语义之上;spawn / send / call / exit 收口到 shield_core,不再在 Lua 层另起一套独立 runtime。

理由:这是项目最初的约定,也是「不重复造 skynet」的核心。CAF 提供了 actor / mailbox / scheduler / request-reply / remote actor,完全够用;选型没有错。

推进顺序(见 roadmap):

  1. [dispatch_stack thread-local 化 + 服务注册表加 shared_mutex(零语义变化)。]
  2. [shield.call 使用 CAF request/reply 驱动 coroutine yield/resume。manager->call() 路由到 CAF actor + 条件变量阻塞等待,自调用返回错误。]
  3. per-service 执行权 + 全局激活队列 + N worker 抢占调度(达成 skynet 式多核并发,每 VM 串行)。

AD-02:Service 是唯一可寻址 RPC 单元 ​

决策:

  • 只有 Service 拥有 mailbox、CAF address、send / call 能力。
  • 一个在线玩家 = 一个 PlayerService = 一个 CAF Actor = 一个私有 Lua VM。
  • 玩家业务状态是该 service 私有 Lua table,不另起运行时对象。

理由:严格保持 skynet 模型——send / call 的目标永远是 Service,不是 Entity、不是玩家对象。这样 owner 边界最清晰,并发模型最简单,调试最容易。

含义:

  • 跨服务调用:shield.send(service, method, ...) / shield.call(service, method, ...)。
  • 客户端 RPC 的 route_id 先由描述符选择逻辑服务名;Gateway 再从该 session 的动态路由上下文解析实际 Service actor。逻辑服务名默认是 player,也可以是 scene、room、map 等当前玩家已绑定的服务。
  • 目标 Service adapter 在本 VM 内按 header route_id 直达启动期注册的 handler。业务 Lua 不接收通用客户端消息后再自行分发。

AD-03:不引入 Entity 运行时 ​

决策:第一阶段不做以下任何一项:

  • PlayerEntity / AvatarEntity
  • Entity mailbox
  • Entity send / call / Entity RPC
  • 全局可写 EntityManager.get(id)
  • AvatarShard / player_pool 分片承载

普通数据(房间、战斗、怪物、掉落物等)作为所属 service 内部 Lua table;只有确实需要独立 actor 语义的对象才成为独立 service。

理由:

  • 引入「可 send/call 的 PlayerEntity」等于在 Service 这层 actor runtime 之上再叠一层 actor runtime,复杂度爆炸,owner/mailbox/timer/coroutine/RPC 全部出现两套归属。
  • KBEngine 的 Entity 之所以能 RPC,是因为它的 Entity 本身就是「带领域语义的 actor-like 对象」,背后有完整运行时;那是另一条路线,不在本阶段采用。
  • 第一版优先把 service/call/runtime 语义做对、做稳,降低理解成本和出错面。

未来:若真需要「Entity 即轻量 actor」,作为一次明确的架构升级引入,不预埋、不污染当前 API。

参见 实体与组件化草案(该文已降级为草案/非当前契约)。

澄清(shield_player P0 起):本文禁止的是「可 send/call 的 Entity actor」这层第二 runtime。shield_player 的 PlayerSession(模块持有的会话元数据)与 PlayerRef({uid, node_id, service_id, epoch} 值引用)不是 actor:没有 mailbox、没有自己的 RPC/timer/coroutine,不能作为 shield.send/call 的 target,因此不在禁止范围内;player_pool 分片仍是压测前置的可选策略(OD-012)。


AD-04:one-player-one-service 为默认容量模型 ​

决策:默认每个在线玩家对应一个 PlayerService(独立 Lua VM)。不为未测到的内存压力提前做 shard / pool 优化。

理由:

  • 这是 skynet agent 模型的默认形态,owner 最清晰、重连/登出/保存/定时器都挂在同一处。
  • 内存取舍经评估可接受:每玩家 VM 的实际成本需用真实压测定,而不是凭空假设后提前优化。
  • 提前引入 shard 会把「玩家路由、owner 校验、shard 内状态管理」等复杂度前置,违背「先把架构做对」的原则。

未来:仅在真实压测证明每玩家 VM 成本不可接受时,才引入 player_pool / avatar_shard,且作为独立架构升级项,不改 public API。


AD-05:客户端 RPC 路由设计 ​

决策:wire header 携带 route_id,body 是纯业务数据。所有客户端消息经 Gateway 发给 session 绑定的目标服务(登录前 → AuthService,登录后 → PlayerService)。route_id 的唯一职责是在目标 Service VM 内命中启动期缓存的 Lua handler。Gateway 不从 route_id 解析目标服务,不做多目标路由。

wire 格式:

text
+------------------+------------------+
|     Header       |       Body       |
| (route_id, ...)  | (业务数据)        |
+------------------+------------------+

Header 携带 route_id 等传输层字段。Body 是纯业务数据,编码格式由 ProtocolProfile 决定(json / protobuf / msgpack)。route_id 不出现在 body 中。

入站路径:

text
socket bytes
  → frame decode → 读 header.route_id
  → Gateway 路由表校验(route_id 合法 + direction + 认证要求)
  → session.target(AuthService 或 PlayerService)
  → CAF send ClientIngress { session_id, epoch, player_id, route_id, body_bytes }
  → 目标 VM: route_id → cached handler → decode body → invoke handler(ctx, client, request)
  • Gateway 只读 header 中的 route_id 做合法性校验,不解码 body。
  • body_bytes 原样传递到目标 VM,由目标 VM 按 RPC schema 解码。
  • handler 签名固定为 handler(ctx, ClientContext, request)(ctx 为 sender 上下文表),不含 route_id 或原始 frame。

预登录路由:认证前 session 尚未绑定 PlayerService,预登录 RPC(login、token 验证等)目标为 AuthService,使用同样的 route_id → handler 机制。认证成功后 Gateway 原子切换 session.target = PlayerService。

出站路径:codegen 生成的 server-to-client helper 已绑定 route_id;出站 encode 按目标 s2c 路由自己的 schema 类型名寻址(同名约定或该路由的 request_schema 覆盖,同 route_id 回包被出站 direction 校验拒绝)。业务只传 ClientContext + 业务参数。helper 编码 body_bytes 后 CAF 发送 ClientEgress 到 Gateway,Gateway 把 route_id 写入 wire header,body_bytes 作为 body。

codec 与路由无关:codec 是 session 级固定的(src/transport/protocol.cpp 监听器构建期解析 vtable 并加入 codecs_),不按 route 选择。codec_for_route() 忽略 route,恒返回 default_codec_id(src/transport/protocol.cpp:1520-1524)。

RPC 描述符:每个客户端 RPC 在编译期由描述符定义 route_id、name、direction、binding、request_schema、request_codec(schema_id / response_schema / binding_hint 已移除,配置出现即报错;schema 寻址收敛为「路由名同名约定 + request_schema 显式覆盖」)。目标 Service 启动时编译为 route_id → cached Lua handler 映射。name 只用于日志和调试,不进入 wire。

请求、响应与 push 边界:

  • Shield 提供 descriptor 驱动的客户端 RPC method dispatch,以及生成的 server-to-client response/push helper。
  • 第一版不提供通用 client.call、seq pending map、future 或自动超时关联。若某个 RPC 需要 correlation token,由该 RPC 契约显式定义业务字段。
  • PacketKind / seq 不暴露为 Lua API,也不借用 CAF request id 建立客户端 pending 表。

详见 客户端 RPC 路由设计、网关设计、Lua API。


AD-06:SessionHandle 边界 ​

决策(实现口径见下方后继说明;SessionHandle 类型已删除):

  • 裸 session/连接句柄只存在于 network / gateway 边界,不进入业务 Lua 或 CAF payload。
  • 登录后每个 ClientIngress 必须携带 runtime 注入的可信客户端上下文:Gateway 地址、session_id、session epoch、player_id 和 protocol profile id。它们是可序列化值,不是连接对象。
  • Lua handler 只看到只读 ClientContext 与该 RPC 的业务参数;ClientContext 可派生用于回包的 ClientRef;该值内部保留 protocol profile identity,但不向业务暴露 socket、CAF handle、codec 或 frame。
  • Player/Scene/Room/Map Service 通过注册的 server-to-client RPC helper 发送 ClientEgress;它们不调用通用 session send API,也不接触 codec / frame / ProtocolPipeline。

理由:网络连接所有权归 Gateway,但直接接收客户端 RPC 的任意目标 Service 都必须知道可信玩家身份,并能把响应送回原连接。因此跨 actor 传递稳定的值语义 client reference,而不是连接 handle。这样:

  • SessionHandle 不会随消息序列化或跨线程传播。
  • 目标 Service 无论位于本地还是远端,都能获得可信 player_id 和回包引用。
  • 业务 Service 与具体协议、socket 实现彻底解耦。
  • 断线、重连和 stale 回包的 owner/epoch 校验集中在 Gateway 一处。

后继实现(架构纠偏 M2 起):SessionHandle 类型已删除。上述边界由只读 ClientContext(单次入站的 userdata)与 ClientRef(可回包的值引用,经 __shield_client_ref JSON 标记跨服务序列化并物化为只读 userdata)承载;session 单一 target 绑定与 epoch 由 Gateway 的 SessionBinding 管理,裸连接句柄不再有任何业务侧类型。


AD-07:可控业务时钟(分层时间) ​

决策:时间分层——Lua 业务时间(shield.now / os.time 无参 / os.date 无参)走可注入的 wall-clock Clock,测试可拨;C++ 调度/网络时间(deadline_ms / timestamp / CAF delayed_send / pump 扫描)锁死真实 monotonic。os.clock 不接入(真实 CPU 时间,用于性能计时)。定时器触发保持真实时钟(属于调度层),callback 内业务读到的时间可拨。

理由:游戏业务强依赖日期语义(刷新/活动/过期),必须可拨才能测试;而网络心跳、call 超时、定时触发属调度层,拨动会破坏运行时本身(连接误超时、调度错乱)。分层让"可测"与"运行时正确"不冲突。业务代码大量直用 os.time(仓库实证缓存过期等 ~12 处),只拨 shield.now 无意义,故 os.time/os.date 无参必须一并接入同一 Clock。os.clock 不动——旧的 DI/IoC 实验脚本(已删除)曾用它做忙等 while os.clock()-start<0.01,拨了会死循环。

实现状态:已实现。shield.now() 读 LuaServiceManager 持有的 Clock(默认 SystemClock,system_clock 墙钟 UTC ms);shield.monotonic() 读 steady_clock(不可拨)。os.time()/os.date() 无参 hook 在 register_full_shield_api 安装,带参形式保留原函数。attach_clock() 用于测试注入 MockClock。Clock/SystemClock/MockClock 定义于 include/shield/lua/clock.hpp。test_lua_api_clock 覆盖 12 个用例:默认墙钟、一致性、MockClock set/advance、os hook 粒度(含 table 带参不受影响)、monotonic 不可拨、缓存过期/签到/冷却完整业务场景。


AD-08:密码学只提供原语,认证语义在 Lua 业务层 ​

决策:runtime 提供密码学原语库(shield.crypto:base64/base64url/hex 编解码、SHA-256、HMAC-SHA256、安全随机字节、常数时间比较),不提供认证/签名等业务语义。JWT 这类"签发什么 claim、何时过期、哪个 issuer 被接受"的决策属于业务,放在 Lua 层(scripts/lib/jwt.lua 为参考实现)。原 C++ 插件 plugins/auth_jwt 随之弃用,保留一个过渡期后移除。

理由:认证策略是业务语义,会随产品形态变化(多租户 issuer、refresh token、非对称签名、密钥轮换),而 C++ 插件是 ABI 冻结的二进制边界——每种策略都塞进插件层等于把业务决策写进 ABI。原语是稳定面:算法与编码格式变化慢、可独立测试(RFC 向量),且实现可直接用已有依赖(OpenSSL 已在依赖树内),无需自行开发。分层后"换认证方案"是改 Lua,不是发新二进制;runtime 也不必为每种 token 形态维护插件 API 版本。

实现状态(2026-09-28 落地,2026-10-09 扩展):include/shield/lua/lua_crypto.hpp + src/lua/lua_crypto.cpp,由 register_full_shield_api 装配;shield_lua 显式链接 OpenSSL::Crypto。API 表见 lua-api.md。scripts/lib/jwt.lua(HS256 与 EdDSA 双算法 + RFC 7517 JWKS 公钥集助手)为纯 Lua 参考实现,只用原语拼装,自带最小 JSON 编解码;二期原语(PBKDF2/HKDF、AES-256-GCM、Ed25519)已于 2026-10-09 补齐。plugins/auth_jwt/manifest.yaml 标 deprecated(不删,兼容既有部署)。


决策与本仓库其他文档的关系 ​

决策影响文档
AD-01architecture.md、architecture-core-concepts.md、skynet-comparison.md、runtime-messaging.md、roadmap.md
AD-02runtime-service.md、runtime-player.md、skynet-comparison.md
AD-03runtime-entity.md、runtime-player.md、architecture.md
AD-04runtime-player.md
AD-05gateway.md、protocol-routing-design.md、lua-api.md、runtime-messaging.md
AD-06runtime-network.md、runtime-player.md、gateway.md、lua-api.md
AD-07runtime-timer.md、lua-api.md
AD-08lua-api.md、plugin-system.md、architecture.md

Apache License 2.0