EasyChat - 局域网互传:手机装 App,另一端打开网页就能互传文件

66 阅读3分钟

我做了一个手机和浏览器互传工具:EasyChat。

Web 入口:easychat.leazer.top

iOS App Store:apps.apple.com/cn/app/easy…

它的核心使用方式是:

电脑打开网页 -> 手机 App 扫二维码 -> 确认设备 -> 开始互传

最重要的点是:另一端不需要安装任何软件。只要那台设备能打开浏览器,就能作为临时接收端或发送端。

这和很多互传工具不一样。很多工具要求手机和电脑都安装客户端,临时用公司的电脑、朋友的电脑、会议室电脑时,安装软件本身就是阻力。EasyChat 只要求手机端装 App,电脑端直接用 Web 页面完成配对和传输。

传输上,EasyChat 也不是把文件上传到服务器再下载。服务器只负责配对和 WebRTC 信令;配对完成后,文字和文件尽量通过局域网内的 WebRTC DataChannel 在手机和浏览器之间直接传。

配对流程

网页上的二维码不是普通链接,它代表一次短生命周期的配对会话。

浏览器和手机不是一开始就直接连上的。它们先通过服务端完成配对和 WebRTC 信令,等 DataChannel 建立后,文字和文件才走两端之间的通道。

这里用到四种连接方式:

  • HTTP:创建配对会话、手机注册、发送信令。
  • SSE:浏览器监听配对状态和浏览器侧信令。
  • WebSocket:手机监听服务端转发过来的信令。
  • WebRTC DataChannel:配对成功后的文字和文件互传。

浏览器打开 EasyChat Web 后,会先用 HTTP 创建一个 pairing session。服务端返回 sessionIdchallenge、浏览器侧信令凭证、过期时间和浏览器设备信息。浏览器把手机需要的信息编码进二维码。手机扫码后,就知道自己要连接哪个服务端、加入哪个会话、用哪个 challenge 注册,以及要向用户展示哪个浏览器设备。

配对流程如下:

sequenceDiagram
  participant Browser as 浏览器 Web
  participant Worker as Cloudflare Worker
  participant DO as Durable Object
  participant Phone as 手机 App

  Browser->>Worker: HTTP POST /api/pairings 创建 session
  Worker->>DO: 写入 session 状态
  Worker-->>Browser: 返回 sessionId / challenge / token / expiresAt
  Browser-->>Browser: 生成二维码
  Browser->>Worker: SSE GET /api/pairings/:id/events 等待手机注册
  Worker->>DO: 绑定浏览器状态订阅
  Phone->>Phone: 扫码并解析 session 信息
  Phone->>Worker: HTTP POST /api/pairings/:id/register 注册
  Worker->>DO: 更新为 phone_registered
  DO-->>Worker: status: phone_registered
  Worker-->>Browser: SSE 推送手机已注册
  Browser->>Worker: SSE GET /api/pairings/:id/signals 订阅浏览器信令
  Worker->>DO: 绑定浏览器信令订阅
  Phone->>Worker: WebSocket /api/pairings/:id/ws-signals 订阅手机信令
  Worker->>DO: 绑定手机信令订阅
  Browser->>Worker: HTTP POST /signals 发送 WebRTC offer
  Worker->>DO: 投递 offer
  DO-->>Worker: offer
  Worker-->>Phone: WebSocket 转发 offer
  Phone->>Worker: HTTP POST /signals 返回 answer
  Worker->>DO: 投递 answer
  DO-->>Worker: answer
  Worker-->>Browser: SSE 转发 answer
  Browser<->>Worker: HTTP POST + SSE 交换 ICE candidates
  Phone<->>Worker: HTTP POST + WebSocket 交换 ICE candidates
  Browser<-->>Phone: WebRTC DataChannel 建立

这套流程里,Worker 和 Durable Object 只保存配对需要的短期状态:

  • 当前 session 是否有效。
  • 手机是否已经注册。
  • 浏览器和手机各自的信令订阅。
  • 未送达的少量信令消息。
  • 会话过期时间。

服务端不需要保存聊天内容、文件内容和文件名。

文件怎么传

WebRTC DataChannel 建立后,数据路径就从“经过服务端转发信令”切换成“两端直接互传数据”:

手机 App <------ WebRTC DataChannel ------> 浏览器页面

DataChannel 里传两类消息:

  • 控制消息:文本、文件开始、文件完成、取消、心跳。
  • 二进制分片:真实文件内容。

文件传输流程如下:

sequenceDiagram
  participant Sender as 发送方
  participant Channel as WebRTC DataChannel
  participant Receiver as 接收方

  Sender->>Channel: file_offer(文件名/大小/类型/分片数量)
  Channel-->>Receiver: file_offer
  Receiver->>Channel: file_resume(nextChunk)
  Channel-->>Sender: file_resume
  loop 按分片发送
    Sender->>Channel: binary chunk
    Channel-->>Receiver: binary chunk
    Receiver-->>Receiver: 记录进度并缓存分片
  end
  Sender->>Channel: file_complete
  Channel-->>Receiver: file_complete
  Receiver-->>Receiver: 检查是否缺片
  alt 文件完整
    Receiver->>Channel: file_received
    Channel-->>Sender: file_received
  else 存在缺片
    Receiver->>Channel: file_resume(缺失位置)
    Channel-->>Sender: 继续补发
  end

为什么要分片:

  • 大文件不能一次性塞进 DataChannel。
  • 分片后可以展示传输进度。
  • 缓冲区过高时可以暂停,避免浏览器或手机卡住。
  • 如果缺片,可以从指定分片继续补发。

所以 EasyChat 的文件传输不是“上传到服务器,再下载到另一端”,而是:

服务器:配对 + 信令
手机和浏览器:文字 + 文件

这带来三个直接结果:

  • 电脑端零安装,浏览器就是客户端。
  • 文件走局域网直传,不绕服务器中转。
  • 服务端不保存聊天内容、文件内容和文件名。

后续我会把 EasyChat 整理开源。