前言
在多端开发中,有一类需求非常常见,却经常需要用过重的方式解决。
比如:
- 想把电脑上的一段测试文本发到手机
- 想把 Android 测试机的截图发到 Windows
- 想让 iPhone 和 Mac 在测试环境中互相发送消息
- 想在几台设备之间验证消息通知
- 想临时传一段视频,但当前环境没有后端服务器
这些需求本身并不复杂。
设备可能就在同一张桌子上,也连接着同一个 Wi-Fi,但为了让它们通信,我们经常需要先准备一套中心服务:
手机 → 服务器 → 电脑
这意味着要搭建主机、部署接口、处理账号、配置数据库,甚至还要考虑公网地址、HTTPS 和云服务费用。
对于正式产品,这些基础设施通常是必要的。
但对于开发、测试和局域网设备联调来说,它们可能只是额外负担。
于是我开发了 Sail Link。
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 端口占用
所以当设备无法连接时,第一步不是检查聊天代码,而是确认网络是否真的允许设备互访。
Sail Link 当前适合哪些场景
我认为它比较适合以下场景。
多端 UI 和功能联调
开发者可以在电脑、Android 和 iOS 之间快速发送测试内容,不需要为一个临时功能准备接口。
通知测试
可以从另一台设备主动发送消息,验证系统通知、声音和震动行为。
图片与视频传输测试
可以观察文件选择、发送状态、接收进度、保存路径和媒体预览。
测试机内容交换
没有登录社交账号的测试机,也可以在可信局域网中直接交换内容。
网络协议学习
项目包含设备发现、Socket 通信、拆包、文件分块、进度事件和异常清理,适合用于学习 Flutter 桌面与移动端网络编程。
它不适合哪些场景
当前版本不适合:
- 公网聊天
- 企业正式即时通讯
- 跨城市设备通讯
- 敏感文件传输
- 离线消息系统
- 需要聊天历史的长期沟通
- 对后台在线率有严格要求的场景
- 不可信公共网络
清楚地说明不适用范围,比把项目描述成“万能通讯工具”更重要。
下一步可以做什么
Sail Link 现在已经完成了最基本的设备发现、文字、图片、视频和通知闭环。
接下来比较有价值的方向包括:
- 任意文件发送
- 文件拖拽
- 剪贴板同步
- 聊天历史持久化
- 设备配对
- 可信设备确认
- 传输加密
- 文件哈希校验
- 多任务队列
- 暂停与继续
- 断点续传
- 二维码连接
- 手动输入 IP
- 桌面托盘
- 通知点击进入对应会话
- 局域网群发
- 协议版本协商
其中,设备认证和加密应该是走向更广泛使用前的重要步骤。
总结
Sail Link 的出发点并不是创造一种新的聊天方式。
它只是想把一个原本需要服务器、接口和账号体系才能完成的开发需求,压缩成一个更直接的流程:
两台设备连接同一个可信局域网
打开 Sail Link
自动发现
直接通信
UDP 负责发现设备,TCP 负责可靠传输,文件通过握手和分块写入完成传输,系统通知负责提醒用户。
整个过程不经过中心服务器,也不要求额外准备主机。
对于正式产品,它还缺少认证、加密、历史记录和后台能力。
但对于多端开发、设备联调和临时测试环境,它已经能够减少很多不必要的基础设施准备。
这就是我开发 Sail Link 的目的:
让手机和电脑在需要沟通的时候,可以直接连接彼此,而不是先去搭建一台服务器。