内嵌 Go 内核初步集成到客户端

December 15, 2025
5 min read
By devshan

Table of Contents

This is a list of all the sections in this post. Click on any of them to jump to that section.

一、背景与目标

  • 目标:把 Go 核心服务通过 gomobile 以 AAR 形式内嵌进 Android Flutter 客户端,让普通我在手机上开箱即用,而不是先手动起一个独立的 HTTP 核心。
  • 模式约定:
    • core_mode = external:客户端通过 HTTP 访问外部核心。
    • core_mode = embedded:客户端在本机进程里拉起 Go 核心,通过 127.0.0.1 回环访问。
  • 本次只做“初步能跑”的版本,重点是:
    • 跑通 gomobile 构建。
    • 在 Android 默认使用内嵌核心。
    • 有足够调试信息可以排查问题(尤其是 S3 连接)。

二、核心集成路径概览

1. Go 侧:mobile 包包装 HTTP 核心

  • core/mobile/mobile.go 里提供用于 gomobile 的导出函数:
    • StartWithParams(...) 用于从 Java 侧传入 S3 参数和端口,然后调用内部的 Start(*CoreConfig)
    • Start 内部基于 config.DefaultConfig() 生成配置,根据传入配置覆盖 S3 endpoint、AK/SK、bucket、UseSSL 以及 server 端口,最后通过 api.NewServer 创建 HTTP 服务器并监听 127.0.0.1:<port>
    • Stop 通过 http.Server.Shutdown 优雅关闭,避免进程残留。
  • 这样 gomobile 导出的 API 就非常简单:startWithParams + stop,内部仍然是原来那套 HTTP 核心。

2. gomobile bind:生成 Android AAR

  • core 模块为工作目录(core/go.mod 里 module 名是 e2eepan),使用 Android Studio 自带 JBR 和本机 SDK/NDK:

    • 设置环境变量:
      • ANDROID_HOME=D:\Android\Sdk
      • ANDROID_NDK_HOME=D:\Android\Sdk\ndk\28.2.13676358
      • PATH 前缀追加 D:\Android\Android Studio\jbr\bin
    • core 下执行:
      • gomobile bind -target=android -androidapi=21 -o ..\client\android\app\libs\e2eepan-mobile.aar e2eepan/mobile
  • 早期踩坑:

    • 一开始在仓库根目录执行,gomobile 找不到 go.mod,提示“cannot find main module”,后来改成在 core 目录执行,按模块名 e2eepan/mobile 调用后正常。
    • PowerShell 中不能用 && 链接命令,这个也踩过一次,后续都用单条命令 + 显式 cwd 解决。
  • Android 侧通过 client/android/app/build.gradle.kts 中的:

    • implementation(files("libs/e2eepan-mobile.aar")) 将 AAR 引入工程。

3. Android 侧:MethodChannel 驱动 Go 内核

  • client/android/app/src/main/kotlin/com/e2eepan/e2eepan_client/MainActivity.kt 中:
    • 创建 MethodChannel("e2eepan/core"),接收 Flutter 端调用。
    • 支持两个方法:
      • startEmbeddedCore:读取 Flutter 传下来的 S3 endpoint、AK/SK、bucket、UseSSL 和端口,启一个后台线程调用 Mobile.startWithParams(...)
      • stopEmbeddedCore:同样在后台线程中调用 Mobile.stop()
    • 将 Go 启动/关闭都放到子线程,避免在主线程阻塞导致 ANR。
  • 这样 Android 部分只负责“转译参数 + 在线程里调用 gomobile”,逻辑比较干净。

4. Flutter 侧:AppState 驱动内嵌核心

  • client/lib/core/services/native_core_service.dart 中:
    • 定义 MethodChannel('e2eepan/core')
    • 暴露静态方法:
      • startEmbeddedCore(Map<String, dynamic> config)
      • stopEmbeddedCore()
  • client/lib/core/state/app_state.dart 中:
    • 增加 _coreMode 字段,默认:
      • Android 默认 'embedded'
      • 其他平台默认 'external'
      • 同步到 SharedPreferences('core_mode'),可调。
    • 启动流程 _initCoreAndNetwork()
      • await _ensureCoreModeLoaded();
      • await _maybeStartEmbeddedCore();
      • await _checkInitialNetworkStatus();
    • _maybeStartEmbeddedCorecore_mode == 'embedded' 且平台是 Android 时:
      • 组装 S3 配置(来自设置页 / 默认值)和 api.baseUrl
      • 调用 NativeCoreService.startEmbeddedCore(config)
      • 提供 suppressError 参数,在正常启动路径中吞掉异常,在调试场景中向外抛出。
    • restartEmbeddedCoreIfNeeded 用于 S3 配置变更后优雅重启:
      • 若模式是 embedded:先 stopEmbeddedCore,再 _maybeStartEmbeddedCore()

三、健康检查与 S3 故障暴露

1. 后端 /health 增强

  • 原有逻辑:/health 只检查 S3 是否可 Ping,失败时返回 status=error 和错误字符串。
  • 本次调整:
    • core/internal/api/server.gohealthCheck 中,将 S3 失败时的响应扩展为:
      • status: "error"
      • component: "s3"
      • code: "S3_UNAVAILABLE"
      • error: 具体错误
    • 正常时仍返回 status: "ok" 和时间戳。
  • 这样客户端在只调用 /health 的前提下也能区分:
    • 核心没起 / 网络不通。
    • 核心在,但 S3 掉线或配置错误。

2. 客户端健康检查细化

  • client/lib/core/api/api_client.dart 中:
    • 保留原来的 healthCheck(),仍然只返回 bool,给老逻辑用。
    • 新增 getHealthDetail()
      • 成功时直接返回 /health 的 JSON。
      • 失败但有 JSON body(例如 503 时)也尝试解析并返回。
      • 无法连接时返回 null
  • AppState 中:
    • 增加 _healthErrorCode 字段,对外提供 healthErrorCode getter。
    • 构造函数里的 15 秒心跳逻辑改为:
      • 调用 api.getHealthDetail()
      • 若返回 status == "ok":认为在线,_isOffline=false_healthErrorCode=null
      • 若返回 status == "error":认为离线,_isOffline=true,记录 code(如 S3_UNAVAILABLE)。
      • 若返回 null:认为离线但不知道具体原因。
    • _checkInitialNetworkStatus() 也切换到 getHealthDetail(),保证一启动就能拿到更细的信息。

3. 文件页上的错误提示区分

  • client/lib/ui/home_page.dart 中的空态渲染 _buildEmptyPlaceholder(AppState state)
    • 增加本地判断:
      • s3Failed = state.isOffline && state.healthErrorCode == 'S3_UNAVAILABLE'
    • 根据 s3Failed 决定文案:
      • 如果离线且 s3Failed
        • 标题显示 S3 连接失败
        • 副标题显示 请检查存储服务配置后重试
      • 如果离线但不是 S3 错误:
        • 保持原来的 内核未启动 / 请启动内核后刷新
      • 在线保持原有“这里空空如也 / 点击右下角按钮上传文件”的文案。
  • 效果:
    • 内核没起来 / 端口不通:我这边看到的是“内核未启动”。
    • 内核在但 S3 配置错误或服务挂掉:我这边看到的是“S3 连接失败”,定位更精准。

四、调试入口与配置集中

1. 新增 DebugPage

  • client/lib/ui/debug_page.dart 新增一个独立页面,专门放调试功能:
    • “重启内核”:
      • 调用 AppState.debugRestartEmbeddedCore() 停止并强制启动内嵌核心。
      • 再通过 AppState.debugCheckCoreStatus() 组合输出当前核心模式、baseUrl、S3 配置、health 和 bootstrap 状态。
      • 结果通过 SnackBar 弹出,便于快速查看。
    • “设置内核地址”:
      • 复用原来隐藏在“关于”里的地址弹窗,读取/写入 core_base_url
      • 可以恢复默认地址,也可以指向局域网其它核心。
  • 这两个调试项都集中在单独文件里,随时可以整体移除,不和业务逻辑强耦合。

2. 设置页只保留正式入口

  • client/lib/ui/settings_page.dart 做了瘦身:
    • 调试相关逻辑移除:
      • 删除了隐藏 5 连点“关于”触发核心地址弹窗的彩蛋。
      • 删除了设置页里“重启内核”的直接入口。
    • 新增一个轻量入口:
      • 在“调试”分区只保留一个 调试选项ListTile,点击后 Navigator.pushDebugPage
    • S3 配置对话框:
      • 仍然在设置页中提供。
      • 修改完配置后调用 appState.restartEmbeddedCoreIfNeeded() 尝试重启内嵌核心,并给出提示。

五、构建 / 运行流程总结

当前一套“从源码到手机上运行内嵌核心”的标准流程:

  1. 在 core 下重建 gomobile AAR

    • 工作目录:core
    • 设置 ANDROID_HOME / ANDROID_NDK_HOME / PATH 指向 Android Studio JBR 和 SDK/NDK。
    • 执行:
      • gomobile bind -target=android -androidapi=21 -o ..\client\android\app\libs\e2eepan-mobile.aar e2eepan/mobile
  2. 运行 Flutter 客户端

    • 工作目录:client
    • 执行:
      • flutter run
    • 选择实际设备(本次是 2311DRK48C),安装并以 debug 模式运行。
  3. 在手机上验证

    • 打开 App,默认在 Android 上会选择 core_mode = embedded
    • 在设置 → 调试选项:
      • 验证“重启内核”是否正常工作。
      • 查看调试输出里的 core_mode / api.baseUrl / S3 配置和 health / bootstrap 状态。
    • 在文件页:
      • 如果 S3 配置错误或 MinIO 没起,空态应该显示 “S3 连接失败”。
      • 如果内核没起 / 端口不通,则显示 “内核未启动”。

六、本次集成中的几个关键坑

  1. gomobile 工作目录与 module 名

    • 一开始在仓库根目录执行 gomobile bind,因为根目录没有 go.mod,导致 go: cannot find main module
    • 正确方式是在 core 目录执行,并使用 module 名 e2eepan/mobile
  2. PowerShell 的命令组合

    • 直接写 cd ... && gomobile ... 会因为 && 不被当前 PowerShell 版本支持而报错。
    • 后面统一使用单条命令并通过工具设置 cwd,避免在 shell 里叠加多条。
  3. S3 地址 vs 实际网络拓扑

    • 最初健康检查 health: FAILED,结果发现根因是:
      • 本地 s3key.txt 中配置的 MinIO 地址属于宿主机子网。
      • 虚拟机网络模式变动后,MinIO 实际跑在 192.168.1.4,客户端仍连老的 192.168.74.101
    • 调整方式:
      • 更新 s3key.txt 和默认配置中的 endpoint。
      • 再次构建并部署,健康检查恢复正常。
  4. ANR 与线程模型

    • 如果在 MainActivity 主线程直接调用 Mobile.startWithParams,会在核心启动或 S3 检查耗时较长时导致 ANR。
    • 通过 Thread { Mobile.startWithParams(...) }.start() 将调用放到后台线程,避免阻塞 UI 线程。

七、后续可以做的事情

  1. 统一健康检查接口的使用

    • 目前有 ApiClient.healthCheck()ApiClient.getHealthDetail() 两条路径,后续可以考虑逐步迁移到后者,以便在更多场景下区分具体错误。
  2. NativeCoreService 真正落地

    • 当前内嵌内核仍然通过 HTTP 回环访问,将来可以尝试在 Flutter 侧引入真正的 FFI 调用路径,减少 HTTP 层开销。
    • 但无论是 HTTP 还是 FFI,都可以复用这次梳理出的:
      • healthCheck / bootstrap 语义。
      • AppState 中对在线/离线和初始化状态的统一处理。
  3. 更多调试工具

    • DebugPage 里继续增加一些调试项,例如:
      • 一键打印 /health/bootstrap 原始 JSON。
      • 显示当前使用的 core_modecore_base_url
    • 保持调试页面与业务弱耦合,方便在生产构建中关闭。

本次内嵌内核的初步集成,已经实现了“内核跟着 App 一起跑、S3 问题可视化”这两个核心目标,为后续彻底切换到 NativeCore / FFI 打下了比较清晰的基础。