在 iOS、macOS 与 Android 之间做一个“同一 Wi-Fi 下互传消息和文件”的工具,第一反应往往是:找到对方 IP,开一个 Socket,把字节写过去。
真正实现后会发现,Socket 只是最后一步。一个能跨平台工作的最小方案,至少要同时回答四个问题:怎样找到对方、怎样确认正在沟通的是谁、怎样组织不同类型的消息、文件流怎样不被随意读取或悄悄损坏。
NearLink 是一个实验性的 iOS/macOS/Android 局域网传输项目。它没有账号和云端中转,使用 Bonjour / DNS-SD 做发现,WebSocket 负责控制消息,临时 TCP 连接负责文件流。本文记录其中的设计取舍,以及哪些结论可以迁移到别的局域网通信项目中。
先明确目标:什么叫“本地互传”
这里的“本地”不是“无需网络”,而是设备处在同一个可互相访问的局域网内,不经过业务服务器转发。
这带来三个直接收益:
- 不需要注册、登录、云端文件存储或带宽成本;
- 文件数据不必离开当前网络;
- 传输路径短,适合近距离设备协作。
但它也放弃了云服务的一些能力:没有全局在线状态、没有跨网络投递、也不能天然把对方认定为可信用户。于是,设计目标不应是“模仿网盘”,而是:在一个可信局域网内,让用户看得见、选得对、传得完整。
一个最小的正确模型:发现、控制、数据分三层
NearLink 没有让一条连接同时承担所有职责,而是分为三层:
┌─────────────┐ DNS-SD / Bonjour ┌─────────────┐
│ iOS / macOS │ ───────────────────────▶ │ Android │
│ NearLink │ ◀─────────────────────── │ NearLink │
└─────────────┘ 发现可用设备与元数据 └─────────────┘
│ │
└──── WebSocket:hello、文字、文件协商 ─────┘
└──── 临时 TCP:经授权的文件字节流 ─────────┘
这不是为了“架构漂亮”,而是因为三类问题的生命周期不同:
| 层 | 解决的问题 | 生命周期 | NearLink 的选择 |
|---|---|---|---|
| 发现层 | 附近有哪些可用设备? | 持续浏览 | Bonjour / DNS-SD |
| 控制层 | 发什么消息、是否接收文件? | 会话期间 | WebSocket + JSON |
| 数据层 | 怎样高效传一个大文件? | 一次传输 | 独立临时 TCP 流 |
如果把文件直接塞进 WebSocket 消息,文本聊天、小型状态消息和大文件会互相争抢同一条通道:进度、取消和异常恢复都难以清楚处理。反过来,为每个文本消息单独开 TCP 连接也不划算。
因此,控制信令与大数据分离是这里最重要的工程判断。它适用于局域网传输,也适用于上传、下载、直播控制等更广泛的系统。
发现不是身份认证:Bonjour 只能回答“附近有什么”
Apple 端使用 Network.framework,Android 使用 NSD(Network Service Discovery)。双方都发布和浏览同一个服务类型:_nearlink._tcp。
服务的 TXT 记录携带少量用于建立会话的信息:
deviceId 稳定设备 UUID
platform iOS、macOS 或 android
protocolVersion 当前协议版本
这解决了“如何列出附近设备”,但不能保证浏览结果总是完整。实际中,发现回调可能先给出服务名,稍后才得到 TXT 记录;系统还可能因重名调整服务名。
NearLink 的处理不是把服务名当身份,而是先用它展示设备,再通过 WebSocket 的 hello 消息取得并校正稳定的 deviceId。这意味着:
发现层给的是“候选连接”,握手层给的才是“应用层身份”。
即使当前项目的 deviceId 还不是加密意义上的可信身份,这个分层也避免了 UI、连接缓存和协议状态长期绑定在不稳定的服务名上。
协议先稳定,再让两端分别实现
跨端项目最容易出现的问题不是某一端写不出代码,而是 Swift 和 Kotlin 各自“理解了一套协议”。所以 NearLink 使用统一的 JSON 信封:
{
"version": 2,
"type": "file_offer",
"messageID": "UUID",
"timestamp": 1760000000000,
"payload": {}
}
其中 type 区分 hello、text_message、file_offer、file_accept、file_reject、transfer_complete 等事件;version 则让接收方可以明确拒绝不兼容的格式,而不是“勉强解析后出错”。
文件传输的关键不是马上发送字节,而是先完成协商:
发送方 接收方
│ file_offer(名称、大小、摘要、端口、令牌) │
│ ───────────────────────────────────────▶ │
│ │ 用户决定是否接收
│ file_accept / file_reject │
│ ◀─────────────────────────────────────── │
│ │
│ 新建 TCP 数据连接,先发送一次性令牌 │
│ ◀─────────────────────────────────────── │
│ 验证令牌后才发送文件字节 │
│ ───────────────────────────────────────▶ │
│ │ 校验 SHA-256
│ transfer_complete │
│ ◀─────────────────────────────────────── │
这个流程把“是否允许收文件”和“怎样读字节”分开了。用户拒绝时,不会建立数据流;传输结束时,控制层还有一个明确的完成事件可用于更新 UI 和历史记录。
临时端口为什么还需要一次性 token
一个容易被忽略的细节是:文件发送方需要打开临时 TCP 监听端口。如果只把端口告诉接收方,那么同一网络中的其他设备只要扫描到端口,就可能抢先连上并读取数据。
NearLink 在每次 file_offer 中加入一个随机生成的 256-bit、URL-safe token:
- token 随文件邀请一起在控制通道中发送;
- 接收方连上数据端口后,先发送
token + "\\n"; - 发送方验证 token,且只服务第一个验证通过的客户端;
- token 两分钟后过期;
- 文件按 256 KiB 分块传输,接收端一边写入、一边计算 SHA-256,最终与邀请中的摘要对比。
这样做不能把局域网变成安全信道,但能阻止“知道端口就能读文件”的低门槛问题,也能发现传输过程中的文件损坏。
这里必须把安全结论说完整:token 是临时数据流的访问凭据,不是加密。 当前控制消息与文件字节都没有端到端加密,也没有配对或强身份认证。能监听或篡改局域网流量的攻击者,仍可能观察到 token 和内容。因此 NearLink 仅适合可信网络与可信可见设备,不应传输隐私或高价值数据。
三种方案怎么选
局域网互传并不存在放之四海皆准的技术选型。下面是常见路径及其适用条件:
| 方案 | 优点 | 代价 | 更适合 |
|---|---|---|---|
| 发现 + WebSocket + 临时 TCP(NearLink) | 分层明确,跨端实现直接,便于展示传输状态 | 需要自己设计协议、处理权限与网络异常 | 同网段跨平台原型与工具型 App |
| 只用 HTTP / WebSocket | 接入简单,服务器和调试工具多 | 大文件、进度、授权与取消语义容易耦合 | 文本为主或文件较小的内部工具 |
| 云端对象存储中转 | 可跨网络、离线可取、权限模型成熟 | 成本、账号、隐私与后端运维 | 面向普通用户的长期文件服务 |
| 平台专有近距离能力 | 往往体验更顺滑 | 跨 iOS/Android 的可移植性差 | 单一生态产品 |
NearLink 的选择只在“跨 Apple 与 Android、同一局域网、可接受实验性安全边界”这一条件下成立。若产品要跨公网、支持陌生人互传或承载敏感资料,优先级应转向身份认证、加密传输、密钥管理与服务端授权,而不是继续给 mDNS 增加功能。
跨端调试:模拟器不能替代实机
局域网发现依赖 mDNS、多播、系统权限和真实网络接口。这些恰好是模拟器与模拟环境最容易偏离真实设备的部分。
NearLink 的验证至少要覆盖:
- 两台真实设备是否能互相发现;
- 设备名称变化、权限拒绝、离开 Wi-Fi 后 UI 是否能正确更新;
- 文本消息能否在双向连接场景下正确归属到发送方;
- 文件接受、拒绝、token 过期、未授权连接和 SHA-256 不匹配时,状态是否正确结束;
- 收到的文件是否进入用户预期的位置:macOS 下载目录、iOS App 文档目录或 Android 对应的媒体/下载位置。
我把“实机局域网验证”写进项目的贡献规范,原因很简单:单元测试可以验证编码、摘要和状态机,但不能替你验证路由器、多播策略、权限弹窗与不同系统实现的组合行为。
可以迁移到下一个项目的四条原则
- 先画清边界,再选技术。 “局域网直连”不是“安全文件传输”的同义词;先确定信任模型和使用场景。
- 发现、会话、数据传输分层。 它们的失败方式与生命周期不同,不要用一条连接硬扛所有语义。
- 协议是跨端项目的共同产品。 在两端各自实现之前,先写出消息信封、版本策略、状态流转和失败处理。
- 安全能力要精确命名。 SHA-256 解决完整性检查;一次性 token 限制临时端口访问;两者都不等于保密性或身份认证。
NearLink 当前仍是实验性项目。下一步会优先补充跨端协议测试样本、传输取消与恢复机制;如果要进入敏感数据场景,则必须先完成设备配对、加密与认证,而不是把它包装成“安全互传”。
项目仓库:https://github.com/Terrydev5/NearLink
协议说明:docs/protocol.md
隐私与安全边界:docs/privacy.md