我做了一个手机和浏览器互传工具: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。服务端返回 sessionId、challenge、浏览器侧信令凭证、过期时间和浏览器设备信息。浏览器把手机需要的信息编码进二维码。手机扫码后,就知道自己要连接哪个服务端、加入哪个会话、用哪个 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 整理开源。