Skip to content

消息运行时语义 ​

本文档包含 Shield 消息通信相关的运行时语义决策。

MessageEnvelope ​

MessageEnvelope 是 runtime 内部信封,Lua 用户不直接构造。

注:下面的 C++ 结构是概念模型,用于说明字段语义;实际实现见 include/shield/core/service_message.hpp(ServiceMessage / CallResponseMessage)。service call 的请求-响应关联由 LuaServiceManager 基于 call_session 与 CAF actor 消息、coroutine pending/resume 维护,不在 Lua API 中暴露。

cpp
enum class MessageKind {
  Send,
  CallRequest,
  CallResponse,
  System,
};

struct MessageEnvelope {
  MessageKind kind;
  ServiceAddress src;
  ServiceAddress dst;
  uint64_t request_id;
  Deadline deadline;
  TraceContext trace;
  MessageFlags flags;
  MessagePayload payload;
};

规则:

  • src 由 runtime 填充,Lua 不允许伪造。
  • Send 的 request_id 为 0。
  • CallRequest 和 CallResponse 使用非 0 request_id。

服务间 vs 客户端:上述 request_id 关联仅用于 service ↔ service 的 shield.call。客户端 ↔ gateway 之间没有框架级 RPC:PacketKind/seq 是 wire 层字段,当前无端到端语义,不构成 client.call、不维护 seq pending map。客户端 request/response/push 的 correlation token 由业务 payload 字段自管(见 架构决策记录 AD-05、网关设计)。

  • deadline 本地用 monotonic time。
  • 跨节点传输时发送 remaining timeout,不发送本机 monotonic deadline。
  • trace 用于 ops、profile、慢调用定位。

顺序保证:

txt
同一个 src -> dst 的本地消息按发送顺序入队。
不同 src 之间不保证全局顺序。
跨节点只保证同一连接内写入顺序。

ClientIngress / ClientEgress ​

客户端 RPC 在 Gateway 与目标 Service 之间传递的内部消息。

ClientIngress ​

客户端 RPC 进入目标 actor 时使用(自架构纠偏 M3 起为入站唯一入口):

cpp
struct ClientIngress {
  ClientContextData context;      // {gateway_address, session_id, session_epoch,
                                  //  player_id, protocol_profile_id}
  uint32_t route_id = 0;          // 来自 wire header
  std::vector<uint8_t> body_bytes;  // 纯业务数据,原样传递
  std::optional<nlohmann::json> decoded_request;  // pipeline codec 产出的规范 JSON
};
  • 这是 CAF/Shield runtime 内部消息,不是客户端 wire 格式,也不是 Lua API。
  • route_id 来自 wire header 的 frame decode,不是从 body 提取。
  • body_bytes 是 wire frame 的 body 部分。handler 的 request 值按 descriptor 契约成形:decoded_request 优先;否则 request_codec 为空或 json 的 route 由目标 VM 把 body_bytes 按 JSON 解码(失败回退原始字节字符串);其他 request_codec(如 raw)始终传原始字节字符串。
  • 目标 VM 按 spawn 期编译的 route 表把 route_id 直接分发到 handler(ClientContext, request)。
  • CAF behavior 按内部消息类型区分 ClientIngress、普通 service send/call、 生命周期控制等。

session 生命周期(接入/断开/被踢)以 typed ClientControlMessage 传递, Lua 侧约定:Bound → on_client_bound(ctx, client),Disconnected → on_disconnect(ctx, client, reason),Unbound → on_client_unbound(ctx, client, reason),Reconnected 预留(当前警告并丢弃)。

玩家重连窗口不经由 Reconnected 控制消息:shield_player 在认证路径内 识别同 uid 的存活会话并 restore(复用实例、按序 flush 离线队列), 见 runtime-player.md 与 lua-api.md "Player API"。

ClientEgress ​

服务端发送 response/push 到 Gateway 时使用:

cpp
struct ClientEgress {
  ClientContextData context;      // 携带 session_id + session_epoch
  uint32_t route_id = 0;          // 由 shield.client_rpc.<name> helper 绑定
  std::vector<uint8_t> body_bytes;  // 按路由 schema 编码的业务数据
  std::optional<nlohmann::json> message;
};
  • Gateway actor 收到后校验 session 存在、存活、epoch 相等、owner 匹配,把 route_id 写入 wire header,body_bytes 作为 body。

客户端 RPC 状态码 ​

当前只有 client_rpc.epoch_expired 是真实回传的结构化状态码,其余各项没有结构化错误回传:

状态码说明当前实现
client_rpc.not_authenticated未认证 session 访问需认证 RPC未回传结构化码:记 WARN 并丢弃消息
client_rpc.route_not_found未知 route_id未回传结构化码:WARN + 计数(egress_route_not_found)
client_rpc.direction_rejected方向不匹配未回传结构化码:WARN + 计数(egress_direction_rejected)
client_rpc.epoch_expiredsession epoch 过期已实现的结构化状态码
client_rpc.handler_missing目标 VM 缺少 handler binding非 RPC 回传码:spawn 期绑定失败时的错误文本

MessagePayload ​

实现状态:本节的 MessagePayload/PayloadCodec/LuaRequestPayload 结构当前不存在于源码,属于格式规格预留。消息路径实际使用 nlohmann::json 序列化(只校验 1MB 消息大小与 unsupported 值,无嵌套深度检查)。LuaPackEncoder/LuaPackDecoder 存在但当前只被测试引用,未接入消息路径;encode_failed 仅由 unsupported 类型映射产生。

core 中 payload 是不可变二进制 buffer。

cpp
enum class PayloadCodec : uint16_t {
  LuaPack = 1,
  RawBytes = 2,
};

struct MessagePayload {
  PayloadCodec codec;
  uint16_t version;
  ByteBuffer bytes;
};

Lua 默认编码:

txt
LuaRequestPayload {
  method: string
  argc: uint32
  args: LuaValue[]
}

支持类型:

类型支持
nil是
boolean是
integer是
number是
string是
table有限制
ServiceHandle是,作为扩展类型
function否
thread/coroutine否
普通 userdata否
循环引用 table否

table 规则:

txt
array table: 连续整数 key 1..n
map table: string/integer key
禁止: table/function/userdata key
默认最大嵌套深度: 64

本地消息也需要序列化,不直接传 Lua 对象指针。优化只能共享 immutable ByteBuffer,不能改变语义。

LuaPack 序列化格式 ​

实现状态:LuaPack 是 Shield 内置的二进制序列化格式规格,但当前未接入消息路径(消息实际用 JSON 序列化);编码器/解码器实现存在,只被测试引用。本节按格式规格阅读。

LuaPack 是 Shield 内置的二进制序列化格式,用于消息编码。

Wire Format ​

┌─────────────────────────────────────────────────────────┐
│  Header (4 bytes)                                       │
│  - magic: 2 bytes (0x4C, 0x50 = "LP")                  │
│  - version: 1 byte                                      │
│  - flags: 1 byte                                        │
├─────────────────────────────────────────────────────────┤
│  Payload                                                │
│  - type_tag: 1 byte                                     │
│  - data: variable length                                │
└─────────────────────────────────────────────────────────┘

字节序:Little-Endian(与 x86/ARM 一致)。

类型编码 ​

类型Tag编码
nil0x00无数据
false0x01无数据
true0x02无数据
integer0x038 bytes (int64 LE)
number0x048 bytes (double LE)
short_string0x051 byte len + data (len < 256)
string0x064 bytes len (uint32 LE) + data
array0x074 bytes count + elements
map0x084 bytes count + key-value pairs
service_handle0x104 bytes node + 8 bytes id
extension0xFF2 bytes type_id + 4 bytes len + data

字符串编码 ​

txt
短字符串 (< 256 bytes):
  [0x05] [len:u8] [data:len bytes]

长字符串 (>= 256 bytes):
  [0x06] [len:u32 LE] [data:len bytes]

数组编码 ​

txt
[0x07] [count:u32 LE] [element1] [element2] ...

空数组:[0x07] [0x00 0x00 0x00 0x00]

Map 编码 ​

txt
[0x08] [count:u32 LE] [key1] [value1] [key2] [value2] ...

key 必须是 string 或 integer 类型。

ServiceHandle 编码 ​

txt
[0x10] [node:u32 LE] [id:u64 LE]

用于跨服务传递 handle 引用。

扩展类型 ​

txt
[0xFF] [type_id:u16 LE] [len:u32 LE] [data:len bytes]

扩展类型用于自定义编码,如:

  • 0x0001: Timestamp (int64)
  • 0x0002: UUID (16 bytes)
  • 0x0003: Decimal (string representation)

嵌套深度限制 ​

默认最大嵌套深度 64 层,超过返回 encode_failed 错误(该行为属于 LuaPack 编码器规格;当前消息路径不经过它,JSON 路径无嵌套深度检查)。

配置:

yaml
# 未接线(预留):codec 配置键当前无解析
actors:
  - name: gateway
    script: scripts/auth.lua
    codec:
      max_nesting_depth: 64       # 最大嵌套深度
      max_string_length: 1048576  # 最大字符串长度 (1MB)
      max_array_length: 1000000   # 最大数组长度
      max_map_entries: 100000     # 最大 map 条目数

shield.send ​

shield.send 是 at-most-once、非阻塞、无 ACK 的投递 API。

lua
local ok, err = shield.send(target, "kick", uid)

返回成功只表示 runtime 接受消息进入投递流程,不表示 receiver 已收到或处理成功。

Phase 1 规则 ​

Phase 1 的 send 只冻结最小投递语义,不暴露 QoS、可靠投递或可配置背压策略:

  • send 不挂起 coroutine。
  • 不自动重试。
  • self-send 允许,但不是 reentrant call,而是入队到未来调度点。
  • target 可以是 handle 或 name。
  • target 是 name 时,每次发送动态 query registry。
  • target 是 handle 时,直接按 handle 路由。
  • CAF actor mailbox 当前无界,因此 send 在本地投递时不会返回 mailbox_full;背压与 QoS 策略为后续设计项。

可靠处理必须用 shield.call 或业务 ACK。

Deferred 高级选项 ​

以下选项是后续消息/QoS 设计草案,不属于 Phase 1 Lua API,也不进入当前验收矩阵。引入前必须先更新 Lua API 契约 和 Lua API 测试用例。

lua
local ok, err = shield.send(target, "event", data, {
  -- QoS 优先级
  priority = "high",  -- "low" | "normal" | "high" | "urgent"

  -- 可靠投递(仅对 send 有效,需要业务 ACK)
  reliable = false,   -- true 时 runtime 会跟踪投递状态
})

优先级:

优先级值说明
urgent0最高优先级,立即处理
high1高优先级
normal2普通优先级(默认)
low3低优先级

优先级为后续设计项,当前 CAF actor mailbox 不保证消息排序。

当前实现状态:Phase 1 已支持单节点本地 service name 字符串路由,消息通过 CAF actor 异步投递到目标 service。shield.call 是 coroutine-aware 的: Lua 侧经 _coro_call + coroutine.yield 挂起 caller 协程,默认超时 5000ms,由超时 sweep 扫描过期 pending call 并恢复 caller(条件变量只用于 外部非协程上下文的同步等待路径)。非 reentrant self-send 已实现。

shield.call 返回格式 ​

shield.call 返回 ok, ...。

lua
local ok, value = shield.call(target, "get_profile", uid)

if not ok then
  local err = value
end

成功:

txt
ok == true
后续返回值是 callee 业务返回值

失败:

txt
ok == 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
-- reason == "banned"

response payload 需要保存 argc,以保留 trailing nil。

当前实现状态:本地 shield.call 已返回 true, ...callee_returns 或 false, Error,支持业务返回 false 与 runtime 错误区分。method dispatch 已协程化:caller 协程在 call 处挂起,runtime 维护 pending call registry, 超时 sweep 恢复 caller 并返回 timeout 错误;返回值经 table.pack/table.unpack 保留 trailing nil。late response(已超时或未知 session 的迟到响应)被直接丢弃。

call 超时 ​

shield.call 使用默认超时,不允许默认无限等待。

默认值:

txt
call_timeout = 5s

覆盖超时使用单独 API,避免最后一个业务参数和 options table 歧义:

lua
local ok, value = shield.call("db.player", "get", uid)
local ok, value = shield.call_timeout(30000, "db.player", "get", uid)

规则:

  • caller coroutine 挂起。
  • 超时后 caller 恢复 false, Error{ code = "timeout" }。
  • pending call 从 registry 移除。
  • callee 不会被自动取消。
  • late response 被丢弃(对未知 session 的迟到响应直接丢弃,当前不计入任何 ops 指标)。
  • timeout 必须传递到 envelope deadline。

错误对象(make_error 只产出 code/message/retryable/detail 字段):

lua
{
  code = "timeout",
  message = "call timeout",
  retryable = true,
}

nested call ​

service handler 内允许再次调用 shield.call。

lua
function M.login(uid)
  local ok, profile = shield.call("db.player", "get", uid)
  if not ok then
    return false, profile
  end

  return true, profile
end

规则:

  • nested call 只挂起当前 coroutine。
  • 同一个 service 可以继续处理其他 ready message。
  • runtime 不做死锁检测。
  • 循环调用依赖 call timeout 释放。
  • 需要避免在持有业务锁或临界状态时发起 call。

self-call 允许,但必须按普通消息入队和调度,不能直接递归调用 handler。

同 service coroutine 调度 ​

每个 service 拥有一个 Lua VM,VM 内允许多个 Lua coroutine。

规则:

  • 同一时间最多一个 OS thread 进入同一个 Lua VM。
  • 每条 incoming message 创建或复用一个 coroutine 执行。
  • handler 执行到 call / sleep / await timer 时 yield。
  • 当前 coroutine yield 后,该 service 可以处理下一条 ready message。
  • response/timer 到达后,把对应 coroutine 放回 ready queue。
  • 不提供抢占式调度,只在显式 yield 点切换。

这与 Skynet 类似:业务状态不会多线程并行访问,但会在 yield 点发生重入和交错。

服务级资源限制(mailbox、coroutine、pending call 等)见 服务语义。

错误处理 ​

Shield 区分 Runtime 错误和业务错误。完整错误码列表见 错误码参考。

错误传播 ​

lua
function M.login(uid)
    local ok, result = shield.call("player", "get_profile", uid)
    if not ok then
        -- Runtime 错误,返回给上层
        return nil, result
    end

    if not result.ok then
        -- 业务错误,按业务契约处理
        return nil, result
    end

    return result.profile
end

Apache License 2.0