从 NearLink 看跨平台局域网互传:难点不在“传文件”,而在边界

  • Post category:独立开发

GitHub:https://github.com/Terrydev5/NearLink

在 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 区分 hellotext_messagefile_offerfile_acceptfile_rejecttransfer_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 的验证至少要覆盖:

  1. 两台真实设备是否能互相发现;
  2. 设备名称变化、权限拒绝、离开 Wi-Fi 后 UI 是否能正确更新;
  3. 文本消息能否在双向连接场景下正确归属到发送方;
  4. 文件接受、拒绝、token 过期、未授权连接和 SHA-256 不匹配时,状态是否正确结束;
  5. 收到的文件是否进入用户预期的位置:macOS 下载目录、iOS App 文档目录或 Android 对应的媒体/下载位置。

我把“实机局域网验证”写进项目的贡献规范,原因很简单:单元测试可以验证编码、摘要和状态机,但不能替你验证路由器、多播策略、权限弹窗与不同系统实现的组合行为。

可以迁移到下一个项目的四条原则

  1. 先画清边界,再选技术。 “局域网直连”不是“安全文件传输”的同义词;先确定信任模型和使用场景。
  2. 发现、会话、数据传输分层。 它们的失败方式与生命周期不同,不要用一条连接硬扛所有语义。
  3. 协议是跨端项目的共同产品。 在两端各自实现之前,先写出消息信封、版本策略、状态流转和失败处理。
  4. 安全能力要精确命名。 SHA-256 解决完整性检查;一次性 token 限制临时端口访问;两者都不等于保密性或身份认证。

NearLink 当前仍是实验性项目。下一步会优先补充跨端协议测试样本、传输取消与恢复机制;如果要进入敏感数据场景,则必须先完成设备配对、加密与认证,而不是把它包装成“安全互传”。


项目仓库:https://github.com/Terrydev5/NearLink
协议说明:docs/protocol.md
隐私与安全边界:docs/privacy.md