docs: 文件传输与局域网专项功能技术方案
产品负责人提出两个需求:① 增加文件传输,不落服务端、端到端、支持超出服务器磁盘的大文件;
② 设备接入局域网时增加一组专项功能。本文给出可行性判定与具体设计。
**结论**:三条诉求都可行,但**必须先说清三条硬约束**,否则方案会打脸:
① **现有协议不能承载文件**。实测 `shared/protocol.js` 的边界:
字符串 ≤512 字节 / 单层键 ≤64 / 嵌套深度 1 —— 它是为小控制消息
(capabilities、keepalive)设计的,1MB 的分片连第一个约束都过不去。
→ 文件必须走**独立通道**,不能塞进 agent:metrics 那套。
(顺带确认:「对象一律丢弃」那段注释是**历史缺陷记录、已修**,
现在允许一层嵌套对象,capabilities 机制可用。)
② **比磁盘更该担心流量包**。腾讯云轻量按流量计费,服务端中继一个 10GB 文件
= 上行 10GB + 下行 10GB = 双倍。所以"不落盘"只是第一步,
**"不经过服务器"才是关键** —— 这两件事必须分开说。
③ **P2P 不是总能打通**(对称 NAT/CGNAT)。失败就得中继,而中继就是②里烧流量的东西。
这个取舍必须显式做出来并让用户知情,不能默默降级。
**文件传输三方案**:A 同网段直连(服务端零流量、100% 成功、零新依赖)
/ B 公网 P2P WebRTC(零流量但引入 flutter_webrtc 增重)/
C 服务端流式中继(唯一会烧流量的)。
推荐 **A 优先 → B 兜底 → C 默认关闭**,理由是收益/代价比而非技术偏好。
信令只需三拍,且**形状落在现有协议边界内,不动 protocol.js**。
**局域网专项五项**:① LAN 优先直连 ② LAN 高速采样 ③ LAN 大文件传输
④ 断公网可用(需采集端有界落盘)⑤ 链路自检(让用户看得见走了哪条路)。
建议顺序 ①②→⑤→③→④。
⚠️ **本文划了一条合规红线**:**同网段不能成为免授权理由**。
方案 2.1 的定位是"监控自己或经明确授权的设备、不提供绕过对方操作的隐蔽监控",
若做成"连上同一 Wi-Fi 就能看到",等于把物理接近当成授权依据 ——
那正是隐蔽监控的形态,与过审与杀软信誉直接冲突。
故:局域网只降低流量与延迟,**不降低授权门槛**;不做同网段自动发现入库。
另附风险清单(流量包、内置 HTTP 服务被扫、采集端落盘配额、
flutter_webrtc 增重、局域网误判不能只凭 IP)。
This commit is contained in:
1 parent
ba41b5ec5c
commit
52cc4bf4d6
1 file changed
+215
@@ -0,0 +1,215 @@
|
||||
# 文件传输与局域网专项功能 · 技术方案
|
||||
|
||||
> 2026-09-18 立。**基于现有代码与部署实况**(非通用方案):
|
||||
> 网关 `socket.io` 的 agent→gateway→viewers 订阅模型、`shared/protocol.js` 的净水规则、
|
||||
> 生产为腾讯云轻量 **2C / 3.5G 内存 / 60G 盘(可用 45G)**。
|
||||
|
||||
## 零、结论先给
|
||||
|
||||
| 诉求 | 判定 | 依据 |
|
||||
|---|---|---|
|
||||
| **不落服务端存储** | ✅ **完全可行** | 三条路径都不需要服务端写盘——数据面可以完全绕开服务器 |
|
||||
| **端到端传输** | ✅ 可行,但**分场景** | 同网段直连 100% 成功;公网 P2P 受 NAT 影响,**必须有兜底** |
|
||||
| **支持超出服务器磁盘的大文件** | ✅ 可行 | 流式分片;服务端**不参与数据面**,容量就不再是瓶颈 |
|
||||
|
||||
### 但先要说清三条硬约束(否则设计会打脸)
|
||||
|
||||
**① 现有协议不能承载文件 —— 这不是"要不要改"的问题,是"根本走不通"**
|
||||
|
||||
`shared/protocol.js` 的净水规则(实测常量):
|
||||
|
||||
| 边界 | 值 | 位置 |
|
||||
|---|---|---|
|
||||
| 字符串长度上限 | **512 字节** | `MAX_STRING_LEN` |
|
||||
| 单层键数上限 | **64** | `MAX_PAYLOAD_KEYS` |
|
||||
| 嵌套深度 | **1** | `MAX_NEST_DEPTH` |
|
||||
|
||||
它是为**小控制消息**设计的(`capabilities` / `keepalive` 这类结构化字段)。
|
||||
一个 1MB 的分片连第一个约束都过不去。
|
||||
→ **文件必须走独立通道**,不能塞进 `agent:metrics` 那套。
|
||||
|
||||
(顺带:`sanitizePayload` 里那段"**对象一律丢弃**"的注释是**历史缺陷记录、已修** ——
|
||||
现在允许一层嵌套对象。`capabilities` 机制是可用的,这点后面要用。)
|
||||
|
||||
**② 比磁盘更该担心的是"流量包",不是"存储"**
|
||||
|
||||
腾讯云轻量按**流量包**计费。若让服务端做中继:
|
||||
**一个 10GB 文件 = 10GB 上行 + 10GB 下行 = 双倍计费**。
|
||||
|
||||
所以"不落盘"只是第一步 —— **"不经过服务器"才是关键**。这两件事必须分开说,
|
||||
否则会做出一个"省了磁盘、烧光流量"的方案。
|
||||
|
||||
**③ P2P 不是总能打通**
|
||||
|
||||
双方都在对称 NAT / 运营商级 NAT(CGNAT)后面时,直连会**失败**。
|
||||
失败就得走中继 —— 而中继就是 ②里那个烧流量的东西。
|
||||
**这个取舍必须显式做出来、并且让用户知情**,不能默默降级。
|
||||
|
||||
---
|
||||
|
||||
## 一、文件传输:三方案对比与推荐
|
||||
|
||||
| | **A 同网段直连** | **B 公网 P2P(WebRTC)** | **C 服务端流式中继** |
|
||||
|---|---|---|---|
|
||||
| 服务端存储 | 0 | 0 | 0 |
|
||||
| 服务端流量 | **0** | ≈0(仅信令,几 KB) | **文件大小的 2 倍** |
|
||||
| 成功率 | 100%(同网段) | 约 70~90%(看 NAT) | 100% |
|
||||
| 实现成本 | 低 | 中高(STUN/TURN + 原生插件) | 低 |
|
||||
| 大文件 | ✅ 不受限 | ✅ 不受限 | ⚠️ 受流量包限制 |
|
||||
|
||||
### 推荐顺序:**A 优先 → B 兜底 → C 默认关闭**
|
||||
|
||||
理由是**收益/代价比**,不是技术偏好:
|
||||
|
||||
- **A 零成本零风险**:局域网千兆,速度最快,服务端一分钟都不参与
|
||||
- **B 零服务端流量**,但要引入 `flutter_webrtc`(原生插件,**显著增重安装包**)——
|
||||
对一个已经 19MB 的 APK 来说,这笔账要单独算
|
||||
- **C 会烧流量包**,只能作为"用户明确同意"时的兜底
|
||||
|
||||
> ⚠️ 诚实提示:如果目标只是"把文件从 A 传到 B",**A 方案 + 一个可选的中继**
|
||||
> 往往比引入 WebRTC 全家桶更划算。B 的价值在**跨公网不走服务器**,
|
||||
> 若实际场景大多是"同一 Wi-Fi 下传",B 可以先不做。
|
||||
|
||||
### 共同骨架:信令三拍(服务端只做媒人)
|
||||
|
||||
```
|
||||
发起端 → 服务端 : transfer:offer {toDeviceId, name, size, sha256, mode:'lan'|'p2p'}
|
||||
服务端 → 接收端 : transfer:offer (转发,仅信令)
|
||||
接收端 → 服务端 : transfer:accept {sessionId, endpoint} 或 reject
|
||||
服务端 → 发起端 : transfer:accepted {endpoint}
|
||||
── 此后数据面双方直连,服务端不再参与 ──
|
||||
```
|
||||
|
||||
**信令复用现有通道即可**:`transfer:*` 是标量 + 一层对象的形状,
|
||||
**不需要动 `protocol.js` 的边界**(那 512/64/1 的规则照样满足)。
|
||||
|
||||
### A. 局域网直连(主路径,建议先做)
|
||||
|
||||
- **传输**:客户端内置一个**只绑内网**的 HTTP 服务(Dart 原生 `HttpServer`,零新依赖)
|
||||
- 接收端 `GET` + `Range` 头 ⇒ **断点续传白送**
|
||||
- 一次性 token 鉴权(**不能只凭同网段**,见第二节的红线)
|
||||
- **地址交换**:双方把 `192.168.x.x:port` 告诉服务端,服务端只做媒人
|
||||
- **不做 mDNS/SSDP**:跨平台差异大、且会主动广播"我在这里";
|
||||
用现成通道交换地址已经够,还少一份要维护的发现协议
|
||||
- **数据不经过服务器** ⇒ 不需要 TURN、不产生任何流量 ✅
|
||||
|
||||
### B. 公网 P2P(跨公网时)
|
||||
|
||||
- WebRTC DataChannel;信令走现成 socket.io
|
||||
- **STUN 用自建 `coturn` 只开 STUN**(几乎零流量)
|
||||
- **TURN 默认不开**;要开必须叠加:并发上限 + 限速 + 单次配额 + **用户勾选确认**
|
||||
- ⚠️ 依赖代价:`flutter_webrtc` 会给包体与构建复杂度加一块原生负担
|
||||
|
||||
### C. 服务端流式中继(兜底,默认关闭)
|
||||
|
||||
- 网关加 `transfer:pipe`:**纯流转发、不落盘**(`stream.pipe`)
|
||||
- 必须配套 4 件事:并发上限(如 2)、限速、单次配额、开关
|
||||
- **默认关闭**;开启时界面写明"**本次传输会消耗服务器流量,约 X**"
|
||||
|
||||
### 可靠性与校验(三方案共用)
|
||||
|
||||
| 机制 | 做法 |
|
||||
|---|---|
|
||||
| 分片校验 | 每片 sha256,失败**只重传单片** |
|
||||
| 整体对账 | 收完比对整体 sha256 |
|
||||
| 断点续传 | A/C 用 `Range`;B 用 offset 协商 |
|
||||
| 中断恢复 | **服务端不持有副本**,恢复依据是双方各自的分片状态 |
|
||||
|
||||
### 改动落点
|
||||
|
||||
| 位置 | 改动 |
|
||||
|---|---|
|
||||
| `shared/protocol.js` | **不动**(信令形状在现有边界内) |
|
||||
| `server/gateway/` | 加 `transfer:*` 信令转发(与现有订阅转发同构) |
|
||||
| `client/lib/host/` | 新增 `transfer.dart`:会话状态机 + A 方案的 HTTP 服务/客户端 |
|
||||
| `client/lib/ui/` | 新增传输卡片(进度 / 校验 / 重试) |
|
||||
| `deploy/` | 仅在启用 C 时才要动 nginx;**A/B 零改动** |
|
||||
|
||||
---
|
||||
|
||||
## 二、局域网专项功能
|
||||
|
||||
### ⚠️ 先划一条合规红线(**必须**)
|
||||
|
||||
> **同网段不能成为"免授权"的理由。**
|
||||
|
||||
方案 2.1 的定位是「监控**自己或经明确授权的**设备」,且**不提供任何绕过对方操作的
|
||||
隐蔽监控能力** —— 这是过审与杀软信誉的前提。
|
||||
|
||||
若做成"**连上同一 Wi-Fi 就能看到**",等于把"物理接近"变成了授权依据 ——
|
||||
那正是**隐蔽监控的形态**,与产品定位直接冲突。
|
||||
|
||||
所以三条底线:
|
||||
|
||||
- ✅ 局域网功能**可以做**,但**授权关系仍由配对码 / 账号归属决定**
|
||||
- ✅ 局域网只降低**流量与延迟**,**不降低授权门槛**
|
||||
- ❌ **不做**"同网段自动发现并自动进入设备列表"
|
||||
|
||||
### 五项功能与实现
|
||||
|
||||
**① 局域网直连(LAN 优先)** — 收益最大,**先做**
|
||||
|
||||
- 有内网地址就走内网,公网地址作备用
|
||||
- **收益**:延迟从几十 ms 降到几 ms;**1 秒一刷不再担心烧流量**
|
||||
(对水墨屏常驻看板这项最有感)
|
||||
- **实现**:内网 endpoint 随 `agent:hello` 的 `capabilities` 上报(一层对象,已支持);
|
||||
查看端连接时**先试内网、超时再回公网**
|
||||
- **切换必须无感且不丢帧** —— `seq` 去重与补传机制已具备,可直接承接
|
||||
|
||||
**② 局域网高速采样** — 顺带就有
|
||||
|
||||
- 公网默认 10 秒节流(水墨屏),**局域网允许 1 秒**(或用户选)
|
||||
- 只是配置下发的一个分支:`capabilities.lan === true` → 下发更短间隔
|
||||
- **不需要任何新协议**
|
||||
|
||||
**③ 局域网大文件传输** — 即第一节的 A 方案,局域网下无需任何中继
|
||||
|
||||
**④ 断公网可用(本地兜底)** — 最后做,需单独评估
|
||||
|
||||
- 公网断了,局域网内仍可看**最近历史**
|
||||
- 实现:采集端本地保留**有界环形缓冲**(如最近 2 小时原始帧),
|
||||
查看端断网时直连拉取
|
||||
- ⚠️ 代价:采集端要落盘。必须**有界 + 配额 + 可一键清空**,且只存指标
|
||||
|
||||
**⑤ 链路自检:让用户看得见走了哪条路**
|
||||
|
||||
- 显示「当前走内网 / 公网」、内网 IP、估计带宽
|
||||
- **没有它,①②的效果就无法被验证** —— 与本项目"可验证"的纪律一致
|
||||
- 复用已有诊断面板(自检页那一套)
|
||||
|
||||
### 实现顺序
|
||||
|
||||
1. **① + ②** —— 收益最大、改动最小
|
||||
2. **⑤** —— 紧接着,否则①②无法验证
|
||||
3. **③**(A 方案)—— 独立特性,可并行
|
||||
4. **④** —— 涉及采集端落盘,最后评估
|
||||
|
||||
### 明确不做(附理由)
|
||||
|
||||
| 不做 | 理由 |
|
||||
|---|---|
|
||||
| 同网段自动发现 + 免授权 | **合规红线**(见上) |
|
||||
| mDNS / SSDP 广播发现 | 跨平台差异大,且主动暴露"我在这里";服务端交换地址已足够 |
|
||||
| 局域网内绕过服务端建会话 | 会话建立仍需服务端裁决授权;局域网只承载**数据面** |
|
||||
| 默认开启 TURN / 中继 | 会烧流量包,必须用户显式选择 |
|
||||
|
||||
---
|
||||
|
||||
## 三、风险清单
|
||||
|
||||
| 风险 | 影响 | 处置 |
|
||||
|---|---|---|
|
||||
| 中继吃光流量包 | 服务不可用 | **默认禁用**;开启时限速 + 配额 + 用户确认 |
|
||||
| 内置 HTTP 服务被同网段扫描 | 信息泄露 | 一次性 token + **只绑内网** + 传完即关 |
|
||||
| 采集端落盘撑爆手机存储 | 应用不可用 | 有界环形缓冲 + 配额 + 一键清空 |
|
||||
| `flutter_webrtc` 增重 | 安装包变大 | **先算实际增重再定**;优先 A 方案 |
|
||||
| 局域网误判(公司网/同名网段) | 连到别人的设备 | 校验 `device_id` + 令牌,**不能只凭 IP** |
|
||||
|
||||
---
|
||||
|
||||
## 四、给决策的两句话
|
||||
|
||||
1. **A 方案(同网段直连)几乎零代价、零风险,可以先落地** —— 它同时解决了
|
||||
"不落服务端""端到端""大文件不限"三个诉求中最实在的部分。
|
||||
2. **跨公网的大文件传输要不要做,取决于一个取舍**:要么接受"直连失败就传不了",
|
||||
要么接受"用中继、烧流量包"。**这个取舍得由人拍板,不该由代码默默决定。**
|
||||
Reference in new issue
Block a user