Skip to content

Lua API 契约 ​

本文是 Shield 重构后的 Lua 用户 API 契约。运行时语义细节仍由各 runtime-*.md 文档展开;当示例或旧源码与本文冲突时,以本文为准。

当前状态:Phase 1 契约已实现,细节以各节实现快照为准。

实现快照(点击展开)

当前源码已跑通单节点 Lua service 路径,包括 actors 配置启动、on_init/on_shutdown/on_exit/on_error/on_panic、shield.spawn/exit/self/sender/names/query/register/unregister/claim/now、coroutine-aware shield.call/call_timeout 与 handler 内 shield.sleep、shield.timer_once/timer/cancel_timer/fork、shield.config、shield.log.*、插件 Lua API(由各插件 register_lua 注册到 shield.<namespace>,详见 "Plugin-provided APIs")、on_exit call guard、call timeout(CAF call_timeout_atom)、timer/fork/on_init 回调统一协程 dispatch(invoke_coroutine,挂起与错误路由到 on_error 均与 handler 一致)、TCP gateway listener 到 Lua handler 的 bootstrap 桥接、HTTP 客户端(shield.http.*)以及 shield_cluster 的静态 peer/route cache 快照 API。

  • HTTP 服务端 Lua 路由(shield.httpd.*)已接入 bootstrap:路由保存于运行时注册表,由 LuaHttpBridge 镜像进 HttpServer,请求派发到注册服务的 actor 线程执行。
  • on_shutdown(ctx) 已实现(shutdown.timeout.service_drain 预算内的 graceful drain,见下文);单 VM 内部 shield.event 仍为目标契约,当前源码尚未实现。

设计原则 ​

  • Lua service 是普通 Lua module,返回一个 table。
  • 不使用 shield.service("name") 构造服务对象。
  • service handler 接收 ctx 作为第一个参数,包含当前消息上下文(sender、trace、deadline 等)。
  • ctx 是只读对象,handler 返回后上下文失效。
  • shield.send 非阻塞、无 ACK。
  • shield.call 挂起当前 Lua coroutine,但不阻塞 runtime 线程。
  • Runtime API 返回值统一使用 ok, result_or_error,业务返回的 nil 和 false 不应和 runtime 错误混淆。
  • shield.event 只用于当前 Lua VM 内部解耦,不跨 service、不跨 VM、不参与 bootstrap lifecycle 编排。
  • DB/Redis API 使用插件 namespace + binding 逻辑名调用:shield.database.mysql("database.default"):query(...),不再提供 shield.db.* / shield.redis.* 全局函数。具体 namespace 与方法见下文 "Plugin-provided APIs"。

Service Module ​

标准 service 文件:

lua
local M = {}

function M.on_init(args)
    M.name = args.name
    M.config = args.config or {}
    shield.log.info(M.name .. " started")
end

function M.ping(ctx, value)
    -- ctx.sender 是发送者
    shield.send(ctx.sender, "pong", value)
end

function M.on_shutdown(ctx)
    shield.log.info(M.name .. " draining: " .. ctx.reason)
end

function M.on_exit(reason)
    shield.log.info(M.name .. " exiting: " .. reason)
end

return M

加载规则 ​

规则说明
返回 table脚本必须返回 table
普通函数即 methodtable 上的普通函数就是可被 send/call 分发的 method
保留名称on_* 名称为运行时 hook 或模块 hook 保留,业务 method 禁止使用 on_ 前缀
顶层轻量声明module 顶层代码只做轻量声明,不执行阻塞 I/O
隔离上下文每个 service 实例有独立 Lua state 或隔离上下文,不能共享可变 Lua 全局业务状态
event 生命周期shield.event 的 listener 表属于当前 service 的 Lua VM;service 退出后随 VM 一起释放

Lifecycle Hooks ​


on_init(args) ​

服务创建后、name publish 前调用。

lua
function M.on_init(args)
    -- args.name: configured or spawned service name, may be nil
    -- args.id: local service id
    -- args.config: actor options/config
    -- args.args: shield.spawn(..., { args = ... }) payload
end

返回值规则:

场景返回值说明
成功无返回值表示成功
成功return true表示成功
失败return false, "reason"表示失败
失败return nil, "reason"表示失败
失败抛出 Lua error表示失败

失败后的处理:

  • shield.spawn 返回 nil, Error
  • 已 reserve 的 name rollback
  • service 不进入 running 状态

on_shutdown(ctx) ​

运行时进入 graceful shutdown drain 阶段时调用。它是服务级 hook,不是全局事件,也不会通过 shield.event 广播。

lua
function M.on_shutdown(ctx)
    -- ctx.reason: "stopping" | "signal" | "check_config" | ...
    -- ctx.deadline_ms: drain 共享截止(业务 Clock,与 shield.now() 同源,
    --   ctx.deadline_ms - shield.now() 即本 hook 剩余预算)
    -- ctx.timeout_ms: 本 service drain 预算(进入本 hook 时的剩余预算)
end

规则:

  • on_shutdown 用于停止接收业务入口、flush 内存状态、通知本服务拥有的外部资源进入 drain
  • 运行时先停止 accept/readiness,再按依赖反向顺序调用 on_shutdown;依赖图未实现时按 spawn 逆序
  • on_shutdown 可以是 coroutine-aware hook,可在 deadline 内使用 shield.call、shield.sleep 等会挂起的 API
  • 超时或抛错只记录错误并继续关闭流程;随后仍会调用 on_exit(reason)
  • 缺失 on_shutdown 视为 no-op
  • 不提供 on_ready 广播;service ready 定义为 on_init 成功并 publish name,application ready 由 bootstrap 在 required actors 启动完且 accept 开启前后判定

实现状态:已实现。bootstrap shutdown 在停止 accept/readiness 之后、shutdown_all 之前调用 LuaServiceManager::drain_all("stopping", shutdown.timeout.service_drain):按 spawn 逆序逐个 service 的 actor 线程运行 on_shutdown(依赖图未实现前的反向顺序),整个 drain 阶段共享 service_drain 预算(ctx.timeout_ms 为进入本 hook 时的剩余预算,预算耗尽后剩余 hook 跳过并记 WARN);hook 抛错/超时只记日志并继续;缺失 on_shutdown 为立即 no-op。drain 期间 spawn 被拒绝(shield.spawn 返回失败),shield.call/sleep 可用;被超时放弃的 hook 不中断,由随后的 shutdown_all 统一收尾。


on_exit(reason) ​

服务停止前调用。

lua
function M.on_exit(reason)
end

规则:

规则说明
reason 类型字符串,如 normal、stopping、panic、timeout
定位final best-effort 清理,不承担异步 drain
禁止操作不允许调用会挂起 coroutine 的 API(如 shield.call、shield.sleep)
跨服务 flush应放在 on_shutdown(ctx),不要放在 on_exit(reason)

实现快照:shield.call / shield.call_timeout 在 on_exit 上下文中调用时,Lua wrapper 检查 shield._is_in_exit() 并立即返回 false, {code="api_not_allowed_in_exit", message="..."}。OnExitCallGuard 测试覆盖。

on_error(err, context) ​

handler、timer、fork task 或本地 shield.event listener 抛出错误时调用。

lua
function M.on_error(err, context)
    shield.log.error(context.type .. ": " .. tostring(err))
end

context 字段:

字段类型说明
typestringhandler、timer、fork、event
methodstringhandler method 名称,非 handler 时为空
eventstringshield.event 事件名,仅 type="event" 时存在
trace_idstring可选 trace id

panic 策略:

on_error 不改变服务状态。Phase 1 的 panic 策略固定为:同一 service 连续 handler/timer/fork 未捕获错误达到固定阈值 10(kDefaultMaxErrorsBeforePanic,当前无配置键)时进入 panic。

实现快照:on_error / on_panic hook 已实现。当 handler 抛错时,call_service_method_coroutine 调用 LuaRuntime::invoke_hook 触发 service table 上的 on_error(err, context);timer callback 错误通过 CAF actor 调度触发;fork task 错误通过 CAF actor 调度 fork_task_atom 触发。连续未捕获错误达到 kDefaultMaxErrorsBeforePanic(默认 10)时触发 on_panic(reason, context) 并 exit("panic")。成功执行后错误计数重置。OnErrorHookCalledOnHandlerThrow 测试覆盖。


on_panic(reason, context) ​

服务进入不可恢复状态前调用。

lua
function M.on_panic(reason, context)
    shield.log.error("panic: " .. reason)
end

规则:

规则说明
禁止挂起不允许挂起
允许操作只能做同步、best-effort 的紧急记录或状态标记
返回值返回值会被忽略

Service API ​

shield.spawn(module, opts) ​

lua
local h, err = shield.spawn("player", {
    name = "player.1001",
    args = { uid = 1001 },
    timeout = 10000,
})

参数:

参数类型说明
modulestringactor 类型或脚本别名,由 actors[].name 映射
opts.namestring可选 service name,本 runtime 内唯一
opts.argstable传给 on_init(args).args 的业务参数
opts.timeoutnumber覆盖 spawn timeout,单位 ms;<= 0 不回退默认值,行为见实现快照

返回值:

场景返回值
成功ServiceHandle, nil
失败nil, Error
实现快照(点击展开)

shield.spawn 是"同步语义、异步实现"。在 handler 协程(含 fork task 协程)中调用时走 _coro_spawn:caller 经 suspend_for_call + coroutine.yield() 挂起,VM 创建、module 加载和 on_init 在专用 spawn worker 线程(单线程串行)上执行,完成后经 CallResponseMessage 路由回 caller actor 恢复协程——慢 on_init 不会阻塞 caller 的 service actor。VM 主线程(如 on_init 内)调用时保持同步路径(_sync_spawn)。

spawn 期间 name 处于 reserved 状态:同名 spawn 立即失败(service name already reserved),shield.query 在 publish 前不可见;on_init 失败回滚 name(init_failed)。挂起超时按 opts.timeout(默认 10000ms)以 {code="spawn_timeout"} 恢复 caller,且成功但超时的子 service 会被补偿退出(reason "timeout")。超时判定仍是事后测量,on_init 本身不会被抢占中断。

opts.timeout <= 0 时不回退默认值:Lua 侧 0 为 truthy,会直接穿过 opts.timeout or 10000 默认表达式进入 C++。C++ spawn 预算取 0 后 init_overran(耗时 ≥ 预算)恒真——on_init 业务失败统一报 spawn timeout: on_init exceeded 0ms limit(on_init 成功则正常返回),同步与协程路径皆然;协程路径 caller 的挂起超时另有 C++ 地板 5000ms(timeout_ms > 0 ? timeout_ms : 5000)。

测试覆盖见 test_lua_api_spawn.cpp。


shield.panic(reason) ​

lua
shield.panic("unrecoverable state")

业务主动触发 panic 路径:调用当前 service 的 on_panic(reason, {type="explicit"}) hook(best-effort),随后以 reason "panic" 退出当前 service。只能在 service dispatch 上下文中调用,否则为 no-op。


shield.exit(reason) ​

lua
shield.exit("normal")

停止当前 service。reason 可省略,默认为 normal。


shield.self() ​

lua
local me = shield.self()

返回当前 service 的 ServiceHandle。只能在 service coroutine 中调用。

ServiceHandle 方法:

方法返回值说明
h:id()number本地 service id
h:node()numbernode id(单节点 runtime 恒为 0,即本地)
h:valid()boolean检查 handle 非空

shield.names() ​

lua
local names = shield.names()

返回本地 registry 中所有已发布服务 name 的列表(全局视图,不按调用者过滤)。


shield.query(name) ​

lua
local h, err = shield.query("gateway.main")

查询本地 registry。cluster 全局发现不进入 core。


shield.register(name) ​

lua
local ok, err = shield.register("gateway.public")

给当前 service 发布一个本地 name。


shield.unregister(name) ​

lua
local ok, err = shield.unregister("gateway.public")

注销当前 service 拥有的 name。


shield.claim(name) ​

lua
local ok, err = shield.claim("gateway.public")

蓝绿热更新交接:当前 service 原子接管一个已存在的 name(原 owner 继续运行,负责 drain 后退出;name 变更通知以新 owner 触发)。已拥有该 name 时幂等成功(无变更、不触发通知)。与 register/unregister 同一 调度上下文规则:只能在自己 service 的 handler 里调用。语义与时序见 Lua VM 语义 · Blue-Green。


Message API ​

shield.send(target, method, ...) ​

lua
local ok, err = shield.send("room.1", "join", uid, token)

规则:

规则说明
target 类型可以是 service name 或 ServiceHandle
成功含义成功只表示 runtime 接受投递
失败返回receiver 不存在、runtime stopping 等返回 false, Error
重试send 不自动重试
self-send允许,但必须进入未来调度点,不允许 reentrant 执行

shield.call(target, method, ...) ​

lua
local ok, profile = shield.call("player.1001", "get_profile", uid)

规则:

规则说明
挂起行为挂起当前 Lua coroutine
阻塞不阻塞 CAF scheduler 线程
超时使用默认 call timeout
成功返回true, ...callee_returns
失败返回false, Error

业务返回 nil 或 false 时仍是成功:

lua
-- callee
function M.check(uid)
    return false, "banned"
end

-- caller
local ok, allowed, reason = shield.call("auth", "check", uid)
-- ok == true, allowed == false

shield.call_timeout(timeout_ms, target, method, ...) ​

lua
local ok, result = shield.call_timeout(3000, "db.player", "get", uid)

使用单独函数覆盖 timeout,避免最后一个业务参数和 options table 歧义。

实现快照(点击展开)

shield.call / shield.call_timeout 只有协程路径——在 handler / timer / fork / on_init 协程中调用时,caller 通过 _coro_call → suspend_for_call + coroutine.yield() 挂起;callee 完成后响应经 caller actor mailbox 回投,resume_caller 在 caller actor 线程恢复 caller 协程(actor 在挂起期间继续处理其他消息)。C++ 侧 manager->call() 是外部等待者:向 target actor 发送统一 call 请求(ServiceMessage + call_session),在外部 call_session 上通过条件变量等待同一套响应路径,对 Lua 语义无感知。

不在协程中的调用(module-level 代码、主线程)返回 false, {code="call_not_allowed_off_coroutine", message="..."}。协程内自调用(caller == target)合法:caller 协程 yield 释放 dispatch 后,请求经自身 actor mailbox 串行处理。call timeout 通过 CAF delayed_send 实现,以 {code="timeout", message="call timeout", retryable=true} 恢复 caller。

yield 窗口与恢复线程不变量:suspend_for_call 注册挂起后、协程执行到 coroutine.yield() 之前存在一个窗口——若响应在窗口内到达,resume_caller 通过 registry 锁保护的 driving 相位注册表(每个 lua_resume 源在驱动前后持有 DrivingGuard)发现协程仍被驱动线程持有,不就地恢复(lua_resume 会拒绝 running 协程),而是把响应消息重新投递回 caller actor 邮箱;协程 yield 后驱动注册释放,重投消息在下一轮处理时自然以挂起态直接恢复。判定只读互斥保护的注册表、绝不窥探另一线程正在驱动的协程的 Lua 状态(跨线程读 lua_status/lua_getstack 是数据竞争,MSVC release 构建曾因此崩溃)。由 Lua 层自驱动、yield 无 C++ 观测点的协程(coroutine.wrap、timer/fork 回调)在响应到达时驱动注册已释放(已挂起),同样走直接恢复。该机制保证一条硬不变量:caller 协程的每次 lua_resume 都发生在拥有它的 actor 线程上——例如 on_init 协程由 spawn 线程驱动,其 call 响应绝不在 spawn 线程恢复协程,否则会与 pre-init 直通消息(timer fire / call response)并发进入同一 lua_State。超时与正常完成共用重投路径;重投计数有上限(协程永不 yield 时对齐历史盲等失败的丢弃语义),每次重投顺延超时扫描的截止时间防止重复触发。

LAPI-005-06 已覆盖。

Message Context ​

lua
function M.handler(ctx, ...)
    local src = ctx.sender
    local trace = ctx.trace
    local deadline = ctx.deadline
end

规则:

规则说明
只读ctx 是只读对象,包含当前消息上下文
有效范围只在 message handler coroutine 中有效
生命周期handler 返回后上下文失效
timer/forktimer callback / fork task 中 ctx.sender 返回 nil
实现快照(点击展开)

ctx 对象包含 sender、trace、deadline 字段。deadline_ms 和 trace_id 字段在 send/call 消息中传播,从 caller 的 dispatch context 携带到 callee 的 dispatch context;但 runtime 当前不生成 trace id,ctx.trace 恒返回 nil(生成属于 Phase 2+)。

timer callback / fork task context 中 ctx.sender 返回 nil,ctx.trace 返回 nil,ctx.deadline 返回 nil。


Timer API ​

shield.timer_once(delay_ms, callback) ​

lua
local id = shield.timer_once(1000, function()
    shield.send("room.1", "tick")
end)

shield.timer(interval_ms, callback) ​

lua
local id = shield.timer(1000, function()
    shield.log.debug("heartbeat")
end)

规则:

规则说明
调度策略fixed-delay:callback 结束后再安排下一次
执行方式callback 是 coroutine(invoke_coroutine 包裹);shield.sleep / shield.call 在 callback 中挂起回调协程,不阻塞 actor
错误处理callback 抛错时触发 on_error
生命周期service exit 自动取消 owned timers

shield.cancel_timer(timer_id) ​

lua
local ok, err = shield.cancel_timer(id)

shield.sleep(delay_ms) ​

lua
shield.sleep(100)

在协程上下文(message handler、timer/fork callback、on_init、on_shutdown)中挂起当前 coroutine,不阻塞 runtime thread;仅在 module-level 代码等无协程上下文中退化为阻塞线程的 _block_sleep。


shield.fork(fn) ​

lua
local task = shield.fork(function()
    shield.send("worker", "done")
end)

返回 numeric task id。fork task 属于当前 service,以协程派发执行(invoke_coroutine),task 体内可用 shield.sleep / shield.call 挂起;service exit 时自动取消尚未执行的 task。


Time API ​

lua
local ms = shield.now()
local mono = shield.monotonic()

shield.now() ​

返回 wall-clock UTC milliseconds(业务时间)。默认读 system_clock,可通过 LuaServiceManager::attach_clock(MockClock) 注入可拨时钟,测试时显式控制 shield.now() / os.time()(无参)/ os.date()(无参)的返回值。

AD-07 分层时钟:shield.now() 返回墙钟 UTC ms(LuaClock,可拨),用于业务逻辑(刷新/活动/过期判断/ID 生成)。deadline_ms、timestamp 等 C++ 内部字段保持 monotonic(InfraClock,不可拨)。参见 架构决策 AD-07。


shield.monotonic() ​

返回 real monotonic milliseconds(steady_clock,不可拨)。用于 Lua 层需要真实流逝时间的场景(网络退避、心跳间隔、性能计时)。不受 MockClock 影响。


os.time / os.date / os.clock ​

函数行为
os.time()(无参)重定向到业务时钟(与 shield.now() 同源)
os.date(fmt)(无参)重定向到业务时钟(与 shield.now() 同源)
os.time(table)(带参)保持原生行为
os.date(fmt, t)(带参)保持原生行为
os.clock()不受业务时钟影响,返回真实 CPU 时间

原因:游戏业务代码大量直接用 os.time() 读当前时间(缓存过期、签到、活动开关),只拨 shield.now() 无法覆盖。将 os.time/os.date 无参形式接入同一业务时钟,使所有业务时间读取统一可控。

实现快照(点击展开)

shield.now() 已实现,读 LuaServiceManager 持有的 Clock(默认 SystemClock 读 system_clock);shield.monotonic() 已实现,读 steady_clock(不可拨)。attach_clock() 在 src/lua/lua_service.cpp 实现,用于测试注入 MockClock。

os.time()/os.date() 无参 hook 在 register_full_shield_api(src/lua/lua_api.cpp)中安装,带参形式保留原函数。os.clock() 不动。Clock/SystemClock/MockClock 定义于 include/shield/lua/clock.hpp。

测试覆盖见 test_lua_api_clock:默认墙钟、一致性、MockClock set/advance、os hook 粒度(含 table 带参不受影响)、monotonic 不可拨、缓存过期/签到/cooldown 完整业务场景。


Config API ​

lua
local host = shield.config("database.host", "localhost")

规则:

规则说明
数据来源读取 bootstrap 后的配置快照
只读默认只读
默认值找不到 key 时返回第二个参数作为默认值;没有默认值则返回 nil
加载方式配置加载和合并由 C++ bootstrap 完成,Lua 不负责加载 YAML 文件
实现快照(点击展开)

shield.config 已实现,key 为扁平字符串匹配(如 "database.host"),不支持嵌套路径遍历。返回值自动尝试转换为 boolean/integer/number/string。


Log API ​

lua
shield.log.debug("message")
shield.log.info("message")
shield.log.warn("message")
shield.log.error("message")

规则:

规则说明
自动注入日志自动注入 service id
参数类型参数必须是 string 或可安全 tostring 的值
禁止操作不允许在日志 API 中执行阻塞 I/O
实现快照(点击展开)

shield.log.* 输出时自动注入当前 service id 前缀(格式:[service_id] message)。service name 和 trace id 注入属于 Phase 2 扩展。


Local Event API ​

shield.event 是单个 Lua service / 单个 Lua VM 内部的同步 observer 工具,用于拆分本 service 内部模块。它不是 runtime event bus,不进入 mailbox,不序列化 payload,不跨 service,不跨 VM,不跨 node,也不承载 lifecycle 编排。

lua
local off = shield.event.on("inventory.changed", function(payload)
    shield.log.info("changed: " .. payload.item_id)
end)

shield.event.emit("inventory.changed", { item_id = "sword_01" })
off()

API:

API说明
shield.event.on(name, fn)注册当前 VM 内 listener,返回 unsubscribe 函数
shield.event.off(name, fn)移除当前 VM 内 listener
shield.event.emit(name, payload)同步调用当前 VM 内 listener,返回被调用数量
shield.event.clear(name?)清空指定事件或全部本地 listener

规则:

规则说明
执行方式listener 在 emit 调用栈内同步执行,顺序为注册顺序
错误处理listener 抛错时由当前 service 的 on_error(err, {type="event", event=name}) 处理;其他 listener 继续执行
禁止用途listener 不能用于接收 application_ready、shutdown、service_started 等 runtime lifecycle 事件;这些不是 shield.event 的职责
跨服务解耦跨 service 解耦使用 shield.send/call 或插件队列;跨 node 发布订阅属于可选模块或插件能力

实现状态:目标契约,当前源码尚未实现 shield.event。


Crypto API ​

shield.crypto 提供密码学原语(C++/OpenSSL 实现)。分层裁定:runtime 只提供原语,业务语义(JWT/会话签名等)在 Lua 层组合——参考实现 scripts/lib/jwt.lua(HS256 + EdDSA + JWKS,见下)。

所有函数接收/返回 Lua string(字节透明);哈希与 HMAC 返回原始摘要(需要文本形式时用 hex_encode / base64url_encode 组合)。

编解码:

API说明
shield.crypto.base64_encode(data)标准 base64(RFC 4648 §4,带 = 填充)
shield.crypto.base64_decode(data)标准/base64url 字母表均可;非法字符/长度抛 Lua error
shield.crypto.base64url_encode(data)URL-safe 字母表(-_),无填充(JWT segment 形态)
shield.crypto.base64url_decode(data)接受无填充与标准填充输入
shield.crypto.hex_encode(data)小写十六进制
shield.crypto.hex_decode(data)大小写均可;奇数长度/非法字符抛 error

摘要 / MAC / 随机:

API说明
shield.crypto.sha256(data)SHA-256,返回 32 字节原始摘要
shield.crypto.hmac_sha256(key, data)HMAC-SHA256,返回 32 字节原始 MAC
shield.crypto.random_bytes(n)密码学安全随机字节(RAND_bytes);n=0 返回空串,n<0 或 n>1MiB 抛 error
shield.crypto.constant_time_compare(a, b)常数时间内容比较;长度不等直接 false(长度本身不设防,内容比较设防)

密钥派生(二期):

API说明
shield.crypto.pbkdf2_hmac_sha256(password, salt, iterations, dklen)PBKDF2-HMAC-SHA256(RFC 8018),口令哈希用;返回 dklen 字节原始派生钥。iterations 必须 1–1000 万、salt 非空、dklen 1–1024,越界抛 error;空口令合法。存库时 salt + iterations 随派生钥一起落,校验用 constant_time_compare
shield.crypto.hkdf_sha256(ikm, salt, info, length)HKDF-SHA256(RFC 5869),协商密钥按用途展开用;返回 length 字节原始输出。ikm 非空、length 1–8160(255×32 上限);salt/info 允许空串(空 salt 按 RFC 取 32 字节零序列)

AEAD(二期):

API说明
shield.crypto.aead_aes256gcm_encrypt(key, nonce, plaintext, aad)AES-256-GCM(NIST SP 800-38D)认证加密;返回 combined ciphertext‖tag(16 字节 GCM tag 尾附)。key 必须 32 字节、nonce 12 字节(GCM 推荐长度),aad 允许空串;尺寸不符抛 error
shield.crypto.aead_aes256gcm_decrypt(key, nonce, combined, aad)先验 tag(常数时间)再返回明文;key/nonce/tag/AAD 任一不符抛 error,绝不返回未经认证的明文;输入短于 16 字节 tag 抛 error

非对称签名验签(二期):

API说明
shield.crypto.ed25519_public_key(secret_key)由 32 字节 RFC 8032 seed 推导 32 字节原始公钥;seed 尺寸不符抛 error
shield.crypto.ed25519_sign(secret_key, message)Ed25519(RFC 8032)确定性签名,返回 64 字节原始签名(无 nonce、无随机性:同 key 同 message 恒得同签名)。secret_key 必须 32 字节 seed,不符抛 error;message 任意长度(含空)
shield.crypto.ed25519_verify(public_key, message, signature)验签,返回 boolean。public_key 必须 32 字节原始公钥(不符抛 error);signature 非 64 字节或验签不过一律返回 false(不抛 error,JWT 式调用方直接映射为拒绝)

正确性锚点:base64/base64url 用 RFC 4648 向量、SHA-256 用 RFC 6234 向量、HMAC-SHA256 用 RFC 4231 TC1–TC4、PBKDF2 用 canonical SHA-256 向量组(iterations 1/2/4096 + 多块 40 字节例)、HKDF 用 RFC 5869 TC1–TC3、AES-256-GCM 用 NIST GCM 已知向量 + 认证失败拒绝矩阵锁定、Ed25519 用 RFC 8032 §7.2 TEST 1 + §7.3 EXAMPLE 逐字节锁定 + 篡改/错钥/坏长度拒绝矩阵(tests/coverage/test_cov_lua_crypto.cpp)。

jwt.lua —— 业务层认证参考实现 ​

scripts/lib/jwt.lua 是纯 Lua 的 JWT(RFC 7519)参考实现,支持 HS256 与 EdDSA(Ed25519)两种算法,并带 RFC 7517 JWKS 公钥集助手。完全由 shield.crypto 原语拼成,展示"业务层自建认证"的正确姿势(runtime 不含 JWT 语义;C++ 插件层的 plugins/auth_jwt 已因此弃用):

lua
local jwt = dofile("scripts/lib/jwt.lua")

-- HS256 签发:claims 表 + 共享密钥
local token = jwt.sign(
    { sub = "player_1", iss = "gate", exp = os.time() + 3600 },
    "shared-secret")

-- EdDSA 签发:claims 表 + 32 字节 RFC 8032 seed,kid 随 header 走
local token = jwt.sign(claims, seed, { alg = "EdDSA", kid = "k1" })

-- 校验:通过返回 claims,失败返回 nil, code, message
local claims, code = jwt.verify(token, "shared-secret",
                                { issuer = "gate" })
-- EdDSA 校验:key 是 32 字节原始公钥,alg 必须显式钉 EdDSA
local claims = jwt.verify(token, pub, { alg = "EdDSA", kid = "k1" })
if not claims then
    -- code ∈ "malformed" | "unsupported_alg" | "bad_kid" | "bad_signature"
    --      | "expired" | "not_yet_valid" | "bad_issuer" | "bad_audience"
    shield.log.warn("token rejected: " .. code)
end
verify 选项说明
alg接受的算法(默认 "HS256";EdDSA 传 "EdDSA",key 为 32 字节原始公钥)
kidpin:header kid 不符即拒绝(bad_kid)
now覆盖当前时间(unix 秒;默认 os.time(),受业务时钟挂钩影响)
leewayexp/nbf 容差秒数(默认 0)
issuer校验 iss 精确匹配
audience校验 aud(string 或 array 形态均可)

安全语义:签名用 constant_time_compare(HS256)或 Ed25519 常数时间原语比较;alg 钉到调用方接受的算法(opts.alg,默认 HS256——alg:"none"/算法混淆 token 在信任任何 claim 之前即拒绝);opts.kid pin 拒绝 header kid 不符的 token;exp/nbf 齐全;头/载荷 JSON 编解码内置于该文件(对象键排序输出,token 字节可复现)。

JWKS 公钥集(RFC 7517 / RFC 8037) ​

EdDSA 侧的公钥下发/轮换用 JWKS 文档(kty=OKP、crv=Ed25519、x、kid):

lua
-- 构造:keys 是非空数组,每项 { kid = <非空 string>, seed = <32 字节 seed> }
local doc = jwt.jwks_build({ { kid = "k1", seed = seed } })
-- doc 含 {"keys":[{"kty":"OKP","crv":"Ed25519","x":"<b64url 公钥>","kid":"k1","use":"sig","alg":"EdDSA"}]}

-- 解析:返回 { { kid = string|nil, public_key = <32 字节原始公钥> }
local keys = jwt.jwks_parse(doc)
-- 非 OKP/非 Ed25519 条目跳过(混合文档可解析);声称 Ed25519 但 x 坏 → 整文档 malformed

jwks_build 的 seed 经 ed25519_public_key 推导 x;jwks_parse 的 public_key 可直接喂给 jwt.verify 的 EdDSA 路径。


Plugin-provided APIs ​

数据库、缓存、消息队列、监控、健康检查、匹配等后端能力由插件提供。认证/JWT 属于业务层策略,不作为 Shield 官方插件发布。插件的 Lua 绑定跟随插件目录,通过 register_lua 钩子注册到 shield.<namespace>。host 端 src/lua/lua_api.cpp 不感知任何具体插件 API。

调用形态 ​

每个插件 namespace 是一个 callable table:传 binding 逻辑名返回绑定到目标 instance 的 proxy。binding 来自主配置 plugins.bindings,host 通过 binding 解析到 instance,再向该 instance 取得对应 interface。binding 是必填参数——省略或解析失败一律软失败(nil, { code = "module_unavailable" }),不存在隐式默认 binding。

lua
-- 显式指定 binding 逻辑名
local db       = shield.database.mongodb("database.default")
local db_audit = shield.database.mongodb("document.audit")
local db_game  = shield.database.mysql("database.default")

-- 在 proxy 上调用方法
db:insert_one("users", { name = "alice", age = 30 })
db_game:query("SELECT * FROM players WHERE id = ?", { pid })

规则:

规则说明
binding 逻辑名业务代码必须传 binding 逻辑名,不得传 instance_id(禁止 shield.database.mysql("db.main") 这类直接传实例 id 的写法)
协程友好API coroutine-friendly
错误返回未启用对应插件、binding 不存在或目标实例未启动时返回 nil, { code = "module_unavailable" }
多实例同一 package 多实例互不影响;每个 proxy 独立持有连接句柄
生命周期proxy 的生命周期由 Lua GC 管理;连接池归插件 instance 自治,proxy 只持有插件定义的轻量引用或临时连接句柄

设计理由:binding 是部署可变的逻辑引用,instance_id 是部署细节;设计理由与命名规则见 插件系统 · 为什么用 binding。

Database(SQL)— shield.database.<driver> ​

lua
local db = shield.database.mysql("database.default")

-- SQL CRUD(协程派发内挂起让位,见"执行模型")
local ok, rows   = db:query("SELECT * FROM users WHERE id = ?", { uid })
local ok, row    = db:query_one("SELECT * FROM users WHERE id = ?", { uid })  -- 无行时 ok=true, row=nil
local ok, result = db:execute("UPDATE users SET name = ? WHERE id = ?", { name, uid })
-- result = { affected = N }(sqlite / mysql 还带 last_insert_id)

-- 事务:body 收 tx 句柄;成功时第二个返回值是 body 的首返回值,不是错误表
local ok, updated = db:transaction(function(tx)
    local ok2, n = tx:execute("UPDATE users SET gold = gold - ? WHERE id = ?", { 10, uid })
    if not ok2 then return false, n end  -- 首返回值 false → ROLLBACK
    return true, n                        -- → COMMIT,n 透传给外层
end)

执行模型(三驱动一致,机制见 DB 异步入口):

规则说明
异步挂起协程派发内的 query / query_one / execute / transaction 挂起当前协程,SQL(含连接池 acquire)在插件 worker 线程执行,service 继续处理其他消息。db:query(...) 冒号与 db.query(...) 点号两种调用形状等价
同步回退非协程上下文(如 on_init)、实例配置 async: false、或 host 缺异步原语时,回退为同步内联执行——返回形状不变,但按原语义阻塞 VM
挂起预算调用方挂起时长上限 = 实例 call_timeout_ms(未配置默认 query_timeout_ms + 500);显式配成 ≥ query_timeout_ms 时启动告警
超时形状预算到期返回 false, { code = "timeout", retryable = true }。超时不等于取消:worker 上的执行照常结束,迟到完成被丢弃(插件负责回收连接与资源,并发 WARN 日志)
可观测在途异步调用与事务持连经 shield.pool.stats.v1 上报:/ops/metrics 的 shield_plugin_db_pending_async / shield_plugin_db_holding

事务契约:

规则说明
返回形状成功返回 true, <body 返回 values...>(body 无返回值时仅 true);只有首值为 false 时第二值才是 {code, message} 错误表
显式回滚body 首返回值为 false → ROLLBACK,false, { code = "transaction_rolled_back", message = "callback returned false" }
body 抛错body raise → 先 ROLLBACK,false, { code = "transaction_rolled_back", message = "callback raised an error" }
COMMIT 失败提交本身失败(死锁、连接断)返回驱动错误码,如 transaction_aborted
tx 句柄方法tx:query / tx:query_one / tx:execute,返回形状与池入口完全一致,语句跑在本事务持有的那条连接上
硬规则(运行时强制)body 内从同一协程调用池级 db:query / db:query_one / db:execute 或嵌套 db:transaction 立即 raise——异步事务挂起期间独占一条连接,body 内可让出的点必须都有超时硬顶
硬规则(评审级)body 内 shield.call / shield.sleep 禁止:那会在持连期间引入无界挂起点。运行时挡不住所有函数引用,这条留在代码评审
超时回收调用方预算到期时,插件自动把 ROLLBACK 排进该事务的串行队列,连接照常回池;对已终止事务的后续 token 操作返回 transaction_closed

可用 namespace:

Namespace接口说明
shield.database.sqliteshield.database.v1嵌入式 SQLite
shield.database.mysqlshield.database.v1MySQL(MariaDB Connector/C)
shield.database.postgresqlshield.database.v1libpq

Database(文档)— shield.database.mongodb ​

lua
local mongo = shield.database.mongodb("document.default")

-- CRUD
mongo:insert_one("users", { _id = "alice", age = 30 })
mongo:insert_many("logs", { { ts = 1 }, { ts = 2 } })

local ok, cursor = mongo:find("users", { age = { ["$gt"] = 18 } })
for _, doc in ipairs(cursor:to_table()) do
    shield.log.info(doc._id)
end

local ok, updated = mongo:update_one("users",
    { _id = "alice" },
    { ["$set"] = { age = 31 } })

local ok, deleted = mongo:delete_one("users", { _id = "alice" })
local ok, count = mongo:count("users", { active = true })

-- 聚合管道
local ok, cursor = mongo:aggregate("users", {
    { ["$match"] = { active = true } },
    { ["$group"] = { _id = "$country", total = { ["$sum"] = 1 } } },
})

-- 索引
mongo:create_index("users", { email = 1 }, { unique = true })

-- 事务(需 MongoDB 4.0+ replica set)
mongo:transaction(function(tx)
    tx:insert_one("orders", { ... })
    tx:update_one("inventory", { sku = "X" }, { ["$inc"] = { qty = -1 } })
end)

说明:

  • Lua table 会被 nlohmann::json 转为 BSON
  • 查询操作符($gt / $set / $sum 等)保持 Mongo 原生语义
  • ObjectId 作为 24 字符十六进制字符串传输

Cache — shield.cache.redis ​

lua
local cache = shield.cache.redis("cache.session")

cache:set("player:" .. uid, json, 3600)
local ok, value = cache:get("player:" .. uid)
cache:del("player:" .. uid)
cache:incr("counter:" .. key)
cache:hset("session:" .. sid, "uid", uid)
local ok, uid = cache:hget("session:" .. sid, "uid")

Queue — shield.queue.redis ​

lua
local q = shield.queue.redis("queue.events")

q:publish("chat.world", payload)
q:subscribe("chat.world", function(channel, data)
    -- 处理消息
end)
q:unsubscribe("chat.world")

Leaderboard — shield.leaderboard.redis ​

lua
local lb = shield.leaderboard.redis("leaderboard.default")

lb:create_board("arena_1v1", {
    fields = { { name = "score", dir = "desc" } }
})
lb:set_entry("arena_1v1", "alice", { score = 1500 })
local ok, rank = lb:get_rank("arena_1v1", "alice")
local ok, top = lb:top_n("arena_1v1", 10)

Metrics — shield.metrics ​

lua
local m = shield.metrics("metrics.default")

-- (name, value[, labels]),labels 是字符串键值表;记录落在插件实例的
-- 内存 registry,下一次 /metrics scrape 可见(exporter 细节见
-- docs/plugins/metrics-prometheus.md)
m:counter("kickstart_heartbeats_total", 1, { note = "alive" })
m:gauge("kickstart_active_players", 42)
m:histogram("kickstart_request_duration_ms", 12.5)

方法面与 shield.metrics.v1 C vtable 一一对应(counter / gauge / histogram)。冒号与点号两种调用形状等价;成功返回 true,labels 含非字符串键/值返回 false, { code = "invalid_labels", ... }。

其他插件 ​

当前已稳定接入 Lua 的插件 namespace,应以各插件文档和实际 register_lua 实现为准。

以下 namespace 当前仍属于规划中,不要视为已经可用的稳定 Lua API:

Namespace说明当前状态
shield.health.http注册健康检查、查询状态规划中,当前 register_lua 未实现
shield.matchmaking.elo匹配队列、ELO 评分规划中,当前 register_lua 未实现

说明:如果后续补齐这些 Lua API,也应优先建立在基础组件与插件接口之上,而不是直接把上层策略塞进 core。相关基础组件方向目前只保留为后置草案,见 基础组件与运行时适配边界。

具体方法签名参考各插件的 lua/ 封装文件、plugins/<package>/lua_bindings.cpp 或对应插件文档中的”规划中”章节。


Client RPC API ​

客户端 RPC 不是通用 Lua 消息入口。每个 RPC 由 RPC descriptor(actors[].rpc.routes)定义 route_id、方向、schema 与 binding;目标 Service VM 在启动期把它编译为缓存 handler, s2c 路由则在同一时刻把 shield.client_rpc.<binding> helper 注册进该 VM。

业务 module 只实现具体方法:

lua
local M = {}
local state

function M.move(ctx, client, request)
    assert(client:player_id() == state.player_id)

    state.position = {
        x = request.x,
        y = request.y,
        z = request.z,
    }

    return shield.client_rpc.move_result(client, {
        accepted = true,
    })
end

return M

示例中的 M.move 由 descriptor 的 binding 在启动期绑定到对应 client-to-server RPC。 runtime 热路径只做 route_id -> cached function,不按字符串反射查 module,也不调用 通用 client-message handler。

Handler 参数 ​

客户端 RPC handler 的固定参数是:

text
handler(ctx, ClientContext, decoded RPC arguments)

实现状态(M3):入站与出站均为最终形态。Gateway 将校验过的入站打包为 typed ClientIngress CAF 消息发送到 session 当前 target 的 actor;目标 VM 按 spawn 期编译的 route 表以 handler(ctx, client, request) 直接分发(热路径 只做 route_id -> cached function)。身份与出站:ClientContext/ClientRef 只读 userdata、shield.client.bind/close、按 descriptor 自动注册的 shield.client_rpc.<name> helper 均已可用。

如果该 RPC 的 request schema 生成单个 request table,则 Lua 形态为 handler(ctx, client, request);若生成多个参数,则按生成契约传入。route、header、codec 和原始 body 都不是业务参数。

ClientContext 与 ClientRef ​

ClientContext 在 __shield_client_ref 标记随消息到达时物化为只读 userdata, ClientRef 是 shield.client.bind 的返回值。两者共用同一套只读属性:

lua
client:player_id()          -- 可信身份;预登录为空字符串
client:session_id()         -- Gateway 侧连接 id
client:session_epoch()      -- 绑定纪元;stale 引用会比对失败
client:protocol_profile_id()
client:gateway()            -- gateway actor 名(egress 回程地址)

client:ref()                -- 仅 ClientContext:派生可传递的 ClientRef

userdata 不可由 Lua 构造(no_constructor),也不可变;作为 service 消息参数 传出时自动序列化回标记形态,在对端 VM 中再次物化。

shield.client.bind / close ​

session 绑定是单一 target:认证前指向 listener 的 auth 入口服务,认证成功后原子 切换到 PlayerService。不存在多服务绑定表;room/scene/map 等动态路由属于 PlayerService 私有状态,用 shield.send/call 转发。

lua
-- 认证服务内(必须运行在 handler 协程中):
-- 挂起当前协程,CAS 替换 session 绑定,恢复时返回 (true, ClientRef) 或 (false, error)
local ok, ref = shield.client.bind(client, player_id, "player")
if not ok then
    -- ref = {code = "client_rpc.epoch_expired", message = ...}
    return {code = ref.code}
end

-- 主动踢下线:失效绑定 + 移除注册 + 关闭 socket(reason 为空时记为 "kicked")
shield.client.close(ref, "kicked")
规则说明
协程要求shield.client.bind 只能在 handler 协程中调用;主协程/on_exit 返回错误表
单一 target绑定只有一个 target service;成功后 epoch 递增,旧引用立即失效
CAS 失败码绑定失败统一返回 client_rpc.epoch_expired(session 消失或 epoch 过期)
bind 挂起bind 挂起协程等待 gateway 应答,超时(5s)由 CAF delayed driver 触发失败恢复
close 默认原因shield.client.close(ref) 不传 reason 时按 "kicked" 关闭

出站 response 与 push ​

服务端只能调用按 s2c descriptor 自动注册的具体 server-to-client RPC helper:

lua
shield.client_rpc.move_result(client, {
    accepted = true,
})

shield.client_rpc.member_joined(client_ref, {
    player_id = member_id,
})

helper 在 spawn 期绑定 s2c route_id(第一个参数接受 ClientContext 或 ClientRef, 第二个参数是 table(结构化消息)或 string(原始字节)),fire-and-forget;Gateway 在 写回前校验 registry 命中、epoch 相等、owner player_id 与路由方向,拒绝只 warn + 计数, 不排队不重试。

规则:

规则说明
禁止通用 API不提供接受 route 字符串、裸 route_id 或通用 envelope table 的发送 API
route 位置route 信息写入 wire header,不写入 body
语义区分response 与 push 使用同一 ClientEgress 路径,由不同 RPC method 语义区分
Gateway 校验Gateway 写回前校验 session id、epoch 和 owner;stale ClientRef 丢弃并计数
Phase 1 限制第一版不提供通用 client.call、seq pending map、future 或自动 timeout correlation

示例:认证服务绑定 player ​

lua
-- auth 服务:routed login(c2s descriptor binding = "login")
function M.login(ctx, client, request)
    local player_id = authenticate(request)
    if not player_id then
        return {code = "auth_failed"}
    end

    -- 挂起协程;gateway CAS 成功后恢复并拿到 ClientRef
    local ok, ref = shield.client.bind(client, player_id, "player")
    if not ok then
        return {code = ref.code}
    end

    -- 新 target(此处即本服务)与客户端都收到登录结果
    shield.client_rpc.login_result(ref, {player_id = ref:player_id()})
    return {code = "ok"}
end

启动校验 ​

Service VM 启动时必须失败于以下情况:

校验项说明
缺失 bindingdescriptor 声明该服务的 client-to-server RPC,但 binding 缺失
函数不存在binding 指向的 Lua function 不存在或不是 function(handler_missing)
重复绑定同一 route_id 或同名 descriptor 重复声明
方向不匹配s2c 路由不会编译为入站 handler;c2s 路由不会注册出站 helper
owner 不匹配owner_service 指向其他 actor 的条目不在本 VM 编译

说明:network.protocol.routes(logical_service/method 形态)已在 M1 移除; descriptor 是路由的唯一来源,见 RPC descriptor。

HTTP API ​

HTTP 客户端 (shield.http) ​

基于 libcurl,支持 HTTPS、HTTP/2、重定向、代理、文件上传/下载、Basic/Bearer 认证与可配置重试。连接池与 Cookie 会话当前未实现(每次请求独立连接)。

基础请求 ​

lua
-- GET
local res = shield.http.get("https://api.example.com/health")

-- POST JSON
local res = shield.http.post("https://api.example.com/data", '{"key":"value"}')

-- PUT / DELETE / PATCH
local res = shield.http.put("https://api.example.com/users/1", '{"name":"test"}')
local res = shield.http.delete("https://api.example.com/users/1")
local res = shield.http.patch("https://api.example.com/users/1", '{"name":"new"}')

完整请求(所有选项) ​

lua
local res = shield.http.request("https://api.example.com/users", {
    method = "POST",
    body = '{"name":"test"}',
    headers = {["X-Custom"] = "value"},
    timeout = 10,
    -- 认证
    auth_bearer = "eyJhbGciOi...",           -- Bearer token
    auth_basic = {user="admin", password="secret"},  -- Basic auth
    -- 代理
    proxy = "http://proxy:8080",
    -- SSL
    verify_ssl = true,
    -- 重试
    retry = 3,
    retry_delay = 1000,
    -- 重定向
    follow_redirects = true,
    max_redirects = 5,
})

JSON 便捷方法(自动序列化/反序列化) ​

大部分 HTTP 请求都是 JSON,以下方法自动将 Lua table 序列化为请求体,响应自动解析 data 字段:

lua
-- 最常用:POST JSON(等价于 json_post)
local res = shield.http.json("https://api.example.com/users", {
    name = "test",
    age = 25,
})
-- res.data 自动解析为 Lua table
print(res.data.name)  -- "test"

-- PUT JSON
local res = shield.http.json_put("https://api.example.com/users/1", {
    name = "updated",
})

-- PATCH JSON
local res = shield.http.json_patch("https://api.example.com/users/1", {
    name = "patched",
})

-- 带认证的 JSON 请求
local res = shield.http.json("https://api.example.com/pay", {
    amount = 100,
    currency = "CNY",
}, {
    auth_bearer = "eyJ...",
    timeout = 5,
})

返回值额外字段:

字段类型说明
datatable自动解析的 JSON 响应体(Content-Type 为 JSON 或 body 以 {/[ 开头时)

文件上传(multipart/form-data) ​

lua
local res = shield.http.upload("https://api.example.com/upload", {
    {field_name = "avatar", file_path = "/tmp/photo.png", content_type = "image/png"},
    {field_name = "doc", file_path = "/tmp/report.pdf"},
}, {
    user_id = "12345",
    description = "Profile photo",
}, 60)

文件下载 ​

lua
local res = shield.http.download("https://example.com/data.zip", "/tmp/data.zip", 60)
if res.ok then
    print("Downloaded: " .. res.status)
end

表单提交(application/x-www-form-urlencoded) ​

lua
local res = shield.http.post_form("https://api.example.com/login", {
    username = "admin",
    password = "secret",
})

返回值字段 ​

字段类型说明
statusnumberHTTP 状态码
bodystring响应体
okbooleanstatus >= 200 && status < 400
errorstring错误信息(成功时为空)
headerstable响应头

规则:

规则说明
同步执行当前实现同步执行 libcurl 请求;如果在 handler 内调用,会阻塞当前 service actor(CAF scheduler 线程)。异步 HTTP 属于后续工作
协议支持支持 HTTP/1.1、HTTP/2、HTTPS(libcurl + OpenSSL/Schannel)
功能支持支持重定向、代理、Bearer/Basic auth、自定义 CA、SSL 校验开关、简单重试。连接复用依赖 libcurl easy handle 生命周期,当前未实现显式连接池
适用场景适合支付 API、webhook、REST API、文件传输等场景
实现快照(点击展开)

基于 libcurl 实现 HttpClient。HttpClient::initialize() 在 register_full_shield_api 时自动调用;proxy、auth_basic、verify_ssl、ca_cert_path、retry 与 retry_delay 会传递到 libcurl。

HTTP 服务端 (shield.httpd) ​

用于注册路由处理入站 HTTP 请求(管理端点、健康检查、webhook 接收等)。

lua
-- 注册 GET 路由
shield.httpd.get("/api/health", function(req)
    return { status = "ok" }
end)

-- 注册 POST 路由
shield.httpd.post("/api/data", function(req)
    return { created = true }
end)

-- 注册 PUT / DELETE / PATCH 路由
shield.httpd.put("/api/users/:id", function(req) end)
shield.httpd.delete("/api/users/:id", function(req) end)
shield.httpd.patch("/api/users/:id", function(req) end)

规则:

规则说明
handler 参数handler 接收 request table,返回 response table
路由注册时机服务 on_init 与运行期均可注册;同 method+path 后注册者覆盖先注册者,服务退出时其路由被移除
派发语义请求派发到注册服务的 actor 线程(与普通消息同一条串行路径),派发超时 2s 返回 504;服务不在时 503、路由不存在 404、handler 抛错 500(JSON {type="error", message=...})
无 middleware不提供 middleware chain
适用场景适合管理/运维端点,不适合高并发业务流量
实现快照(点击展开)

基于 Boost.Beast 的 HttpServer 提供 HTTP 服务。shield.httpd.* 将路由保存到 LuaRuntime 的注册表,bootstrap 的 LuaHttpBridge 把路由镜像进 HttpServer;请求经 enqueue_forked_task 派发到注册服务的 actor 线程上执行 Lua handler(与普通服务消息同一条串行派发路径,无 VM 竞争)。支持 :param 路径参数(如 /api/users/:id,通过 req.params 访问),req 携带 method/path/query/params/headers/body。handler 返回 nil(204)、字符串(text/plain)或 {status, body, headers} table(body 为 table 时 JSON 序列化)。要求在 http.enabled: true 时生效,且必须在服务上下文中注册(裸 VM 中调用会抛错)。


Player API ​

shield.player 是可选模块 shield_player 提供的玩家会话门面 (SHIELD_ENABLE_PLAYER 编译开关)。业务不直接操作 socket 或 session 表, 而是通过 shield.player.setup(M, opts) 一次性声明钩子,框架负责 认证、状态机(connecting → authenticating → online → ready)、 客户端消息准入、离线推送队列与重连窗口。

模块未编译时每个 shield.player.* 入口返回 nil + {code="module_unavailable"}, 而不是 nil 字段。

setup 与钩子 ​

lua
local M = {}

-- 必填钩子(缺任一 setup 失败: nil + {code="setup_invalid"})
function M.auth(ctx, client, request)
    -- 校验 request,返回 true, player_id 或 {player_id = ...}
    return true, request.player_id
end

function M.login(ctx, client, auth_result) end
function M.client_message(ctx, client, route_name, request)
    -- 返回 false, code 即拒绝该消息(丢帧 + warn + 计数)
    return true
end
function M.disconnect(ctx, client, reason) end
function M.logout(ctx, client, reason) end

-- 可选钩子(未提供时框架执行文档化默认实现;覆盖后默认不自动执行,
-- 需要保留时显式调用 shield.player.defaults.*)
function M.ready(ctx, client)
    return shield.player.defaults.ready(ctx, client)
end

local player = shield.player.setup(M, {
    instance_script = "scripts/player_instance.lua",  -- 每玩家实例脚本
    instance_routes  = { ... },                       -- 实例 VM 的 s2c 路由
})

-- 认证入口(在 pre-login 路由 handler 内调用)
local ok, client_ref = player:authenticate(ctx, client, request)
规则说明
钩子签名所有钩子首参为 dispatch ctx,随后是 client 与业务参数
认证流程auth hook → anonymous/spectator 开关 → PlayerManager 准入(单设备踢旧/多设备上限) → spawn player_<uid> 实例 → shield.client.bind
重连窗口断开后 player.reconnect_window_ms(默认 30000)内同 uid 登录复用实例,按序 flush 离线队列;超时 logout(reason="timeout")
客户端消息ready 前到达的业务消息被 guard 丢弃(not_ready 计数);ready 后先过 M.client_message
离线推送player:push 在线直发 s2c helper,离线入队(上限 player.message_queue_limit 默认 64,满则 offline_queue_full)
跨服务传值只传 PlayerRef / ClientContext / ClientRef,不传整个会话对象

P2 语法糖 shield.player.Base.setup(M, opts):钩子按 setup 字段名 (auth/login/client_message/disconnect/logout/ready/reconnect/ save,无 on_* 前缀)自动从 M 收集,不必在 opts 里列名;opts 只携带 非钩子选项(instance_script/instance_routes),显式给出的钩子函数 优先于同名模块方法。Base 是纯糖(OD-014):内部转调 setup,不引入 第二套 lifecycle、不做继承;必填钩子缺失同样 setup_invalid。

lua
local player = shield.player.Base.setup(M, {
    instance_script = "scripts/player_instance.lua",
})

门面方法(setup 返回值) ​

方法说明
player:authenticate(ctx, client, request)完整认证闭环;成功返回 true, 新鲜 ClientRef
player:push(target, route_name, payload)s2c 推送;target 为 uid 字符串或 PlayerRef
player:set_data(key, value) / player:get_data(key)实例内存状态(persistence 白名单字段之外不落盘)
player:session()只读快照 {uid, state, device_id, service_id} 或 nil
player:logout(reason)主动登出:unregister + logout hook + save

模块级函数 ​

函数说明
shield.player.resolve(ref)PlayerRef → 会话快照;失效 player_not_found,跨节点 remote_resolve_unimplemented
shield.player.get(uid)等价 manager.get
shield.player.config()生效配置快照(扁平键,如 message_queue_limit)
shield.player.stats(){rejected_not_ready, rejected_by_guard, offline_dropped} 计数
shield.player.now_ms()单调毫秒时钟
shield.player.node_info(){node_id, epoch}(epoch 为十进制字符串,uint64 不走 Lua double)
shield.player.Base.setup(M, opts)P2 语法糖:钩子按名从 M 收集后转调 setup(OD-014 边界)
shield.player.defaults.allow/ready/reconnect/save默认钩子实现,业务覆盖后显式调用

manager(uid 索引操作) ​

shield.player.manager.admit / register_session / mark_disconnected / mark_reconnected / get / get_devices / set_state / size / unregister 暴露 C++ PlayerManager 的原语(测试与运维用;业务认证走 authenticate)。

PlayerRef 与 ClientContext 的访问契约 ​

两类只读 userdata 的访问面不同,跨服务传值经 marker JSON 往返:

类型访问形式字段/方法
PlayerRef(ref.uid)属性式(record-like)uid / node_id / service_id / epoch
ClientContext / ClientRef(client:player_id())方法式player_id / session_id / session_epoch / protocol_profile_id / gateway(client.ref 亦为方法 client:ref())

错误码清单见 错误码参考 "六、shield_player 错误"; 模块契约详见 runtime-player.md。

实现快照(点击展开)

register_player_api 在每个 VM 注册 shield.player 表;orchestration chunk (kPlayerOrchestration)持有 per-VM 的 impl 状态(会话、离线队列、计数)。 认证准入与状态裁决在 C++ PlayerManager(进程级单例,admit 返回 admit/kick_old/restore/reject);ready 真相在 PlayerManager,guard 与 player:session() 都读它。实例 spawn 为 player_<uid> 独立服务,断开重连 窗口内 restore 复用,flush 走新鲜 bind 的 ClientRef(旧 epoch 的 egress 被 gateway 按 binding epoch 校验丢弃)。PlayerRef 跨服务序列化为 __shield_client_ref / __shield_player_ref marker JSON,接收端物化回 只读 userdata。


Server API ​

shield.server 是可选模块 shield_server 提供的服务器状态门面 (SHIELD_ENABLE_SERVER 编译开关)。它暴露服务器状态机查询与迁移、 运行时信息(uptime/version/node_id)和状态观察者注册。 业务语义(维护准入 gate、关服前的数据保存)由业务 service 在观察回调里实现。

模块未编译或未初始化时每个 shield.server.* 入口返回 nil + {code="module_unavailable"},而不是 nil 字段。

lua
local server = shield.server

-- 只读查询
local state = server:state()        -- "starting" | "running" | "maintenance" | "shutdown"
local up    = server:uptime()       -- running 起点的 steady 秒数
local ver   = server:version()      -- info.version → 回退 app.version → ""
local node  = server:node_id()      -- cluster node_id;standalone 为 ""
local at    = server:started_at()   -- running 起点的 wall-clock ms(未进入 running 为 0)
local cfg   = server:config()       -- {name=..., info={name=..., version=..., region=...}}

-- 状态控制
local ok, err = server:set_state("maintenance")  -- 非法迁移 → invalid_state_transition
local ok, err = server:shutdown(30000)           -- 30 秒后请求进程停止

-- 状态观察(回调签名 fn(ctx, new_state),与 handler dispatch 同规则)
local id = server:watch(function(ctx, new_state)
    if new_state == "shutdown" then
        -- 保存数据等关服准备
    end
end)
server:unwatch(id)

语义要点 ​

规则说明
状态机starting → running(bootstrap init complete 自动);running ⇄ maintenance;running/maintenance/starting → shutdown(终态);→ starting 恒非法
set_state未知状态名 invalid_state;非法迁移 invalid_state_transition;同值幂等成功且不通知
shutdown重复调用 shutdown_already_scheduled;参数非非负整数 invalid_argument;drain 预算仍归 bootstrap shutdown.timeout.*
watch同一 service 重复 watch 幂等(同 id);回调在观察者 actor 线程串行;观察者退出自动注销;unwatch 幂等

错误码清单见 错误码参考 "七、shield_server 错误"; 模块契约详见 runtime-server.md。

实现快照(点击展开)

register_server_api 在每个 VM 注册 shield.server 表;状态真相在 C++ ServerManager(进程级单例,global()/set_global(),bootstrap init complete 时 mark_ready)。watch 注册表在 C++ 侧只存 {watch_id, service_id},不持有 Lua 闭包;每 VM 编排层持有观察者函数并安装转发 handler,投递走 system 消息通道(send_system,与 gateway 桥同路径), 观察者 service 退出时按判活自动注销。shutdown(ms) 计时线程在 ServerManager 内部(std::jthread),到期调用与 SIGINT/SIGTERM 同路径 的进程停止请求。


Error Object ​

运行时错误统一返回只读 table:

lua
if not ok then
    shield.log.warn(err.code .. ": " .. err.message)
end

字段:

字段类型说明
codestring稳定错误码字符串
messagestring面向日志的错误说明
retryableboolean是否适合业务重试(已实现:timeout=true,其他=false)
detailany可选调试信息(Phase 2,当前未填充)

错误码清单见 错误码参考。

实现快照(点击展开)

Error Object 返回 code、message、retryable 三个字段。detail 属于 Phase 2 扩展,当前未填充。timeout 错误 retryable=true,其他错误 retryable=false。


删除的旧 API ​

以下旧 API 不进入重构目标,不保留兼容层:

旧 API说明
shield.service("name")旧服务构造函数
旧 Lua 插件执行模型旧 shield.plugin.list/by_type/loaded/capabilities
DI/IoC 注入 API依赖注入 API
annotation / condition API注解/条件 API
gateway middleware chain API网关中间件链 API
shield.db.*、shield.redis.*全局数据 API(包括 shield.db.query(...)、shield.redis.get(...))
shield.db:query(...)、shield.redis:get(...)旧冒号形式
on_message(src, msg_type, data)handler 形如 on_message(src, msg_type, data) 的统一入口

说明:插件系统 v1 会重新引入只读 introspection API:shield.plugin.packages()、shield.plugin.instances()、shield.plugin.instance(id)、shield.plugin.binding(name)。这些 API 只查询插件 catalog/runtime state,不提供 Lua 插件执行能力,也不暴露 native vtable。

Apache License 2.0