• 请不要在回答技术问题时复制粘贴 AI 生成的内容
fregie
V2EX  ›  程序员

我把 Go 服务器塞进了 Flutter App:一个无服务端照片同步应用的架构分享

  •  1
     
  •   fregie ·
    fregie · 16h 40m ago · 1227 views

    利益相关声明:这篇文章介绍的是我自己独立开发的项目 Pho ,同步引擎开源( GPL-3.0 ,GitHub: github.com/fregie/pho)。下文是架构层面的真实取舍与踩坑,尽量讲技术、少讲产品。

    需求与选型

    这个应用要解决一个很朴素的问题:把手机上的照片同步到用户自己的 NAS ( SMB / WebDAV / NFS / 百度网盘),然后本地和云端照片在同一个时间线里浏览。

    第一版我试图用纯 Flutter/Dart 实现,很快撞了墙:

    • Dart 生态里 SMB / NFS 协议栈几乎没有可用的成熟库,WebDAV 也就一两个半成品;
    • 文件 IO 、并发上传、流式加密这些重活,Dart 写起来既别扭又担心性能;
    • 而 Go 生态里 gowebdav 、go-nfs-client 、smb 客户端都是生产验证过的。

    于是架构变成:UI 层用 Flutter ,所有"重"逻辑用 Go 写,把 Go 编译成原生库嵌进 App,进程内用 gRPC + HTTP 通信。

    ┌─────────────────────────────────────────┐
    │  Flutter UI (Dart)                      │
    │  ─ 状态/相册/设置/同步界面                │
    └──────────────┬──────────────────────────┘
                   │ gRPC (控制/元数据 15 RPC )
                   │ HTTP (文件字节流,Image-* 头)
    ┌──────────────▼──────────────────────────┐
    │  Go (gomobile 编译的原生库)              │
    │  ─ StorageDrive 接口 × 4 后端            │
    │  ─ 缩略图生成 / 增量对比 / 加密           │
    │  ─ worker 队列(异步缩略图)              │
    └──────────────┬──────────────────────────┘
                   │ SMB / WebDAV / NFS / 百度网盘
            ┌──────▼──────┐
            │  用户的存储  │ ← 唯一的"数据库"
            └─────────────┘
    

    启动链路:Flutter 怎么把 Go 拉起来

    App 启动时,Dart 侧通过平台通道把内嵌的 Go server 拉起来:

    // lib/run_server.dart
    final ports = await const MethodChannel('com.example.img_syncer/RunGrpcServer')
        .invokeMethod('RunGrpcServer');
    

    Go 侧(server/run/run.go)做的事:

    1. 初始化 ImgManager
    2. 127.0.0.1:10000-20000 范围内逐个尝试监听,找到两个空闲端口( gRPC 一个、HTTP 一个);
    3. 注册 15 个 gRPC RPC + HTTP handler ,go grpcServer.Serve(...)go http.Serve(...) 双协程跑起来;
    4. "grpcPort,httpPort" 返回给 Dart ,Dart 据此连 gRPC 和 HTTP 。

    移动端禁止绑定固定端口,所以"自动扫端口"这个细节看似随意,其实是避开各种冲突的最省事方案。

    双协议:gRPC 管控制,HTTP 管文件

    一开始想全走 gRPC ,后来发现文件传输还是 HTTP 顺手:

    • gRPC:控制面 + 元数据。列表、配置、FilterNotUploaded双向流,服务端走目录结构对比出"未上传"清单,客户端增量上传);
    • HTTP:文件字节流。上传下载带进度、支持断点续传,自定义头带业务信息:Image-DateImage-Encrypt-TypeImage-Encrypt-PasswordImage-Is-Live-Photo

    路由没引任何框架,一个 http.HandlerFunc 按 method + path 前缀分发——15 个 RPC 的规模真不需要 gin/echo 。

    无数据库:文件系统就是数据库

    这个项目刻意不用任何数据库。设计原则是:远程存储的文件系统本身就是数据库

    • 照片按 YYYY/MM/DD/{timestamp}_{name} 组织,缩略图放 .thumbnail/ 镜像目录,Live Photo 放 live_<name>/ 子目录;
    • 增量对比直接 RangeByDate 走目录;文件名里 encodeName 编码了时间戳,decodeName 反解——目录遍历就能还原时间线;
    • 好处是用户随时可以脱离 App,用任何文件管理器直接访问原始文件,不存在"被锁在某个数据库里"的问题。

    StorageDrive:五个方法抽象所有存储

    存储后端是变化最快的部分,所以抽象得很薄:

    type StorageDrive interface {
        Upload(ctx, path, reader) error
        Download(ctx, path) (io.ReadCloser, error)
        DownloadWithOffset(ctx, path, offset) (io.ReadCloser, error)
        Delete(ctx, path) error
        Range(ctx, path, start, count) ([]byte, error)
    }
    

    SMB / WebDAV / NFS / 百度网盘各一个实现,SetDrive() 热切换——同一套同步、缩略图、加密逻辑跑在完全不同的协议上。这也是当初"重逻辑下沉到 Go"的最大收益:后端逻辑只写一遍,四个协议都能用。

    加密与流式播放

    加密做了两代:

    • 旧方案 AES-128-CFB ,能加密文件但不能随机读;
    • 新方案 AES-256-GCM,文件头带 PHO1 魔数,GetOffset() 能定位到指定加密块——加密的视频也支持 HTTP Range 拖动进度条,不用整文件下载。

    加密与否、用什么方案,通过 HTTP 头(Image-Encrypt-Type)在客户端和服务端之间协商,对 UI 完全透明。

    踩过的坑

    1. gomobile 不是万能的:Android ( AAR )和 iOS/macOS ( XCFramework )用 gomobile bindCGO_ENABLED=0);但 Windows 没有 gomobile 支持,只能用 CGo c-shared + //export 导出符号,还要手动 sed + dlltool 处理生成的 .h(详见 build.md,每次升级 Go 都可能踩一遍)。
    2. 构建顺序的隐形坑flutter run 不会自动重建 AAR 。AAR 过期/缺失时 App 能正常装、界面正常,但内嵌 Go server 起不来——这种"静默失败"排查起来很费时间,只能靠构建脚本强约束顺序:make protobuf → make server-aar → flutter run
    3. protoc 插件版本锁死:Dart 侧 protoc 插件必须用 21.x ,升到 25.x 生成的代码与项目用的 protobuf 3.x 不兼容,编译期报一堆晦涩错误。
    4. iOS 后台同步:App 在前台用 Timer.periodic,iOS 后台用 BGProcessingTask 拉起 headless FlutterEngine ,走同一个 runSyncOnce() 纯逻辑入口——UI 和后台必须共享同一套同步引擎,否则行为会分叉。
    5. 日志:没用 slog/zap ,就标准库 log.Logger 三个实例( Info/Error/Debug ),Debug 默认丢到 io.Discard,调试时开 -d 才输出。够用,别过度设计。

    为什么值得这么做

    最终 App Store 包体只有 48.6MB( Go 后端 + Flutter UI ),单机同步性能足够,一个开发者能维护 4 个存储协议 + 双端 UI 。如果当初用 Dart 硬写协议栈,大概率维护成本翻倍还做不完。

    同步引擎开源在 github.com/fregie/pho( GPL-3.0 ,1.1k+ stars ),iOS 版 App Store 搜 "Pho",安卓开源版可直接从 GitHub Releases 下载 APK 。

    欢迎交流

    12 replies    2026-08-11 11:12:59 +08:00
    404www
        1
    404www  
       16h 32m ago
    排版好整齐,我现在已经没有精力排版怎么整齐了
    hsuehly
        2
    hsuehly  
       16h 23m ago
    为什么不用 rust,使用 go 还要塞个 runtime
    fregie
        3
    fregie  
    OP
       16h 17m ago
    @404www AI 帮大忙
    fregie
        4
    fregie  
    OP
       16h 16m ago
    @hsuehly rust 写网络的生态不如 go ,go 很快就写完了
    hsuehly
        5
    hsuehly  
       16h 15m ago
    @fregie 现在有 ai 了,直接说需求 ai 写不好吗,并且 rust 还能编译成 so 直接调用
    psllll
        6
    psllll  
       16h 14m ago via Android
    安卓版的 pro 什么时候上架
    fregie
        7
    fregie  
    OP
       16h 13m ago
    @hsuehly 其实我开始做这个项目的时候还没有 vibe coding ,都是我手搓的
    fregie
        8
    fregie  
    OP
       16h 13m ago
    @psllll 正在努力上 google play
    batnss
        9
    batnss  
       16h 4m ago
    用过一段时间,后来装了飞牛,就没用了,总之小巧精致,感谢
    lel020
        10
    lel020  
       16h 2m ago
    速度如何, 我在用的 folderSync ,smb 只有 10MB/s ,sftp 快一些也只有 50MB/s ,死活跑补上局域网千兆,
    我经常需要把几个 G 的视频同步到局域网,速度慢是真难受,
    Martens
        11
    Martens  
       15h 54m ago
    go 启动的时候感觉可以不指定端口让操作系统随机分配,127.0.0.1:0 ,或者 unix socket 更方便些(这里不清楚移动端或 flutter 是否支持)?
    Serefrefee
        12
    Serefrefee  
       15h 28m ago
    标记一下
    About   ·   Help   ·   Advertise   ·   Blog   ·   API   ·   FAQ   ·   Solana   ·   1044 Online   Highest 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 56ms · UTC 18:41 · PVG 02:41 · LAX 11:41 · JFK 14:41
    ♥ Do have faith in what you're doing.