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 文件:
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 |
| 普通函数即 method | table 上的普通函数就是可被 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 前调用。
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 广播。
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)
服务停止前调用。
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 抛出错误时调用。
function M.on_error(err, context)
shield.log.error(context.type .. ": " .. tostring(err))
endcontext 字段:
| 字段 | 类型 | 说明 |
|---|---|---|
type | string | handler、timer、fork、event |
method | string | handler method 名称,非 handler 时为空 |
event | string | shield.event 事件名,仅 type="event" 时存在 |
trace_id | string | 可选 trace id |
panic 策略:
on_error 不改变服务状态。Phase 1 的 panic 策略固定为:同一 service 连续 handler/timer/fork 未捕获错误达到固定阈值 10(kDefaultMaxErrorsBeforePanic,当前无配置键)时进入 panic。
实现快照:
on_error/on_panichook 已实现。当 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)
服务进入不可恢复状态前调用。
function M.on_panic(reason, context)
shield.log.error("panic: " .. reason)
end规则:
| 规则 | 说明 |
|---|---|
| 禁止挂起 | 不允许挂起 |
| 允许操作 | 只能做同步、best-effort 的紧急记录或状态标记 |
| 返回值 | 返回值会被忽略 |
Service API
shield.spawn(module, opts)
local h, err = shield.spawn("player", {
name = "player.1001",
args = { uid = 1001 },
timeout = 10000,
})参数:
| 参数 | 类型 | 说明 |
|---|---|---|
module | string | actor 类型或脚本别名,由 actors[].name 映射 |
opts.name | string | 可选 service name,本 runtime 内唯一 |
opts.args | table | 传给 on_init(args).args 的业务参数 |
opts.timeout | number | 覆盖 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)
shield.panic("unrecoverable state")业务主动触发 panic 路径:调用当前 service 的 on_panic(reason, {type="explicit"}) hook(best-effort),随后以 reason "panic" 退出当前 service。只能在 service dispatch 上下文中调用,否则为 no-op。
shield.exit(reason)
shield.exit("normal")停止当前 service。reason 可省略,默认为 normal。
shield.self()
local me = shield.self()返回当前 service 的 ServiceHandle。只能在 service coroutine 中调用。
ServiceHandle 方法:
| 方法 | 返回值 | 说明 |
|---|---|---|
h:id() | number | 本地 service id |
h:node() | number | node id(单节点 runtime 恒为 0,即本地) |
h:valid() | boolean | 检查 handle 非空 |
shield.names()
local names = shield.names()返回本地 registry 中所有已发布服务 name 的列表(全局视图,不按调用者过滤)。
shield.query(name)
local h, err = shield.query("gateway.main")查询本地 registry。cluster 全局发现不进入 core。
shield.register(name)
local ok, err = shield.register("gateway.public")给当前 service 发布一个本地 name。
shield.unregister(name)
local ok, err = shield.unregister("gateway.public")注销当前 service 拥有的 name。
shield.claim(name)
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, ...)
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, ...)
local ok, profile = shield.call("player.1001", "get_profile", uid)规则:
| 规则 | 说明 |
|---|---|
| 挂起行为 | 挂起当前 Lua coroutine |
| 阻塞 | 不阻塞 CAF scheduler 线程 |
| 超时 | 使用默认 call timeout |
| 成功返回 | true, ...callee_returns |
| 失败返回 | false, Error |
业务返回 nil 或 false 时仍是成功:
-- callee
function M.check(uid)
return false, "banned"
end
-- caller
local ok, allowed, reason = shield.call("auth", "check", uid)
-- ok == true, allowed == falseshield.call_timeout(timeout_ms, target, method, ...)
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
function M.handler(ctx, ...)
local src = ctx.sender
local trace = ctx.trace
local deadline = ctx.deadline
end规则:
| 规则 | 说明 |
|---|---|
| 只读 | ctx 是只读对象,包含当前消息上下文 |
| 有效范围 | 只在 message handler coroutine 中有效 |
| 生命周期 | handler 返回后上下文失效 |
| timer/fork | timer 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)
local id = shield.timer_once(1000, function()
shield.send("room.1", "tick")
end)shield.timer(interval_ms, callback)
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)
local ok, err = shield.cancel_timer(id)shield.sleep(delay_ms)
shield.sleep(100)在协程上下文(message handler、timer/fork callback、on_init、on_shutdown)中挂起当前 coroutine,不阻塞 runtime thread;仅在 module-level 代码等无协程上下文中退化为阻塞线程的 _block_sleep。
shield.fork(fn)
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
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
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
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 编排。
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 已因此弃用):
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 字节原始公钥) |
kid | pin:header kid 不符即拒绝(bad_kid) |
now | 覆盖当前时间(unix 秒;默认 os.time(),受业务时钟挂钩影响) |
leeway | exp/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):
-- 构造: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 坏 → 整文档 malformedjwks_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。
-- 显式指定 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>
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.sqlite | shield.database.v1 | 嵌入式 SQLite |
shield.database.mysql | shield.database.v1 | MySQL(MariaDB Connector/C) |
shield.database.postgresql | shield.database.v1 | libpq |
Database(文档)— shield.database.mongodb
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
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
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
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
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 只实现具体方法:
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 的固定参数是:
handler(ctx, ClientContext, decoded RPC arguments)实现状态(M3):入站与出站均为最终形态。Gateway 将校验过的入站打包为 typed
ClientIngressCAF 消息发送到 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 的返回值。两者共用同一套只读属性:
client:player_id() -- 可信身份;预登录为空字符串
client:session_id() -- Gateway 侧连接 id
client:session_epoch() -- 绑定纪元;stale 引用会比对失败
client:protocol_profile_id()
client:gateway() -- gateway actor 名(egress 回程地址)
client:ref() -- 仅 ClientContext:派生可传递的 ClientRefuserdata 不可由 Lua 构造(no_constructor),也不可变;作为 service 消息参数 传出时自动序列化回标记形态,在对端 VM 中再次物化。
shield.client.bind / close
session 绑定是单一 target:认证前指向 listener 的 auth 入口服务,认证成功后原子 切换到 PlayerService。不存在多服务绑定表;room/scene/map 等动态路由属于 PlayerService 私有状态,用 shield.send/call 转发。
-- 认证服务内(必须运行在 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:
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
-- 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 启动时必须失败于以下情况:
| 校验项 | 说明 |
|---|---|
| 缺失 binding | descriptor 声明该服务的 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 会话当前未实现(每次请求独立连接)。
基础请求
-- 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"}')完整请求(所有选项)
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 字段:
-- 最常用: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,
})返回值额外字段:
| 字段 | 类型 | 说明 |
|---|---|---|
data | table | 自动解析的 JSON 响应体(Content-Type 为 JSON 或 body 以 {/[ 开头时) |
文件上传(multipart/form-data)
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)文件下载
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)
local res = shield.http.post_form("https://api.example.com/login", {
username = "admin",
password = "secret",
})返回值字段
| 字段 | 类型 | 说明 |
|---|---|---|
status | number | HTTP 状态码 |
body | string | 响应体 |
ok | boolean | status >= 200 && status < 400 |
error | string | 错误信息(成功时为空) |
headers | table | 响应头 |
规则:
| 规则 | 说明 |
|---|---|
| 同步执行 | 当前实现同步执行 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 接收等)。
-- 注册 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 与钩子
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。
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 字段。
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:
if not ok then
shield.log.warn(err.code .. ": " .. err.message)
end字段:
| 字段 | 类型 | 说明 |
|---|---|---|
code | string | 稳定错误码字符串 |
message | string | 面向日志的错误说明 |
retryable | boolean | 是否适合业务重试(已实现:timeout=true,其他=false) |
detail | any | 可选调试信息(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。