不搭服务器,让手机和电脑直接通信

Sail Link 是一款基于 Flutter 开发的局域网点对点通讯工具。

前言

在多端开发中,有一类需求非常常见,却经常需要用过重的方式解决。

比如:

  • 想把电脑上的一段测试文本发到手机
  • 想把 Android 测试机的截图发到 Windows
  • 想让 iPhone 和 Mac 在测试环境中互相发送消息
  • 想在几台设备之间验证消息通知
  • 想临时传一段视频,但当前环境没有后端服务器

这些需求本身并不复杂。

设备可能就在同一张桌子上,也连接着同一个 Wi-Fi,但为了让它们通信,我们经常需要先准备一套中心服务:

手机 → 服务器 → 电脑

这意味着要搭建主机、部署接口、处理账号、配置数据库,甚至还要考虑公网地址、HTTPS 和云服务费用。

对于正式产品,这些基础设施通常是必要的。

但对于开发、测试和局域网设备联调来说,它们可能只是额外负担。

于是我开发了 Sail Link。

Sail Link 想解决的问题很直接:

当手机和电脑处在同一局域网时,能不能不搭服务器,让设备直接发现彼此并通信?

答案是可以。

Sail Link 是一款使用 Flutter 开发的局域网点对点通讯工具。

它让每台设备同时承担两个角色:

  • 它可以接收其他设备的连接
  • 它也可以主动连接其他设备

通讯路径从传统的中心转发:

设备 A → 云服务器 → 设备 B

变成局域网直连:

设备 A ←────────→ 设备 B

只要两台设备处于同一个可互相访问的局域网,就可以直接发送:

  • 文字
  • 图片
  • 视频

不需要账号,不需要数据库,也不需要额外运行一台服务器。

为什么不直接使用微信、QQ 或网盘

日常传文件当然可以使用成熟的聊天软件。

但 Sail Link 的开发目标不是替代社交软件,而是服务于开发和测试环境。

在设备联调中,我们有时会遇到这些情况:

  • 测试机没有登录个人账号
  • 测试环境不能接入外部云服务
  • 企业设备不允许安装社交软件
  • 多台设备需要快速确认局域网连接
  • 需要观察文件传输进度和状态
  • 需要验证通知、声音和震动效果
  • 希望代码和协议都可以自己控制

Sail Link 更像一个轻量的开发工具,而不是社交产品。

它强调的是:

打开应用
发现设备
点击连接
直接发送

设备是如何自动发现的

Sail Link 使用 UDP 广播完成设备发现。

每台设备启动后,都会定期在局域网内广播一份简单的设备信息:

  • 设备名称
  • 设备类型
  • TCP 端口
  • 时间戳

默认使用 UDP 端口 37020

其他正在运行 Sail Link 的设备收到广播后,就能知道局域网中出现了一台可通信设备。

应用每隔几秒发送一次心跳。如果某台设备超过一段时间没有继续广播,就会被标记为离线。

这使得设备列表不需要中心服务器维护。

局域网本身就是设备发现的媒介。

为什么发现使用 UDP,聊天使用 TCP

UDP 很适合局域网设备发现。

它不需要先知道目标设备的地址,可以直接向局域网广播:

“我是 Sail Link,我在这里,我的 TCP 端口是 37021。”

但 UDP 不保证数据一定到达,也不适合直接传输完整图片和视频。

所以真正的消息和文件传输使用 TCP。

当用户点击一台设备时,Sail Link 会连接该设备广播出来的 TCP 端口。

TCP 可以提供:

  • 有序传输
  • 丢包重传
  • 长连接
  • 稳定的数据流
  • 更适合文件分块传输的基础

因此,Sail Link 的网络结构是:

UDP:找到设备
TCP:发送消息和文件

两种协议各自承担最适合的工作。

每台设备本身就是“服务器”

这里容易产生一个误解。

Sail Link 不需要部署服务器,并不代表它完全没有服务端逻辑。

实际上,每一台运行 Sail Link 的设备都会在本地启动一个 TCP 监听服务。

区别在于,这个服务不需要部署到云主机,也不需要单独维护。

它就在手机或电脑里运行。

所以更准确地说,Sail Link 是:

不需要中心服务器的设备直连方案。

每台设备同时具备:

本地 TCP Server
+
对端 TCP Client

这也是它能够双向通信的基础。

文字消息是如何发送的

当两台设备建立 TCP 连接后,文字会编码为 UTF-8 数据,再封装为 Sail Link 自己的数据包。

接收端不会把每次 Socket 回调都当成一条完整消息,因为 TCP 传输中可能出现:

  • 一条消息被拆成多个数据块
  • 多条消息被合并在一个数据块中

因此项目实现了增量拆包逻辑。

接收端会持续把 Socket 数据加入缓冲区,只有解析出完整数据包后,才生成一条聊天消息。

这是局域网通讯工具中很重要、但界面上看不到的一部分。

图片和视频为什么要分块传输

发送小段文字很简单,但图片和视频可能很大。

如果直接一次性读取整个文件并放进内存,会带来明显问题:

  • 大文件占用大量内存
  • 移动设备容易出现卡顿
  • 无法持续展示传输进度
  • 失败时难以处理临时数据

Sail Link 使用了一个简单的文件传输流程:

META

READY

CHUNK

COMPLETE

发送方先发送文件名称、大小和类型。

接收方创建临时文件,准备完成后回复 READY

发送方收到确认后,再开始分块发送文件内容。

接收方一边收到数据,一边写入磁盘,而不是把整个文件保存在内存中。

传输完成后,接收方会检查实际接收字节数是否与声明大小一致。

校验通过后,文件才会从临时目录移动到最终保存目录。

如果传输失败或被取消,未完成的临时文件会被清理。

为什么需要 READY 握手

发送方不能连接成功后立刻无脑发送大文件。

接收方可能还需要:

  • 创建临时目录
  • 创建文件
  • 打开写入流
  • 检查当前是否已经在接收其他文件

所以发送方会先发送文件元信息,然后等待接收方返回 READY

只有接收端确认准备完成,发送端才开始发送文件块。

这是一种很小的握手设计,但可以避免文件数据在接收端准备完成之前到达。

通知不只是弹一个提示框

Sail Link 的目标之一,是让多设备测试更接近日常应用体验。

当一台设备收到消息时,可以触发:

  • 应用内最新消息提醒
  • 系统通知
  • 系统提示音
  • 震动反馈

这些能力可以分别控制。

例如,开发者可以测试:

  • Windows 向 Android 发送文字时,Android 是否出现通知
  • 手机收到图片时,通知摘要是否正确
  • 静音模式下是否只显示通知
  • 关闭通知后,消息是否仍能正常接收
  • 震动开关是否生效

这让 Sail Link 不只是一个文件传输工具,也可以成为通知与多端交互测试的辅助工具。

为什么使用 Flutter

Sail Link 本身就是一个多端工具,所以 Flutter 很适合这个项目。

同一套 Dart 代码可以覆盖:

  • Android
  • iOS
  • Windows
  • macOS
  • Linux

网络协议、消息模型、状态管理和大部分界面逻辑都可以复用。

项目使用 Riverpod 管理:

  • 设备列表
  • 聊天记录
  • 文件传输进度
  • 消息状态
  • 用户设置
  • 最新入站提醒

不同平台真正需要区别处理的主要是:

  • 本地网络权限
  • 文件目录
  • 系统通知
  • 防火墙
  • 平台生命周期

需要说明的是,虽然 Flutter 项目目录中包含 Web,但当前核心通讯依赖 dart:io 的 UDP、TCP 和文件系统,所以 Web 并不是当前可用平台。

哪些设置会被保存

Sail Link 会在本地保存这些设置:

  • 设备显示名称
  • 文件保存目录
  • TCP 监听端口
  • 是否显示通知
  • 是否播放声音
  • 是否震动

这些设置会写入应用支持目录中的 JSON 文件。

因此,用户下次启动应用时不需要重新配置。

但聊天记录目前不同。

为什么当前聊天记录不会永久保存

当前聊天记录保存在 Riverpod 的内存状态中。

这意味着应用关闭后,消息列表会消失。

这样设计的好处是实现简单,也符合当前“临时开发通讯工具”的定位。

但它也有明显限制:

  • 无法查看历史消息
  • 重启后无法找到之前接收的文件入口
  • 无法搜索聊天记录
  • 无法恢复失败任务

接收完成的图片和视频文件仍然保存在磁盘中,只是聊天界面中的消息索引不会恢复。

后续如果需要把 Sail Link 做成长期使用的工具,可以增加 SQLite、Isar 或其他本地持久化方案。

没有中心服务器意味着什么

没有中心服务器带来了很多便利:

  • 不需要部署
  • 不需要账号系统
  • 不需要公网域名
  • 不需要数据库
  • 数据不经过云端中转
  • 同一局域网内延迟较低

但它也带来了明确边界:

  • 设备必须在同一可达局域网
  • 不支持跨互联网通信
  • 不支持离线消息
  • 不支持 NAT 穿透
  • 移动系统后台限制会影响在线状态
  • 局域网隔离会导致设备无法发现

所以 Sail Link 不是“另一款互联网聊天软件”。

它的使用范围是清晰的:

可信局域网内的开发、测试和临时设备通讯

局域网并不等于天然安全

很多人会认为,只要数据不经过互联网,就一定安全。

这并不准确。

如果设备连接的是公共 Wi-Fi、校园网络、酒店网络或其他包含陌生设备的局域网,那么其他设备可能有机会发现 Sail Link 的广播端口。

当前版本没有实现:

  • 账号认证
  • 设备配对
  • 可信设备列表
  • 访问令牌
  • 应用层传输加密
  • 端到端加密

因此,不应该用当前版本传输密码、密钥、身份证件、客户信息或其他敏感数据。

它更适合:

  • 家庭私人网络
  • 团队测试网络
  • 手机热点
  • 实验室隔离网络
  • 受信任的临时联调环境

安全边界必须说清楚,否则“无服务器”很容易被误解成“天然安全”。

为什么有时候连接同一个 Wi-Fi 也发现不了

同一个 Wi-Fi 名称并不一定意味着设备之间可以互相通信。

很多路由器和公共网络会开启:

  • AP Isolation
  • Client Isolation
  • 访客网络隔离
  • 企业终端隔离

这些功能会阻止同一网络中的设备直接访问彼此。

除此之外,Sail Link 还可能受到:

  • Windows 防火墙
  • macOS 本地网络权限
  • iOS 本地网络权限
  • Android 厂商网络限制
  • VPN 路由
  • 多网卡选择
  • TCP 端口占用

所以当设备无法连接时,第一步不是检查聊天代码,而是确认网络是否真的允许设备互访。

我认为它比较适合以下场景。

多端 UI 和功能联调

开发者可以在电脑、Android 和 iOS 之间快速发送测试内容,不需要为一个临时功能准备接口。

通知测试

可以从另一台设备主动发送消息,验证系统通知、声音和震动行为。

图片与视频传输测试

可以观察文件选择、发送状态、接收进度、保存路径和媒体预览。

测试机内容交换

没有登录社交账号的测试机,也可以在可信局域网中直接交换内容。

网络协议学习

项目包含设备发现、Socket 通信、拆包、文件分块、进度事件和异常清理,适合用于学习 Flutter 桌面与移动端网络编程。

它不适合哪些场景

当前版本不适合:

  • 公网聊天
  • 企业正式即时通讯
  • 跨城市设备通讯
  • 敏感文件传输
  • 离线消息系统
  • 需要聊天历史的长期沟通
  • 对后台在线率有严格要求的场景
  • 不可信公共网络

清楚地说明不适用范围,比把项目描述成“万能通讯工具”更重要。

下一步可以做什么

Sail Link 现在已经完成了最基本的设备发现、文字、图片、视频和通知闭环。

接下来比较有价值的方向包括:

  • 任意文件发送
  • 文件拖拽
  • 剪贴板同步
  • 聊天历史持久化
  • 设备配对
  • 可信设备确认
  • 传输加密
  • 文件哈希校验
  • 多任务队列
  • 暂停与继续
  • 断点续传
  • 二维码连接
  • 手动输入 IP
  • 桌面托盘
  • 通知点击进入对应会话
  • 局域网群发
  • 协议版本协商

其中,设备认证和加密应该是走向更广泛使用前的重要步骤。

总结

Sail Link 的出发点并不是创造一种新的聊天方式。

它只是想把一个原本需要服务器、接口和账号体系才能完成的开发需求,压缩成一个更直接的流程:

两台设备连接同一个可信局域网
打开 Sail Link
自动发现
直接通信

UDP 负责发现设备,TCP 负责可靠传输,文件通过握手和分块写入完成传输,系统通知负责提醒用户。

整个过程不经过中心服务器,也不要求额外准备主机。

对于正式产品,它还缺少认证、加密、历史记录和后台能力。

但对于多端开发、设备联调和临时测试环境,它已经能够减少很多不必要的基础设施准备。

这就是我开发 Sail Link 的目的:

让手机和电脑在需要沟通的时候,可以直接连接彼此,而不是先去搭建一台服务器。