Files
MoonBridge/docs/channel-messaging-contract.md
T
Claude 4def198cc0 docs: 修正 CI 引用、拆出历史验证记录并统一中文文档
- CLAUDE.md、testing.md、publishing.md 的 .gitea 路径改为实际的 .github/workflows;
  说明 verify.yml 在 PR 上运行 check
- testing.md 中带日期的逐轮结果迁到 docs/records/
- README 精简为简介、快速开始和入口;配置与迁移细节只保留在 operations.md
- channel-messaging-v2.md 更名为 channel-messaging-contract.md,与 transfer-coordination.md 一起译为中文
- benchmarks/results 中两个记录改为日期前缀命名
- 新增 compileBenchmarks 并挂到 check,防止 benchmarks/ 静默失效
- .gitignore 删除不存在的 deployment/ 条目

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GGL9BudzWm4dUQgBsQw4Qr
2026-10-20 21:06:01 +00:00

5.7 KiB
Raw Blame History

Channel 消息设计契约

问题与验收标准

后端 A 上的插件必须能通过具名 channel 通知或查询代理插件或后端 B。插件不得自行建立网络连接、分发凭据、实现代理中继插件,也不得依赖在线玩家。同一后端内的多个插件共享一条已认证连接。两端收到的都是不可变消息,带框架分配的标识和经过认证的来源。

验收要求在没有玩家的情况下进行真实 socket 测试:后端/代理与后端/后端请求;通知扇出;并发响应与各自请求正确匹配;插件独立停用;有界过载;包含排队时间的超时;断线不重放;旧连接隔离。由一个 Java 8 Bukkit 宿主构件提供共享服务。单元测试和 socket 测试不能代替在运行中的 Forge 整合包里的部署验收。

模型推导

  1. 连接认证的是后端进程,而不是进程内的每个插件。因此代理根据已认证连接推导来源。插件标签可用于本地生命周期归属,但不是单独认证的远端主体。
  2. channel 标识业务通信域及其权限边界;destination 标识接收节点。若将两者合并,应用将被迫把后端名编码进 channel,或自建路由协议。
  3. 通知没有业务响应,可以有多个订阅者;请求在目的节点上需要恰好一个负责的处理器。分开的 subscribe 与 onRequest 注册消除了多重回复的歧义;同一节点/channel 注册第二个请求处理器会失败。
  4. 网络故障可能发生在执行之后、回复之前。超时和 socket 写入成功都无法证明业务操作是否已执行。消息仅在线投递,队列有界,不做跨连接自动重放。持久状态和幂等性由应用负责。
  5. 消息标识在跨代理转发和日志中都有用,它独立于传输操作临时的回执关联。每条消息(包括通知和回复)都有一个 UUID;回复指向请求的 UUID。路由保留标识;普通调用无需应用生成关联字段。
  6. 后端游戏线程不得执行 socket I/O,也不得等待远端回复。宿主提供回调的执行器。超时从提交时开始计时,覆盖排队、分派和等待;已过期的排队消息不得在之后发送。

公开模型

Endpoint 是代理或某个具名后端。Message 包含 id、kind(EVENT、REQUEST、REPLY)、channel、已认证的 source、target、replyTo,以及防御性复制的二进制 payload。事件 target 为 null 表示向订阅者发布;请求和回复总是只有一个 target。只有回复带 replyTo。业务消息类型和 schema 版本可编码在 payload 中,它们不是传输层 kind。连接代次保持内部。

每个插件获得一个归其所有的 Messaging 作用域,并按名称取得 MessageChannel。subscribe 接收事件,onRequest 异步生成回复,send 向单个端点发送事件,request 取得一条完整回复消息,publish 扇出事件。关闭插件作用域只撤销该插件的注册和未决调用。关闭后保留的句柄不能再创建新工作。

send 返回包含消息 ID 和结果的回执。ACCEPTED 表示目的节点已将通知接入其有界处理器分派,不表示业务代码已完成。NO_SUBSCRIBER、NOT_CONNECTED、BACKPRESSURED、REJECTED、TIMED_OUT 和 FAILED 相互区分。超时或未知失败不能证明接收方未执行该操作。请求在无处理器、过载、超时、断线、拒绝或处理器失败时以类型化错误失败。

publish 访问代理以及当前已连接且有权接收该 channel 的每个后端(包括发布者自身)。每个节点分派给其当前的本地订阅者。结果对每个候选节点各含一项(包括 NO_SUBSCRIBER);它不是跨节点的原子事务。首个实现刻意不引入复制的订阅目录及其注册/重连竞态。一次发布最多接受 256 个候选节点(含代理);超过上限会在分派前拒绝整个发布。直接发送和请求没有这一扇出限制。将来的优化可以引入带确认的订阅路由,而不改变公开语义。

所有权与传输

代理通过 PluginContext 提供消息服务。单个 Java 8 Bukkit 桥接插件持有后端客户端,并通过 Bukkit 的 ServicesManager 发布服务。使用方插件声明对桥接插件的依赖,并以 provided/compile-only 依赖使用其 API;不得 shade 另外一份服务 API 或客户端。桥接插件提供游戏线程回调执行,并在 PluginDisableEvent 时自动关闭作用域。

现有的认证注册、公告游戏地址校验、心跳租约和后端目录所有权仍是传输基础。后端到后端的流量经过代理内置路由器。路由既不改变游戏连接,也不需要代理业务插件。

协议版本 2 增加共享的有界消息编解码器。逐跳操作 ID 关联投递回执和传输响应;端到端消息 UUID 和回复引用在转发中保留。代理根据已认证连接覆盖或校验来源,并将未决调用绑定到确切连接。接收权限与发送权限分别检查。版本 1 的对端必须一并升级;不兼容的版本会明确失败,而不是互相解码对方的帧。

验证边界

实现在合入当前 main 时必须保留富文本/命令补全测试。必需的消息测试覆盖:两个独立后端客户端与代理、事件订阅者与唯一请求处理器、正确的来源和回复标识、调用方修改 payload、畸形/有界协议帧、停滞的处理器、慢速 socket 读取方、插件停用,以及断线/重连。打包的桥接插件必须只包含一份 Java 8 API/客户端实现,并排除 Bukkit 类。

实际 Bukkit/Uranium 加载、真实服务器调度下的主线程行为和生产容量属于单独的验收证据,不得从编译或隔离的 socket 测试推断。