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

发布于

**利益相关声明**:这篇文章介绍的是我自己独立开发的项目 Pho ,同步引擎开源(GPL-3.0,GitHub: [github.com/fregie/pho](http://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 拉起来:

```dart
// 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-Date`、`Image-Encrypt-Type`、`Image-Encrypt-Password`、`Image-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:五个方法抽象所有存储
存储后端是变化最快的部分,所以抽象得很薄:
```go
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 bind`(`CGO_ENABLED=0`);但 Windows 没有 gomobile 支持,只能用 CGo `c-shared` + `//export` 导出符号,还要手动 `sed` + `dlltool` 处理生成的 `.h`(详见 [build.md](http://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](https://github.com/fregie/pho)(GPL-3.0,1.1k+ stars),iOS 版 App Store 搜 "Pho",安卓开源版可直接从 GitHub Releases 下载 APK 。

欢迎交流

---

原文链接:[点击查看](https://www.v2ex.com/t/1233441)

评论(6)

好奇为什么不用 Rust?用 Go 的话还得在 App 里塞个 Runtime。

· 0 个赞

回复

主要是 Rust 在网络这块的生态还是不如 Go,用 Go 的话开发进度快得多,能迅速把东西搓出来。

· 0 个赞

蹲一个,安卓的 Pro 版本什么时候能上架应用商店啊?

· 0 个赞

回复

正在折腾上架 Google Play 了,再等等。

· 0 个赞

我之前用过一段时间,后来换了飞牛 OS 就没用了。软件本身还是挺小巧精致的,感谢开源。

· 0 个赞

楼主,这个局域网传输速度怎么样?我之前用 FolderSync,SMB 跑满也就 10MB/s,SFTP 稍微快点能到 50MB/s,死活跑不满千兆。我经常要同步几个 G 的大视频,速度慢了是真的难受。

· 0 个赞

0.041164s