Skip to content

Bridge 协议 ​

Starcloud.Bridge 把 Core 以 stdio 行式 JSON 暴露给非 .NET 客户端(Flutter 壳、Android 端)。它不是 HTTP 服务——没有端口,没有路由,没有鉴权。

为什么是 stdio ​

Bridge 由父进程拉起,两端共享同一个数据根(STARCLOUD_DATA_DIR 注入),生命周期完全跟着父进程走。走管道意味着「谁启动我谁就是我的客户端」,不需要再发明一套连接和握手。

协议长这样,三种帧:

json
{"id":1,"method":"catalog.versions","params":{"includeSnapshots":true}}
{"id":1,"ok":true,"result":{"versions":[...]}}
{"id":1,"ok":false,"error":"版本 26.3 不在官方清单里"}
json
{"event":"install.progress","params":{"versionId":"26.3","stage":"库文件","current":42,"total":120,"percent":35}}

请求带 id;响应回同一个 id,ok 布尔,失败时 error 是人类可读的中文串;事件没有 id,用 event 键,客户端按名字订阅。

JSON 选项是 camelCase + 忽略 null(PropertyNamingPolicy = CamelCase、DefaultIgnoreCondition = WhenWritingNull)。

安全边界靠进程,不靠协议 ​

协议层零鉴权。 没有 token、没有握手、没有任何校验——任何能往 Bridge 的 stdin 写一行 JSON 的进程都能调它的所有方法。

边界是 OS 级的:Bridge 只绑定父进程的 stdin / stdout 管道,管道本身就是边界。别人拿不到这条管道,就用不了 Bridge。

但管道 EOF 只能保证「父进程正常关闭 stdin」时 Bridge 退出。父进程崩溃时不一定。所以还有一道看门狗:

csharp
// STARCLOUD_BRIDGE_PARENT_PID 由父进程注入
while (true)
{
    await Task.Delay(2000);
    try
    {
        using var parent = Process.GetProcessById(parentPid);
        if (parent.HasExited) Environment.Exit(0);
    }
    catch (ArgumentException) { Environment.Exit(0); }  // 父进程已不存在
    catch { /* 权限等临时错误不当作父进程消失 */ }
}

2 秒轮询。父进程没了,Bridge 自己退出,不留孤儿进程在后台跑下载。

Bridge 能做什么

它能读写用户的账户、装整合包、启动游戏、发起联机。把 Bridge 当成「以用户权限运行的完整启动器」来对待,不要暴露到网络上。

三层并发 ​

主循环读一行、派发一行。派发有三种待遇。

普通方法同步执行。 读 params、调 DispatchAsync、回响应。绝大多数方法走这条。

长任务白名单异步化。 IsLongRunning(method) 里列了一批方法(download.file、java.list、launch.start、content.search、versions.importPack、runtime.diagnose、Nova 与联机相关等),它们被丢到 Task.Run 里跑,主循环立刻回来读下一行。

为什么必须这样:否则下载期间 ping 也要排在下载后面,客户端的短超时会「其实已经受理了」误判成失败。

先回执,再走事件流。 install.vanilla、multiplayer.install、nova.chat 三个方法立刻回:

json
{"id":1,"ok":true,"result":{"accepted":true}}

然后进度全部走事件:install.progress / install.done / install.failed,multiplayer.progress / multiplayer.install.done,nova.delta / nova.done / nova.failed。任务结束后再补一条终态响应。

每个这类任务还有一把互斥锁:已有安装任务在跑时,新请求直接回 ok:false, error:"已有安装任务在进行",不排队。

编码和锁 ​

强制 UTF-8。 入口第一件事:

csharp
var utf8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
using var stdin = new StreamReader(Console.OpenStandardInput(), utf8);
var stdout = new StreamWriter(Console.OpenStandardOutput(), utf8) { AutoFlush = true };

Windows 控制台默认代码页(936/1252)会让中文路径和中文版本名变成乱码,而 Dart 侧的严格 UTF-8 解码器遇到非法字节会把整条协议流判死——不是丢一帧,是后面全部失效。

stdout 全部加锁再 Flush。 事件是异步线程发的,响应是主循环发的,两者会交错。WriteLine 里一把静态锁罩住「写 + Flush」,保证一帧一行、不会半行交错:

csharp
private static void WriteLine(TextWriter writer, string line)
{
    lock (StdoutLock)
    {
        writer.WriteLine(line);
        writer.Flush();
    }
}

核心坑:没有背压 ​

Bridge 是同步写 stdout 的。客户端如果停止读 stdout,管道缓冲区很快填满,写线程就阻塞在 Write 里——而它还持着那把 StdoutLock。

结果是整个进程假死:主循环想回任何响应都拿不到锁,连 ping 都发不出去。表现是「Bridge 好像死了」,其实只是写侧堵住了。

这不是靠协议能修掉的问题——行式 JSON 没有流控字段。只能靠约定:客户端必须持续读 stdout;事件量大时(下载进度 10 Hz 聚合)要跟得上;排障时先看客户端有没有真的在读,而不是先怀疑 Bridge。

事件别当消息读

事件没有 id,也不保证顺序。需要顺序的东西(安装完成前后的状态)请用响应 + 终态事件配合,不要假设事件流是有序队列。

相关链接:项目结构与分层 · 内核的生命周期 · 常见坑速查

基于 MIT 许可证(有修改)开源