Files
MoonBridge/docs/backend-channel-design.md
layue13andClaude Opus 5.5 547ba98f8f perf: Spigot/CraftBukkit 1.7.10 上读取 GameProfile 也不再用反射
- SpigotProfileAccess:直接调用 org.bukkit.craftbukkit.v1_7_R4.entity.CraftPlayer#getProfile
  与重定位到 net.minecraft.util.com.mojang.authlib 的 authlib。签名用 javap 从
  spigot-1.7.10-R0.1-SNAPSHOT(build 1649)和 craftbukkit-1.7.10-R0.1 读取,两者一致;
  五个调用的描述符逐一与 Spigot JAR 核对
- 选择顺序:Uranium → Spigot 1.7.10 → 缓存反射回退
- src/serverStubs 加入对应的编译期副本;verifyBackendJar 额外拒绝 net/minecraft/
- 测试:SpigotProfileAccessTest;文档更新

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 20:04:08 +08:00

128 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 后端 Channel 消息服务
业务插件通过命名 channel 与 proxy 或指定 backend 通信。每个后端由一个宿主持有认证控制连接,多个插件共享;proxy 内置路由,后端之间通信不需要另写代理转发插件,也不依赖在线玩家。
模型推导、失败语义及验收条件见 [设计契约](channel-messaging-contract.md)。消息服务与 Minecraft PLAY 转发独立;现有后端注册、心跳租约和游戏地址目录继续由控制服务维护。
```mermaid
flowchart LR
A[Backend A 业务插件] <--> HA[共享消息宿主]
HA <-->|单条认证连接| R[Proxy 内置路由]
P[Proxy 业务插件] <--> R
R <-->|单条认证连接| HB[共享消息宿主]
HB <--> B[Backend B 业务插件]
```
## 参与者、Channel 与消息
- `Endpoint.proxy()`、`Endpoint.backend("island-b")` 表示目标节点。
- `islands:control` 这样的 channel 表示业务域及权限边界;目标 backend 名称不编码进 channel。
- `Message` 包含 UUID `id`、`kind`、`channel`、认证 `source`、`target`、`replyTo` 和复制后的二进制 `payload`。普通调用由框架生成这些元数据;业务消息类型与数据版本由 payload 表达。
- 通知使用 `subscribe`,同一节点同一 channel 可有多个订阅者。请求使用 `onRequest`,同一节点同一 channel 只能有一个处理者;重复注册会失败。
- `request` 返回完整回复消息。回复有自己的 `id`,`replyTo` 等于请求的 `id`;经 proxy 转发保持原始消息身份。来源由认证连接确定,不能相信 payload 中自称的发送者。
后端共享一个认证主体,代理不能独立认证其中每个业务插件。本地插件 owner 用于生命周期清理,不是新的跨进程安全身份。连接代次留在传输实现中。
## 后端插件接入
在 Bukkit/Uranium 后端安装一次 `backend` 模块生成的 `moonbridge-backend` JAR(`gradlew release` 输出的 `backend/` 目录),插件名为 `MoonBridgeBackend`。配置唯一的实例 ID、后端名称、代理控制地址、游戏地址与凭据。宿主异步建立连接,不在启动主线程等待网络。
消费者 `plugin.yml` 声明:
```yaml
depend: [MoonBridgeBackend]
```
消费者对 `uk.potatolab.moonbridge:backend` 使用 `compileOnly`,它就是服务器上安装的那个 JAR,传递依赖平台无关的 `messaging`。业务插件只应使用 `dev.moonbridge.bukkit` 中的 `BukkitMessagingService`、`BukkitSessionService`、`BackendPlayerSession` 和 `dev.moonbridge.messaging` 的公共 API;`dev.moonbridge.backendchannel` 是宿主内部的控制通道客户端,只有在其他平台自行实现宿主时才直接使用。源码默认 `0.1.0-SNAPSHOT` 是本地开发版本;使用已发布构件时,必须将示例中的 `<published-version>` 替换为 CI 成功发布的准确不可变版本。发布版本及构件列表见[发布与依赖版本](publishing.md)。
Gradle 示例(Bukkit API 仍由业务插件按目标服务器版本自行声明):
```kotlin
repositories {
maven("https://git.nest.potatolab.uk:8443/api/packages/layue13/maven")
}
dependencies {
compileOnly("uk.potatolab.moonbridge:backend:<published-version>")
}
```
Maven 使用同一坐标并设置 `<scope>provided</scope>`。本地开发可改用 `0.1.0-SNAPSHOT`,但不要把它当作已发布版本。隔离构建环境也可以直接对发行包 `backend/` 中的 JAR 编译。本例中的 Bukkit API 仍由业务插件按目标服务器版本自行声明为 `compileOnly` / `provided`。不要把这个 JAR 重新打包进业务插件;运行时由服务器 `plugins/` 中的 `MoonBridgeBackend` 提供唯一一份 API 类和服务实现。
```java
import dev.moonbridge.bukkit.BukkitMessagingService;
import dev.moonbridge.messaging.Endpoint;
import dev.moonbridge.messaging.MessageChannel;
import dev.moonbridge.messaging.Messaging;
// 在业务插件的 onEnable 中取得服务;plugin.yml 声明 depend。
BukkitMessagingService service = getServer().getServicesManager()
.load(BukkitMessagingService.class);
if (service == null) throw new IllegalStateException("MoonBridgeBackend service is unavailable");
Messaging messaging = service.forPlugin(this);
MessageChannel channel = messaging.channel("islands:control");
channel.subscribe(message -> handleNotice(message));
channel.onRequest(message -> CompletableFuture.completedFuture(query(message.payload())));
channel.send(Endpoint.proxy(), noticeBytes);
channel.send(Endpoint.backend("island-b"), noticeBytes);
channel.request(Endpoint.backend("island-b"), queryBytes, Duration.ofSeconds(3))
.thenAccept(reply -> logReply(reply.id(), reply.replyTo(), reply.payload()));
```
宿主在游戏主线程调用通知/请求处理器;业务应快速返回,耗时操作返回异步 stage。不要在游戏主线程对网络 future 调用 `get` / `join`。Future 的后续回调不保证在游戏主线程,操作 Bukkit 游戏对象前应显式安排主线程任务。
`forPlugin` 对同一个启用中的插件返回同一 scope。消费者停用时,宿主自动关闭它的 scope,撤销注册并结束未决调用;其他插件继续工作。宿主停用则关闭唯一的连接。需要提前停止服务时,也可主动 `messaging.close()`。
非 Bukkit 宿主可以创建一次 Java 8 `BackendChannelClient`,通过 `client.messaging(owner, executor)` 给各插件提供 scope,并负责调用 scope/client 的 `close`。普通业务插件不持有凭据或直接构造客户端。
## Proxy 插件接入
```java
MessageChannel channel = context.messaging().channel("islands:control");
channel.onRequest(message -> prepare(message.payload()));
channel.subscribe(message -> audit(message.id(), message.source()));
channel.send(Endpoint.backend("island-a"), bytes);
channel.request(Endpoint.backend("island-b"), bytes);
```
Proxy 为每个插件管理独立的 scope,在停用时自动撤销。项目处于开发阶段,旧 `backendChannels()`、裸字节处理器及直接客户端消息方法已经移除;两端插件统一使用 `messaging()` API。
## 交付、发布与故障
`send` 是定点通知,其 `SendReceipt` 含消息 ID。`ACCEPTED` 只表示目标节点的消息服务接受了有界处理任务,不表示业务执行完成;没有订阅者、目标断连、过载和权限拒绝分别为 `NO_SUBSCRIBER`、`NOT_CONNECTED`、`BACKPRESSURED`、`REJECTED`。`TIMED_OUT` 表示未及时收到回执,`FAILED` 表示其他故障,这两者均不能证明业务尚未执行。
`request` 是一对一请求。超时从提交时开始,覆盖排队、传输和等待回复;默认 5 秒,最长 60 秒。无处理器、超时、断线、权限拒绝和处理失败有可区分的错误。过期的排队请求不能恢复后再发出。
`publish(payload)` 面向 proxy 与当前有接收权限的在线 backend,包括发布者自身;节点按当时的本地订阅者分发。返回结果按候选节点列出投递状态,也会包含 `NO_SUBSCRIBER`。多节点投递不是原子事务,部分节点可能接受、部分失败。首版按在线连接扇出,避免维护远端订阅目录及其重连同步状态。一次发布最多 256 个候选节点(包含 proxy);超过时在任何投递前明确拒绝,不截断结果。定点 `send` / `request` 不受此发布上限影响。
消息不离线保存,断线不自动重放。超时或断线不能证明远端没有执行;需要重试的业务应有自己的稳定幂等键。UUID 提供身份和追踪能力,不承诺 exactly-once。多个并发请求可以乱序回复,框架根据关联信息匹配。
## 配置与部署
代理在 `backendChannel.listen` 启动独立监听器。配置示例见 [moonbridge-channel.example.yml](../proxy/core/src/main/resources/config/moonbridge-channel.example.yml)。每个实例的 `secret` 至少 32 个 UTF-8 字节,`allowedHosts` 限制其广告游戏地址;`allowedNamespaces` 控制发送,`allowedReceiveNamespaces` 控制接收,未配置接收列表时继承发送列表,显式空列表表示禁止该方向的所有消息。跨后端路由同时检查源发送权限与目的接收权限。Channel registration v3 将代理启动 epoch 和 backend registration epoch 同时交给后端。代理用该 backend client secret 为发往该 backendName 的 legacy forwarding handshake 生成短时 HMAC 会话证明;证明绑定两端当前 epoch、backendName、UUID、connectionId、一次性 nonce 和到期时间。后端 `MoonBridgeBackend` 校验它并把会话绑定到实际 Bukkit Player 登录对象。两端身份或 secret 不匹配、registration 未完成或 epoch 已变化时,`BukkitSessionService.find(player)` 返回空值。证明不代表任何 AetherShard 操作。
在 `OFFLINE` 代理模式中,只有显式配置了该 backend 的 session-binding secret 时,代理才会在初始连接或转服时转发由代理派生的 offline UUID 和会话证明;这证明代理连接身份,不表示 Mojang 已认证。没有配置 secret 的 offline backend 仍接收原始握手。静态 backend 或尚未获得正数 registration epoch 的目标不会收到会话证明。
后端宿主在玩家加入时从 GameProfile 属性中取出并移除会话证明。Bukkit API 不提供 GameProfile,所以按服务器选择访问方式,启动日志会写明 `GameProfile access: …`:Uranium(`org.bukkit.craftbukkit.entity.CraftPlayer`、未重定位的 authlib)和 Spigot/CraftBukkit 1.7.10(`v1_7_R4`、authlib 重定位到 `net.minecraft.util`)都使用编译进 JAR 的直接调用,没有反射;其他服务器使用按类缓存的反射回退。编译调用与服务器实际签名不符时,宿主记一条警告后改用反射回退。两条路径都找不到 GameProfile 时,该玩家没有会话绑定,不会猜测。
实例的游戏地址必须是代理实际可达的 `tcp://host:port`。Docker 中使用容器服务名,容器间不要使用 `127.0.0.1`。仅将玩家端口公开,控制端口放在私有网络。不要把真实凭据写入镜像或提交到仓库。
HMAC 挑战认证本身不加密链路;跨机器使用私有加密链路或可信 TLS 终止器。每个实例配置独立的实例 ID、后端名称和密钥;重复认证同一实例会接管旧连接,旧连接的迟到回复不能影响新连接。
协议已升级为版本 2,proxy 与后端宿主必须一起升级;旧协议和 Java API 已移除,版本 1 连接会明确拒绝。关闭时在有限等待内尝试发送注销帧,代理收到后立即注销目录;若链路阻塞或意外断线,则保留到租约过期,不强制关闭已经存在的玩家游戏连接。
## 验证范围
自动化验证针对 Java 8 API/宿主字节码、宿主 JAR 打包边界、真实 TCP 的零玩家双向及跨后端请求、发布、多插件关闭、并发回复关联、超时与异常帧。构建和独立 TCP 测试不能代替真实 Bukkit/Uranium 装载、玩家整合包验收或生产容量测试。
本次构建、244 项回归和实际 Java 8 进程的结果见 [2026-09-27 验证记录](../testing/smoke/results/2026-09-27-channel-messaging.md)。
安装发行包的 `backend/` 提供共享宿主 JAR;它同时包含非 Bukkit 宿主所需的控制通道客户端、消息 API 和协议。运行 `:proxy-core:installDist` 后,可用实际 JDK 8 运行独立进程验收:
```powershell
.\smoke\channel-java8.ps1 -Java8Home 'C:\path\to\jdk8' -Java25Home 'C:\path\to\jdk25'
```
脚本用 JDK 25 启动本机 router,再用 JDK 8 编译并运行两个后端客户端,校验请求关联、来源与三个插件订阅者;不启动 Minecraft 玩家或 Bukkit 服务端。