先交代一下背景。我自己手边长期同时用一台 Windows 台式机、一台 MacBook 和两部手机,日常高频动作就那么几个:把电脑上的文档发到手机上继续看;手机收到验证码,懒得一个个字符抄到电脑;在电脑上复制了一段网址,想直接在手机上继续编辑;偶尔还要把手机刚拍的照片快速传回电脑。听起来都是小事,但累积起来真的让人火大。带数据线太原始,用聊天软件中转又压画质、多步骤,试过一些跨设备协同工具之后,发现要么绑定厂商生态,要么过度依赖云端,要么根本没法定制。于是去年我开始动手写自己的工具,项目名叫 UniMate。Uni 是 Unified,Mate 是搭档,合起来的意思很直接:让手头的设备像搭档一样配合,而不是各干各的。
这篇不是产品说明书,而是 UniMate 整个项目的设计拆解和踩坑记录,包括整体架构、协议设计、核心功能实现、问题排查和实测数据。如果你也是多设备党,或者正在做类似的协同工具,这份记录应该能帮你少走很多弯路。
1. UniMate 的整体设计思路:为什么不做成“又一个传文件工具”
1.1 需求拆解:我到底想要一个什么样的“Mate”
一开始我也差点把方向带偏,以为做个“局域网传文件工具”就够了。但梳理需求后,我发现真正让人烦躁的不是“传文件”这个动作,而是信息在设备之间流动时需要太多手工步骤。会议资料、验证码、复制的链接、临时记下的想法、相册里的截图,这些内容不该被困在某一块屏幕里。
我的核心需求可以列成几个可验证的场景:
- 剪贴板双向同步:电脑复制,手机粘贴;手机复制,电脑直接粘贴。
- 文件互传:不经过任何中转服务器,局域网内直接传,支持大文件。
- 通知转发:手机收到验证码,电脑屏幕直接弹出来。
- 指令执行:手机可以给电脑发简单指令,比如播放下一首、锁屏。
- 条件自动化:结合时间、网络、内容关键词,把设备动作串联起来。
现成工具其实都能覆盖其中一两个场景,但往往过于依赖账号体系,或者把“云同步”做得太重。我不需要数据经过别人的服务器,也不需要一个必须注册的账号。UniMate 的定位很明确:一台设备就是私有网络里的一个节点,节点之间直接对话。
1.2 技术选型:Go + Flutter + Protobuf 的取舍
确定目标后,选型没有纠结太久,我最终用了Go + Flutter + Protobuf + WebSocket/mDNS,具体原因如下:
| 组件 | 选型 | 为什么是它 |
|---|---|---|
| 核心守护进程 | Go | 编译成单文件二进制,Windows/macOS/Linux/Android 都能跑;goroutine 天然适合同时处理多个设备连接和多个监听器 |
| 客户端界面 | Flutter | 覆盖桌面和移动端一套 UI,减少双倍工作量 |
| 数据序列化 | Protobuf | 二进制格式紧凑,schema 演进方便,比 JSON 更适合长期维护 |
| 本地通信 | gRPC | UI 和守护进程分离后,本地调用需要一套规范的接口定义 |
| 设备发现 | mDNS | 局域网自动广播,不需要配置中心 |
| 远距离传输 | WebSocket | 双向通信好实现,配合中继服务器也能跑 |
| 数据存储 | SQLite | 保存设备信息、消息记录、规则和配置,简单可靠 |
这套组合最大的好处是每个端都有一个本地守护进程,界面只是遥控器。守护进程负责监听剪贴板、接收消息、管理加密会话、执行规则,即使界面崩了,数据同步不会断。
1.3 架构分层:守护进程与界面分离
我最后做成的是“界面层 – 本地API – 核心守护 – 对端核心守护 – 对端界面”的分层结构。每台设备的 Flutter 只通过 gRPC 和本机守护通信,守护和守护之间才走加密数据流。这样有几个实际好处:
- 命令行也能调用守护功能,方便写脚本。
- 其他应用可以集成设备间的能力,不必依赖界面。
- 后台运行时可以只跑轻量守护,内存占用比整个界面低得多。
实际开发中,分层最大的价值是能单独调试。我在写 Android 端时,直接对守护发 gRPC 请求,不需要先把界面画完。
2. 核心协议与数据模型设计
2.1 设备发现与配对:mDNS 广播加手动二维码兜底
设备发现我优先选择了 mDNS,也就是让每个守护进程在局域网里广播_unimate._tcp服务,广播内容包含设备名、设备类型和公钥指纹。这样用户打开 UniMate,理论上设备列表会自动出现。mDNS 不需要中心服务器,路由器配置一次后基本不用管。
但实际使用中,广播不一定可靠:Windows 防火墙可能拦,路由器可能隔离多播,公司网络尤其明显。所以 UniMate 还保留了手动配对通道:一方生成包含 IP 和配对码的二维码,另一方扫码后直接建立加密连接。
配对流程不是简单地“两边输同一个码”,我采用的是:
- 发起方生成 6 位随机配对码。
- 等待方展示自己的身份公钥。
- 发起方用配对码派生共享密钥,将加密后的握手消息发给等待方。
- 等待方解密后回执,双方交换并保存对方的长期身份公钥。
这 6 位码只用于首次握手,后续不会重复使用。每次通信都会重新协商会话密钥,所以即使某个时间窗口内的密钥泄漏,也不影响长期安全。
2.2 消息类型与 Protobuf 定义
协议层我用一条Envelope包裹所有消息,消息体用 oneof 区分类型。以下是简化后的定义:
message Envelope { string msg_id = 1; // 全局唯一消息ID uint64 timestamp = 2; // 毫秒时间戳 DeviceInfo sender = 3; // 发送者信息 oneof payload { ClipboardData clipboard = 4; FileBlock file_block = 5; NotificationData notification = 6; CommandData command = 7; Probe probe = 8; // 连接探测 } }每种 payload 再各自定义字段,比如文件块必须带file_id、offset、data、checksum,通知消息必须带app_name、title、body、post_time。用 oneof 的好处是未来新增消息类型时,不需要改动已有字段,只扩展 proto 文件并同步 Codegen 即可。
msg_id 我特意做成全局唯一:由设备ID + 自增序号 + 随机后缀拼接,再用 UUID 短编码简化。后面做消息去重时,这个字段帮了大忙。
2.3 传输安全与去重设计
跨设备传数据,我默认所有内容都是敏感内容。UniMate 的传输层没有直接用裸 WebSocket,而是走了一层端到端加密:
- 每个设备在第一次启动时生成 ed25519 身份密钥,公钥指纹就是设备 ID。
- 设备间建立会话时,用 X25519 进行密钥协商,同时用身份密钥签名完成双向认证,防止中间人。
- 实际消息加密使用 ChaCha20-Poly1305,性能和安全性兼顾。
连接建立后,每条 WebSocket 消息都是密文,就算有抓包工具也只能看到随机字节。这一点对“验证码转发”“剪贴板同步”尤其重要,因为我需要把用户的敏感信息传给另一台设备,但绝不允许在传输过程中被别人读到。
去重设计上,每个接收端维护一个最近 10000 条 msg_id 的 LRU 缓存。收到任何消息,先查询是否已经存在,存在就直接丢弃。这样即便网络重连后补发历史消息,也不会产生重复弹窗或重复写入剪贴板。
3. 核心功能模块的实现细节与实操要点
3.1 剪贴板同步:如何防止死循环和“回环”
剪贴板同步看起来简单:监听剪贴板变化,有变化就把内容发给对端,对端收到后写入剪贴板。但一写就会触发对端自己的监听,然后又把内容弹回来,结果就是两台设备来回互发,直到被去重机制拦下。我最初就是在这一步被打了个措手不及。
最终实现分三层过滤:
- 来源过滤:每条剪贴板消息都携带来源设备ID,收到后如果来源是自己,直接丢弃。
- 内容哈希过滤:每个端只广播“哈希与最后一次不同”的剪贴板内容。
- 类型和大小过滤:文本超过 2MB 不自动同步;图片超过 10MB 会先压缩到最长边 1920px 再同步;如果是文件管理器复制的文件路径列表,直接忽略。
这里的实操经验是:剪贴板监听一定不能同步阻塞。复制大段文字时,操作系统会连续触发多次变化事件,必须用去抖窗口加上限,否则 0.5 秒内可能发 5 次相同的 payload。我的做法是事件到达后先启动 300ms 定时器,定时器结束后再检查哈希并发送。
3.2 文件传输:分块、断点续传与校验
文件传输相对剪贴板简单,但要做得可靠,必须有套完整的确认机制。UniMate 的传输流程是:
- 发起端生成
file_id,发送文件元信息FileMeta。 - 接收端回复确认,并给出可接收的窗口大小。
- 发送端把文件按 1MB 切块,按滑动窗口发送
FileBlock。 - 每个块都带序号和 SHA256 校验值。
- 接收端全部写完后,再对整体文件做一次校验,随后从
.part改名成正式文件。
断点续传的细节在于:接收端需要持久化每个文件已接收块的位图。中断重连后,发送端发SyncRequest,接收端回复位图,发送端只发送缺失的块。我实测过在局域网 Wi-Fi 5 环境下,一个 2GB 的文件传输,速度能跑到 28MB/s 左右。
这里有个容易忽略的点:接收端写临时文件时,最好按 file_id 分目录,不要用文件名直接拼。防止两个设备传输同名文件时互相覆盖。我吃过这个亏,后来统一改成临时目录/file_id/file,完成后再移到正式目录。
3.3 通知转发与验证码提取
通知转发是我个人使用频率最高的功能。实现上有两条线:一是系统通知监听,二是规则引擎筛选。Android 端我用 NotificationListenerService,桌面端通过系统事件订阅通知。为了用户隐私,默认不转发完整通知正文,只转发应用名和标题,正文要显式开启才允许发送。
验证码提取属于规则引擎的特例。我在每个端内置了常用的正则表达式:验证码|校验码|动态密码|verification code。命中后,会把匹配到的数字串单独提取出来,生成一条高优先级通知,直接推到对端屏幕。测试下来,从手机收到短信到电脑弹出提示,平均 3 秒左右。
这条通道必须加密、必须脱敏、必须可控。我一开始直接把整个短信内容转发到电脑,结果发现短信里经常带完整用户名或推广链接,非常不雅。后来改成“默认只转发验证码本身,不转发原文”,体验和隐私都好很多。
3.4 自动化规则引擎:把“同步”变成“协同”
如果只是传文件、同步剪贴板,那 UniMate 最多算个“局域网工具”。真正让它变成个人中枢的,是一套轻量级规则引擎。规则用 JSON 文件维护,放在每个端的数据目录里,改变量重启守护即可生效。
一个典型的规则示例:
[ { "trigger": { "type": "clipboard", "device": "phone", "match": "github.com/" }, "action": { "type": "open_url", "target": "desktop", "url": "${clipboard}" } } ]这条规则的语义是:当手机剪贴板内容包含github.com/时,自动让电脑用默认浏览器打开这段链接。类似地,我还可以做“晚上 10 点后手机勿扰,由电脑弹出提醒”这种结合时间和设备状态的规则。
规则引擎最需要注意的是别轻易造循环。比如一条规则监听剪贴板变化,又把内容写回剪贴板,很容易引发震荡。我的办法是在执行动作时打上action_id,规则触发后把 action_id 写入本地去重缓存,避免同一事件被反复处理。
4. 关键问题排查实录:我在开发中踩过的坑
4.1 mDNS 在 Windows 上“消失”,防火墙是第一嫌疑
第一次做局域网测试时,Mac 和手机都能互相发现,但 Windows 那边死活找不到任何设备。查了路由器、关了无线 AP 隔离,都没用。后来用抓包工具看多播流量,发现 Windows 主机的多播响应根本没发出去。
问题在 Windows Defender 防火墙的入站规则。UniMate 的 mDNS 监听端口5353默认被拦截,放行之后设备列表立刻出现。现在的安装包里会主动写入防火墙规则,否则让用户手动添加太反人性。同样的问题也会出现在企业网络或有第三方安全软件的机器上,排查时优先看防火墙。
4.2 WebSocket 重连机制一塌糊涂,退避算法救了我
初版的重连逻辑很简单:断线立即重连,导致路由器一波动,所有设备进入疯狂重连状态。每台设备同时向对端打连接请求,消息也会重复发送。后来改成指数退避:第一次 1 秒,第二次 2 秒,第四次 4 秒,最多 30 秒封顶。重连成功后还要发送一次StateSync消息,让对端补齐断线期间的消息。补齐消息同样走 msg_id 去重,这样用户不会看到重复通知。
调试这个问题的难点在于复现,不稳定网络是最难模拟的。后来我用tc命令给网卡随机加丢包和延迟,才能稳定复现。
4.3 剪贴板同步被“文件列表”逼疯
有次我在电脑上选中一批文件按 Ctrl+C,再把手机解锁,结果手机弹出十几条通知,全部是文件路径的字符串。这是因为 Windows 复制文件时,剪贴板里同时存在 CF_HDROP 和文本格式的路径列表,我的监听程序把路径列表当正文同步到手机了。
处理方式是在剪贴板消息的 mime 字段里增加“内容类型”约束。音频、图片、文件列表这些类型默认不同步,除非用户显式指定。现在这条规则已经写死在代码里,几乎不会误触发。
4.4 局域网找不到设备时,中继模式是最后一道保险
即使做了 mDNS 加手动配对,两台设备完全不在同一网络时依然无法直连。UniMate 支持一个可选的中继模式:数据包先发到我自己维护的中继服务器,再由服务器转发到对端。中继服务器只做转发,看不到内容,因为端到端加密的密钥只在两端持有,服务器手里只有密文。
这个模式很适合在外用手机流量连家里电脑的场景。实测 5G 网络下,走中继的传输速度约 8MB/s,延迟在 50ms 左右,用来传文档、看照片完全够用。
4.5 常见问题速查表
| 问题 | 表现 | 排查与解决 |
|---|---|---|
| 设备在局域网互相发现不了 | 设备列表为空 | 检查防火墙入站规则、路由器 AP 隔离、是否同一网段 |
| 剪贴板同步不及时 | 电脑复制后手机 5 秒才出现 | 检查剪贴板监听服务是否被系统休眠,查看日志确认是否被去重拦截 |
| 文件传输卡在 0% | 发送方消息已发,接收方无响应 | 检查接收端临时目录权限,特别是 Windows 锁屏后的写入权限 |
| 通知转发没反应 | 手机收到短信,电脑不弹 | 确认通知监听服务在系统设置里被允许,确认应用在“最近任务”里没被滑掉 |
| 连接反复断开 | 设备状态来回切换 | 检查网络切换场景,查看重连日志中的退避时间,必要时调整最大重试次数 |
5. 实测结果与性能数据
5.1 五类核心场景测试记录
项目进入可日常使用的阶段后,我连续两周记录了不同场景的实测数据,整理成下面这张表:
| 场景 | 测试环境 | 结果 |
|---|---|---|
| 100MB 文件传输 | Wi-Fi 5,同一局域网 | 平均 28 秒,峰值速度约 36MB/s |
| 2GB 大文件断点续传 | Wi-Fi 5,中途手动断开 Wi-Fi | 重新连接后续传,只补传约 12% 的块 |
| 剪贴板文本双向同步 | 台式机到手机 | 平均延迟 260ms |
| 验证码自动转发 | 手机 5G 网络,电脑连家庭宽带 | 短信收到后约 3 秒在电脑弹出 |
| 跨网段中继传输 | 手机 5G + 家中电脑 | 速度约 8MB/s,延迟 52ms |
这个数据在我的机器上很稳定,但不同硬件和网络环境差异会很大。尤其是在复杂无线环境里,2.4GHz 和 5GHz 混用可能导致速度波动,建议局域网传输固定用 5GHz 频段。
5.2 资源占用与后台保活
UniMate 的桌面端守护进程编译后用 Go 写的,常驻内存约 60MB,界面进程约 40MB。移动端受系统限制,Android 守护进程约 35MB,整夜运行耗电大约 2%。iOS 因为后台能力受限,通知转发成功率明显低于 Android,这是平台限制,目前没有完全绕过。
后台保活是移动端的重头戏。Android 上必须允许电池优化白名单,否则系统几分钟就会杀掉守护进程。这一步会影响所有同步功能,我在引导页里专门做了检查,用户跳过后会收到持续提醒。
6. 给想动手复刻或二次开发的人的经验建议
6.1 先定协议,再写传输,最后做界面
我一开始是先画界面,再做传输,再补协议,结果前两周的代码几乎全部推翻。正确顺序应该是先把消息类型定下来,哪怕先用 JSON 也好;把设备发现、配对、加密这三条链路打通,再开始做剪贴板监听和文件传输。协议稳定后,界面开发就是纯耗时间的体力活。
6.2 写一个模拟对端,效率翻倍
手动用两台真机测试是最慢的。我用 Go 写了一个虚拟对端,可以直接监听同一套协议端口,然后从脚本里发送固定序列的测试数据。这样剪贴板回环、消息去重、断线重连这些逻辑都可以在桌面端跑迭代,不用每次折腾两台设备。
6.3 留下的坑和未来方向
目前 UniMate 还有很多地方没完善,比如 iOS 的后台限制、Windows 锁屏状态下文件保存时的权限处理、以及规则引擎缺少一个可视化编辑界面。后续我想让规则配置做成普通用户也能操作的图形化流程,而不只是改 JSON。
这个项目从最开始的无聊小脚本,到现在已经是我日常离不开的基础设施了。每次换电脑,我一定会先装 UniMate,再装开发环境。它让我觉得手里的设备不是一个一个孤岛,而是一套真正可以协同的工作台。如果你也有类似痛点,建议不要急着找现成方案,试着从自己最常用的 2 个场景开始,写一个只服务你自己的“Mate”。