拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

跨设备协同实战:从剪贴板同步到大文件传输的架构设计与踩坑记录

跨设备协同实战:从剪贴板同步到大文件传输的架构设计与踩坑记录

先交代一下背景。我自己手边长期同时用一台 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 更适合长期维护
本地通信gRPCUI 和守护进程分离后,本地调用需要一套规范的接口定义
设备发现mDNS局域网自动广播,不需要配置中心
远距离传输WebSocket双向通信好实现,配合中继服务器也能跑
数据存储SQLite保存设备信息、消息记录、规则和配置,简单可靠

这套组合最大的好处是每个端都有一个本地守护进程,界面只是遥控器。守护进程负责监听剪贴板、接收消息、管理加密会话、执行规则,即使界面崩了,数据同步不会断。

1.3 架构分层:守护进程与界面分离

我最后做成的是“界面层 – 本地API – 核心守护 – 对端核心守护 – 对端界面”的分层结构。每台设备的 Flutter 只通过 gRPC 和本机守护通信,守护和守护之间才走加密数据流。这样有几个实际好处:

  • 命令行也能调用守护功能,方便写脚本。
  • 其他应用可以集成设备间的能力,不必依赖界面。
  • 后台运行时可以只跑轻量守护,内存占用比整个界面低得多。

实际开发中,分层最大的价值是能单独调试。我在写 Android 端时,直接对守护发 gRPC 请求,不需要先把界面画完。

2. 核心协议与数据模型设计

2.1 设备发现与配对:mDNS 广播加手动二维码兜底

设备发现我优先选择了 mDNS,也就是让每个守护进程在局域网里广播_unimate._tcp服务,广播内容包含设备名、设备类型和公钥指纹。这样用户打开 UniMate,理论上设备列表会自动出现。mDNS 不需要中心服务器,路由器配置一次后基本不用管。

但实际使用中,广播不一定可靠:Windows 防火墙可能拦,路由器可能隔离多播,公司网络尤其明显。所以 UniMate 还保留了手动配对通道:一方生成包含 IP 和配对码的二维码,另一方扫码后直接建立加密连接。

配对流程不是简单地“两边输同一个码”,我采用的是:

  1. 发起方生成 6 位随机配对码。
  2. 等待方展示自己的身份公钥。
  3. 发起方用配对码派生共享密钥,将加密后的握手消息发给等待方。
  4. 等待方解密后回执,双方交换并保存对方的长期身份公钥。

这 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 的传输流程是:

  1. 发起端生成file_id,发送文件元信息FileMeta。
  2. 接收端回复确认,并给出可接收的窗口大小。
  3. 发送端把文件按 1MB 切块,按滑动窗口发送FileBlock。
  4. 每个块都带序号和 SHA256 校验值。
  5. 接收端全部写完后,再对整体文件做一次校验,随后从.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”。

返回列表