先说结论
SixDrop 是一个打开网页就能用的传文件工具:两边各开一个网页,一边把 6 位取件码报给对方,对方输码,两个浏览器建立 WebRTC 直连,文件从一台设备直接流到另一台。
- 不装客户端、不注册、不加好友
- 直连时文件字节不经过服务器,服务器只交换 SDP/ICE 信令
- 没有会员、没有排队、没有「先上传再下载」
- 打不通会明确告诉你为什么,默认不会偷偷把文件传到云上(中转是可选项)
项目名是 SixDrop,名字和站点同名,前后端代码在一个仓库里,文末写了怎么自己部署一份;在线版入口放在最后一段。
为什么做这个
起因很具体:给同事传一段 2.3 GB 的录屏。
- 微信:超过 1 GB 直接拒绝
- 网盘:先上传 40 分钟,对方下载还要解压/会员
- 分享链接:对方打开先弹注册框
- 最后拿移动硬盘拷的
中间这些环节,本质都是「把文件先搬到某个中间服务器,再让对端去取」。可如果两个浏览器能直接对话,这些环节全都能省掉——WebRTC 的 DataChannel 就是干这个的。
怎么用
两种入口,按场景选:
| 场景 | 操作 |
|---|---|
| 两台设备在同一个 Wi-Fi | 大厅里会直接列出对方的设备,点「申请加入」→ 对方点「同意」→ 进同一个房间 |
| 不在同一个网络 | 一方复制链接或报 6 位取件码,另一方输码进房间 |
| 双 WAN / 多出口的网络 | 自动发现可能对不上(出口 IP 每条连接都在变),直接用取件码最稳 |
进房间之后:
- 选文件(可多选)
- 对方确认,选保存目录
- 传完房间还在:可以再发一批,也可以切到「消息」发文字(走同一条连接)
技术栈
| 层 | 选型 | 作用 |
|---|---|---|
| 接入 | Cloudflare Workers | 路由、取件码格式校验、静态资源分发 |
| 信令 | Durable Objects(带 SQLite 存储 + 休眠) | 每个房间一个 DO,转发 SDP/ICE;空闲自动休眠,不烧额度 |
| 同网发现 | 另一个 Durable Object(按出口 IP 分组) | 大厅列表、一次性邀请 token |
| 传输 | WebRTC DataChannel | 真正的数据通道,点对点 |
| NAT 穿透 | STUN(4 个公共服务器) | 拿 srflx 候选;不配置 TURN |
| 兜底中转 | R2(可选,用户主动点) | 打不通时把文件放 R2,给对端下载链接 |
| 校验 | DTLS 指纹 + SHA-256 | 两端显示同一个 8 位校验码,防中间人 |
| 前端 | 原生 JS,无框架 | 一个app.js,没有构建步骤 |
成本:Workers Free + Durable Objects Free 额度内,0 元。R2 只有主动用中转时才产生存储/流量。
三个关键设计
1)为什么是 6 位码,不是长链接
链接有三个实际问题:微信里点陌生链接会弹风险提示;太长,口头说不清;群里发出去谁都能点。
6 位码用的是去掉易混字符的 32 字符表(2-9 A-H J-N P-Z,没有0/1/I/O),口头报、手输都不容易错。码只在房间存活期内有效,房间用完可以换新码。
2)不装 TURN:打不通就直说
TURN 会让「连通率」好看,但代价是:所有流量都过你的服务器,免费额度撑不住,而且用户以为自己在直连。
所以这里的选择是:只做 STUN 打洞,不做 TURN。打不通就明确失败,把原因写清楚;需要兜底时,用户自己点「改用中转」,文件才会上 R2——这是一个显式动作,不是默认行为。
3)失败必须可见
失败页不是一句「网络错误」,而是给出:
- 候选统计:
host 2 / srflx 4 / relay 0,以及其中几个是 IPv6 - 失败类型:超时、打洞失败、传输中断
- 对端通知:两边同时知道,不会一边转圈一边干等
- 三条后续:换网络重试 / 同码重连 / 改用中转
做了这套之后,“为什么连不上” 从玄学变成了可以对着排查的东西。
传输实现:分块、背压、写盘
数据面没什么魔法,核心就是几个常量和一条发送循环:
// 分块默认 256 KiB(?chunk=64 可调小做对比),真正发送时还会按平台 SCTP 上限收窄constCHUNK_LIMIT=256*1024;// 背压水位:缓冲超过 8 MB 就停,降到 4 MB 再继续constHIGH_WATER=8*1024*1024;constLOW_WATER=4*1024*1024;发送循环(简化):
for(constbufofchunks){awaitwaitForDrain(dc);// bufferedAmount >= HIGH_WATER 就等 bufferedamountlowawaitsendChunk(dc,buf);// 队列满(send queue is full)时等对方接住,最长 30s}这两个地方都踩过坑:
- 只等背压不够:
dc.send()仍可能抛send queue is full(手机、5G、双 WAN 抖动时很常见)。早期实现 2 秒就放弃,直接判整个传输失败;现在改成等bufferedamountlow、最长 30 秒,期间界面提示「网络卡了一下,正在等对方接住」。 - 失败要分清:已经传过数据再断,标题是「传输中断」而不是「打洞失败」,诊断行也会说明「直连本来是通的」。
接收端写盘分两种:
- Chrome / Edge:File System Access API,边收边写,内存占用恒定
- Firefox / Safari:没有流式写盘,先收进内存再落盘,所以建议单文件 < 1 GB
校验码:两端对一下就知道有没有中间人
连接建立后,两端各自把 DTLS 指纹排序后做 SHA-256,取前 4 字节转成 8 位十六进制:
constbytes=newTextEncoder().encode([localFingerprint,remoteFingerprint].sort().join('|'));constdigest=newUint8Array(awaitcrypto.subtle.digest('SHA-256',bytes));constcode=[...digest.slice(0,4)].map((b)=>b.toString(16).padStart(2,'0')).join('').toUpperCase();两端显示的是同一个码,说明双方看到的是同一对证书,没有中间人。这个码不是文件哈希,是连接的指纹——文件完整性由 DTLS 自己保证。
实测与测试
速度这件事我不想吹:同一个 Wi-Fi 的局域网直连、跨网络打洞、经 R2 中转,这三档完全不是一个量级,而且跨网络能不能打通还取决于两边 NAT。
能说的是工程上的验证:
- 本地 e2e(Playwright + 本地 Edge):3 个文件共 4.4 MB,端到端 1.8–2.5 s,逐文件哈希一致
- 房间模型 e2e:同房间连续三批、接收中发消息不抢焦点、拒绝/返回、离开换码
- 同网发现 e2e:互相发现、申请/同意、拒绝、可见性开关
- 单测 5 组:房间状态机、大厅、信令协议、前端结构、R2
其中专门注入过一次「发送队列满」:让发送端前 3 秒所有send都抛queue is full,验证传输能挺过去——这在真实手机上是会发生的。
局限(重要,别踩坑)
| 限制 | 说明 |
|---|---|
| 对称 NAT / 公司网 / 校园网 | 打洞大概率失败,没有 TURN 就是打不通;可以改用中转 |
| 双 WAN、CGNAT、VPN | 出口 IP 每条连接都可能变,自动发现会时有时无,用取件码 |
| Firefox / Safari 大文件 | 没有流式写盘,建议单文件 < 1 GB |
| 没有断点续传 | 中断要重传;对端在线才能传(不是离线分享) |
| 不做账号 | 没有历史记录,关掉页面就散 |
| 不适合同一场合给很多人发 | 一个房间是 1 对 1,多人分发请用网盘/CDN |
本地跑起来
# 先把仓库克隆到本地并进入目录,然后:nodetools/dev-server.mjs# 本地垫片,默认监听 127.0.0.1:8787nodetest/room.test.js# 单测nodetools/e2e.mjs# 端到端(需要本地装 Edge/Chromium)部署(需要自己的 Cloudflare 账号):
npminstallnpx wrangler deploywrangler.jsonc里已经配好ROOMS、DROPS、LOBBY三个 Durable Object 绑定和 R2 bucket,迁移标签是 v1/v2/v3。
node tools/dev-server.mjs默认监听127.0.0.1:8787(可用PORT覆盖);它是个本地垫片,不依赖 Cloudflare 账号就能调前端和信令。
代码结构
| 路径 | 内容 |
|---|---|
src/worker.js | 路由、房间创建、大厅/邀请、R2 中转 |
src/room.js | 房间 DO:WebSocket 信令、成员状态、knock/admit/reject |
src/lobby.js | 同网发现 DO:条目 TTL、一次性 invite token |
src/drop.js | R2 中转与分享页 |
public/app.js | 前端全部逻辑(约 2500 行,无框架) |
public/index.html/style.css | 大厅 + 房间两个视图、两个标签页 |
tools/*-e2e.mjs | 4 套 Playwright 验收脚本 |
适合谁 / 不适合谁
适合:临时给同事/家人传个文件、手机照片挪电脑、跨网络发个安装包、不想注册任何账号的场景。
不适合:需要长期保存和分享链接、需要多人同时下载、需要断点续传、或者网络环境本身打不通 P2P 的场景。
最后
这个项目最初只是想解决「传个文件为什么这么麻烦」,做完之后最大的收获其实是:把「连不上」的原因讲清楚,比把速度吹快更难,也更有用。
在线版就是一个网页:sixdrop.dev,两边打开就能用,不装、不注册。
如果你在自己的网络里也遇到过打洞失败,欢迎在评论区贴一下失败页的候选统计(host / srflx / relay 各几个、有没有 IPv6)——不同的 NAT 组合,处理方式确实不一样。