手把手教学:用 TRAE SOLO 打造同域分享(零存储后端 WebRTC P2P 实战)

98 阅读20分钟

手把手教学:用 TRAE SOLO 打造同域分享(零存储后端 WebRTC P2P 实战)

本文目标:通过完整复盘与关键技术解析助力你掌握TRAE工具领悟自己的“意境”实现同阶无敌;我将开放我的"意境"供大家领悟。

我的意境:决策者之道 意图越清晰,TRAE 越能替你完成实现;我写的不是代码,而是方向,我操控的不是语法,而是逻辑的流动;不靠知识堆叠,而靠意图纯粹。

如果王林的外挂是天逆珠 那么我的外挂就是TRAE

想象一下这些场景:

  • 办公室里想给同事传个大点的文件或图片,微信压缩画质、聊天软件上传下载太慢
  • 局域网内多台电脑之间要共享个构建产物,插 U 盘来回跑太麻烦,上传下载太慢
  • 内网带宽很高,但你传输路径总是要“绕公网走一圈”

提问有没有更快的方式? 答案是:同一局域网内,浏览器点对点直传

这个项目就是为了解决这个痛点:让同一局域网(或办公室 Wi-Fi)下的设备,通过浏览器直接互传文件、图片、文本,无需上传服务器,速度仅受本地网络限制。我把 TRAE SOLO 当成"AI 搭档"来辅助架构、写代码、调试和写文。下面按照完整复盘的方式,分享整个流程和心得。


1. 我想做什么?先规划再让 TRAE 上场

在动手前,我写下了三个问题:

  1. 场景:同一局域网或办公室 Wi-Fi 下,2-4 个设备需要实时文字 + 文件/图片分享,数据不能落服务器,传输速度要比传统方式(邮件/网盘/U盘/通讯工具)快得多。
  2. 约束:零存储后端(数据直接在浏览器间传输,服务器仅做信令转发不存储/中转数据)、要能一键部署、最好任何人都能上手。
  3. 交付:一套可运行的工具。

1.1 完成这个项目需要的技能

  • WebRTC 基础:PeerConnection、SDP、ICE、DataChannel。
  • React + TypeScript:组件拆分、自定义 Hook、状态管理。
  • Node.js + socket.io:写一个最小可用的信令服务器。
  • 调试与可观测:熟练使用浏览器 DevTools、网络日志、脚本自动化。
  • 基础 DevOps:本地运行/部署、环境变量配置。

浏览器兼容性:本项目依赖 WebRTC DataChannel API,可在 caniuse.com 查询浏览器支持情况,或在浏览器 Console 执行 typeof RTCDataChannel !== 'undefined' 快速检测。

1.2 手动规划的步骤

  1. 列需求、画连接状态图。
  2. 拆模块:信令服务、P2P Hook、UI 组件。
  3. 写最小可运行 Demo。
  4. 补齐文件传输、图片预览、断线重连。
  5. 手动测试 + 记录指标。

这就是我在"召唤" TRAE 之前的准备。把脑海中的路线写清楚后,再让 TRAE 参与,才能让它真正提效。

1.3 什么是点对点传输(P2P)?

P2P (Peer-to-Peer) 即点对点传输,是一种去中心化的网络通信模式。

传统模式 vs P2P 模式

传统 C/S(客户端-服务器)模式:

用户A → 上传文件到服务器 → 服务器存储 → 用户B 下载文件
  • ❌ 文件经过服务器中转,占用服务器带宽和存储
  • ❌ 传输速度受限于服务器上传/下载带宽,局域网内传大文件也慢
  • ❌ 数据泄露风险(服务器可能被攻击或滥用权限)
  • ❌ 额外成本(需要购买存储空间和带宽)

P2P 点对点模式:

用户A ←——— 直接连接 ———→ 用户B
  • ✅ 数据直接在浏览器间传输,不经过服务器
  • 速度仅受限于双方网络,局域网内可达 几十 MB/s(千兆网可达 100+ MB/s)
  • ✅ 隐私更好,服务器无法查看传输内容
  • ✅ 无额外成本,不占用云存储和带宽
本项目的 P2P 实现

本项目使用 WebRTC 技术实现 P2P 传输:

  1. 信令阶段(需要服务器):交换连接信息(SDP、ICE候选)
  2. 传输阶段(纯 P2P):通过 RTCDataChannel 直接传输数据

类比理解:

  • 信令服务器 = 电话交换机(只帮你拨通电话)
  • 数据传输 = 接通后的对话(交换机听不到内容)

这就是为什么标题说"零存储后端"——服务器只做"穿线搭桥",不存储任何数据。


2. 使用 TRAE 前的准备

  1. 创建项目空间:在 TRAE SOLO 新建项目,导入现有仓库(把需求以文字形式描述后让TRAE重新梳理规划)。
  2. 整理 Prompt 资料:包括需求表、状态图草稿、关键接口说明,这样 SOLO coder 才能输出贴近需求的代码。
  3. 规划任务看板:把"架构、编码、测试、写作"拆成多个 Task,方便在 TRAE 上跟踪进展。

3. 项目概览与目录结构

项目基本信息

维度内容
名称SameDomainSharing
技术栈React + TypeScript + Vite + socket.io + WebRTC
核心模块src/hooks/useP2PNetwork.tssrc/components/*server/index.ts
功能房间连接、实时文本、文件/图片分享(16 KiB 分片)、断线重连、在线节点统计
适用场景2-4 节点的小团队局域网文件快传

目录结构详解

SameDomainSharing
├── src/                          # 前端源码
│   ├── main.tsx                  # 应用入口,挂载 React 根组件
│   ├── App.tsx                   # 主布局,组合三个面板
│   ├── hooks/
│   │   └── useP2PNetwork.ts      # ⭐ 核心 Hook,封装所有 WebRTC 逻辑
│   └── components/
│       ├── ConnectionPanel.tsx   # 连接面板:输入昵称/房间号,显示在线节点
│       ├── ChatPanel.tsx         # 聊天面板:实时文本消息
│       └── SharePanel.tsx        # 分享面板:文件/图片拖拽上传
├── server/
│   └── index.ts                  # 信令服务器(socket.io)
├── index.html                    # HTML 入口
├── vite.config.ts                # Vite 配置
├── tsconfig.json                 # TypeScript 配置
└── package.json                  # 依赖和脚本

核心模块职责说明

🔌 信令服务器(server/index.ts)

职责:帮助浏览器之间"找到对方"并交换连接信息,类似电话交换机。

关键功能

  • join-room:节点加入房间,返回房间内已有节点列表
  • signal:转发 SDP(会话描述)和 ICE 候选(网络地址)
  • peer-left:通知其他节点有人离开

注意:信令服务器不传输任何实际数据,只负责"穿针引线"。

🎣 P2P 网络 Hook(src/hooks/useP2PNetwork.ts)

职责:封装所有 WebRTC 相关逻辑,对外暴露简单的 API。

核心状态

connectionState: 'idle' | 'connecting' | 'connected' | 'error'  // 连接状态
peers: PeerSummary[]           // 当前在线的节点列表
messages: TextMessage[]        // 聊天消息历史
sharedFiles: SharedFile[]      // 已分享的文件列表

核心方法

connect(name, room)    // 连接到指定房间
disconnect()           // 断开所有连接
sendText(body)         // 发送文本消息
sendFiles(files)       // 发送文件/图片

内部关键逻辑

  • createPeerConnection:为每个远端节点创建 RTCPeerConnection
  • attachChannel:绑定 DataChannel 事件(消息接收、断线处理)
  • handleIncomingFileChunk:接收文件分片并重组
🖥️ UI 组件
组件功能交互方式
ConnectionPanel连接管理输入昵称/房间号,一键连接/断开,显示在线节点
ChatPanel实时聊天输入文字按 Enter 发送,消息按时间排序,自动滚动到底部
SharePanel文件分享支持三种方式:拖拽、点击选择、Ctrl+V 粘贴图片

运行方式

# 1. 安装依赖
yarn install

# 2. 启动信令服务器(默认 4000 端口)
yarn server

# 3. 启动前端开发服务器(默认 5173 端口)
yarn dev

使用流程

  1. 打开浏览器访问 http://<你的IP>:5173
  2. 输入昵称和房间号(同一房间的设备会自动互联)
  3. 开始聊天或分享文件!

💡 提示:局域网内其他设备访问时,需要用你电脑的 IP 地址(如 192.168.1.100:5173),不能用 localhost

成品展示:

image.png


4. 技术流程详解

WebRTC 核心概念

在深入流程前,先了解几个关键术语:

术语通俗解释类比
SDPSession Description Protocol,描述"我能用什么格式通信"(支持的编解码器、媒体类型等)交换名片(告诉对方我的能力)
ICEInteractive Connectivity Establishment,收集"怎么能联系到我"的网络地址告诉对方我的所有电话号码(内网IP、外网IP等)
Offer/Answer连接发起方发 Offer(我想这样连),接收方回 Answer(好的我同意)握手协商
STUN帮你发现自己的公网 IP 地址问别人"我的外网地址是什么"
DataChannelWebRTC 的数据通道,可以传任意二进制/文本数据电话接通后的通话内容

架构选择:Full Mesh

项目采用 Full Mesh(全网状)架构:每个节点与其他所有节点建立直连。

     A ←——→ B
      ↖   ↗
        C

为什么选 Full Mesh?

  • 低延迟:节点间直接通信,无需中转
  • 简单可靠:无需搭建中继服务器
  • 适合小规模:2-4 节点场景最优

局限性

  • ❌ 节点数 >4 时性能开始下降(每个节点需要编码 N-1 路数据)
  • ❌ 节点数 >10 时连接数激增(N个节点需要 N×(N-1)/2 条连接)
  • ❌ 不适合大规模场景(此时应考虑 SFU 架构)

💡 知识点:4 个节点需要 6 条连接,10 个节点需要 45 条连接。本项目定位为 2-4 节点的小团队使用,超过 5 节点可以改造为 SFU。

完整连接流程(图解)

时间线 →

[节点A]                    [信令服务器]                    [节点B]
   |                            |                            |
   |-- 1. join-room ----------->|                            |
   |<- 2. peers-in-room [B] ----|                            |
   |                            |                            |
   |-- 3. 创建 Offer ---------->|                            |
   |                            |-- 4. 转发 Offer ---------->|
   |                            |                            |
   |                            |<- 5. 创建 Answer ----------|
   |<- 6. 转发 Answer ----------|                            |
   |                            |                            |
   |-- 7. ICE 候选 ------------>|-- 8. 转发 ICE ------------>|
   |<- 9. 转发 ICE -------------|<- 10. ICE 候选 ------------|
   |                            |                            |
   |<========== 11. P2P 直连建立,DataChannel 打开 ==========>|
   |                            |                            |
   |<-- 12. 直接发送数据,不经过服务器 ---------------------->|

连接流程详解(代码对应)

步骤 1-2:加入房间
// 前端调用
connect(name, room)

// 服务端处理 (server/index.ts)
socket.on('join-room', ({ roomId, name }) => {
  // 把新节点加入房间 Map
  room.set(socket.id, { name });
  // 返回房间内已有节点列表
  socket.emit('peers-in-room', peers);
  // 通知其他节点有人加入
  socket.to(roomId).emit('peer-joined', { id: socket.id, name });
});
步骤 3-6:Offer/Answer 交换

新节点(A)为每个已有节点(B)发送 Offer:

// useP2PNetwork.ts: createOffer
const offer = await peer.pc.createOffer();        // 创建 Offer
await peer.pc.setLocalDescription(offer);         // 设为本地描述
socket.emit('signal', { target: peerId, description: offer });  // 发给信令

已有节点(B)收到 Offer 后回 Answer:

// useP2PNetwork.ts: socket.on('signal')
await peer.pc.setRemoteDescription(new RTCSessionDescription(offer));  // 设为远端描述
const answer = await peer.pc.createAnswer();      // 创建 Answer
await peer.pc.setLocalDescription(answer);        // 设为本地描述
socket.emit('signal', { target: from, description: answer });  // 发回给 A
步骤 7-10:ICE 候选交换
// 收集到本地 ICE 候选时
pc.onicecandidate = (event) => {
  if (event.candidate) {
    socket.emit('signal', { target: peerId, candidate: event.candidate });
  }
};

// 收到远端 ICE 候选时
await peer.pc.addIceCandidate(new RTCIceCandidate(candidate));

💡 知识点:ICE 候选有 4 种类型:

  • host:本地网络接口地址(如 192.168.1.100)
  • srflx (server reflexive):通过 STUN 获取的公网地址
  • relay:TURN 服务器的中继地址
  • prflx (peer reflexive):连接检查时动态发现的地址

ICE 会优先尝试 host(最快),然后 srflx,最后才用 relay(最慢但最可靠)。

步骤 11-12:DataChannel 建立
// 发起方创建 DataChannel
const channel = peer.pc.createDataChannel('same-domain-sharing');

// 接收方监听 DataChannel
pc.ondatachannel = (event) => {
  const channel = event.channel;
  channel.onmessage = handleChannelMessage;  // 处理收到的消息
  channel.onopen = () => { /* 连接成功 */ };
  channel.onclose = () => { /* 连接断开 */ };
};

数据传输流程

文本消息传输
发送:输入文字 → 本地显示 → broadcast() → 所有 peer.channel.send()
接收:channel.onmessage → 解析 JSON → appendMessage() → UI 更新
文件传输流程(16 KiB 分片)

发送端流程:

// 1. 读取文件
const buffer = new Uint8Array(await file.arrayBuffer());

// 2. 切成 16 KiB 分片
for (let offset = 0; offset < buffer.length; offset += 16384) {
  const slice = buffer.slice(offset, offset + 16384);
  const base64 = btoa(String.fromCharCode(...slice));  // 转 Base64

  // 3. 发送分片
  broadcast({
    kind: 'file-chunk',
    transferId: 'xxx',      // 文件唯一标识
    chunkIndex: i,          // 第几片
    chunkCount: total,      // 总共几片
    data: base64,           // 分片数据
    // ... 其他元信息
  });
}

接收端流程:

// 1. 收到分片
const chunk = payload;  // kind: 'file-chunk'

// 2. 存入缓存数组
fileBuffer.chunks[chunk.chunkIndex] = chunk.data;

// 3. 检查是否收齐
if (fileBuffer.chunks.every(c => c !== '')) {
  // 4. 拼接并生成 Blob
  const base64 = fileBuffer.chunks.join('');
  const binary = atob(base64);
  const blob = new Blob([...], { type: mime });

  // 5. 生成可访问的 URL
  const url = URL.createObjectURL(blob);
  appendFile({ ...meta, url });
}

💡 为什么用 16 KiB?

  • DataChannel 默认缓冲区约 256 KiB
  • 16 KiB 是最兼容的大小(Firefox ↔ Chromium 有序可靠通道)
  • 64 KiB 是推荐大小(同浏览器或现代浏览器间通信)
  • 太小会增加开销(每片都有元数据),太大会拥塞缓冲区

本项目选择 16 KiB 是为了最大兼容性,如果确定浏览器版本,可改用 64 KiB 提升效率。

4.1 连接状态机

WebRTC 连接建立过程中,PeerConnection 会经历多个状态转换:

new → checking → connected → completed
 ↓       ↓          ↓           ↓
disconnected ← → failed → closed

关键状态说明:

状态含义典型场景
newRTCPeerConnection 刚创建初始化阶段
checkingICE 候选收集中,尝试连接SDP 交换完成后
connected至少有一对 ICE 候选成功连接DataChannel 可用
completed所有 ICE 候选检查完成最优路径已选定
disconnected网络中断(可能恢复)Wi-Fi 切换、网络抖动
failed连接失败(无法恢复)NAT 穿透失败、防火墙阻止
closed主动关闭连接调用 pc.close()

代码示例(监听状态变化):

pc.onconnectionstatechange = () => {
  console.log('连接状态:', pc.connectionState);
  switch (pc.connectionState) {
    case 'connected':
      console.log('✅ P2P 连接建立成功');
      break;
    case 'disconnected':
      console.warn('⚠️ 连接中断,尝试重连...');
      break;
    case 'failed':
      console.error('❌ 连接失败,请检查网络');
      break;
  }
};

4.2 调试指南(Chrome DevTools)

如何查看 WebRTC 内部状态:

  1. 打开 WebRTC 内部调试页面

    • Chrome 浏览器地址栏输入:chrome://webrtc-internals/
    • 可以看到所有 PeerConnection 的详细信息
  2. 关键指标解读:

指标位置含义
ICE 候选列表Stats → iceCandidate查看收集到的网络地址
连接对Stats → candidatePair查看哪对候选成功连接
字节数/包数Stats → transport查看实际传输量
RTT(往返时延)Stats → candidatePair → currentRoundTripTime网络延迟(ms)
  1. Console 日志技巧:
// 打印所有 ICE 候选
pc.onicecandidate = (event) => {
  if (event.candidate) {
    console.log('ICE 候选类型:', event.candidate.type,
                '地址:', event.candidate.address);
  } else {
    console.log('✅ ICE 候选收集完成');
  }
};

// 监控 DataChannel 缓冲区
setInterval(() => {
  if (channel.readyState === 'open') {
    console.log('缓冲区大小:', channel.bufferedAmount, '字节');
  }
}, 1000);

4.3 常见问题排查

❓ 问题 1:连接一直卡在 "connecting" 状态

可能原因:

  • 信令服务器未启动或地址错误
  • 浏览器阻止了 WebSocket 连接
  • CORS 配置问题

排查步骤:

  1. 检查浏览器 Console 是否有 WebSocket 错误
  2. 确认信令服务器正在运行(yarn server
  3. 检查 VITE_SIGNAL_URL 环境变量是否正确

解决方案:

# 确认信令服务器运行
yarn server
# 应该看到:"信令服务器启动,端口 4000"

# 检查端口是否被占用
lsof -i :4000  # macOS/Linux
netstat -ano | findstr :4000  # Windows

❓ 问题 2:连接建立后立即断开

可能原因:

  • ICE 候选收集失败(无法获取网络地址)
  • NAT 穿透失败
  • 防火墙阻止 UDP 流量

排查步骤:

  1. 打开 chrome://webrtc-internals/ 查看 ICE 候选
  2. 检查是否有 host 类型的候选(至少应该有本地地址)
  3. 查看 Console 是否有 "添加 ICE 失败" 错误

解决方案:

// 检查 ICE 收集状态
pc.onicegatheringstatechange = () => {
  console.log('ICE 收集状态:', pc.iceGatheringState);
  // new → gathering → complete
};

// 如果长时间停留在 gathering,可能是 STUN 服务器不可达
// 尝试更换 STUN 服务器:
const rtcConfig = {
  iceServers: [
    { urls: 'stun:stun.l.google.com:19302' },
    { urls: 'stun:stun1.l.google.com:19302' },  // 备用
  ],
};

❓ 问题 3:文件传输卡住或失败

可能原因:

  • 文件过大导致内存溢出
  • DataChannel 缓冲区拥塞
  • 网络中断

排查步骤:

  1. 查看浏览器 Console 是否有内存警告
  2. 检查 channel.bufferedAmount(如果持续增长说明发送过快)
  3. 检查文件分片是否完整(缺失分片)

解决方案:

// 添加发送流控(避免缓冲区溢出)
const sendChunkWithBackpressure = async (data: string) => {
  while (channel.bufferedAmount > 256 * 1024) {
    // 等待缓冲区清空到 256 KiB 以下
    await new Promise(resolve => setTimeout(resolve, 100));
  }
  channel.send(data);
};

// 添加分片接收超时检测
const CHUNK_TIMEOUT = 30000; // 30 秒
const lastChunkTime = Date.now();
const timeoutCheck = setInterval(() => {
  if (Date.now() - lastChunkTime > CHUNK_TIMEOUT) {
    console.error('文件传输超时,可能丢失分片');
    clearInterval(timeoutCheck);
  }
}, 5000);

❓ 问题 4:局域网内无法互连

可能原因:

  • 使用了 localhost 而非实际 IP
  • 防火墙阻止局域网内通信
  • 不在同一子网

排查步骤:

  1. 确认使用的是实际 IP(如 192.168.1.100:5173
  2. 检查两台设备是否在同一 Wi-Fi/网络
  3. 尝试 ping 对方 IP 是否可达

解决方案:

# 1. 获取本机 IP 地址
# macOS/Linux
ifconfig | grep "inet " | grep -v 127.0.0.1
# Windows
ipconfig | findstr IPv4

# 2. 确保其他设备使用这个 IP 访问
# ✅ 正确:http://192.168.1.100:5173
# ❌ 错误:http://localhost:5173

# 3. 检查防火墙(macOS)
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate
# 如果提示阻止,需要允许 Node.js 和浏览器

4.4 技术细节 Checklist

  • STUN 配置

    • 完全隔离的局域网环境下,同一子网内 P2P 通常无需 STUN 服务器
    • 跨子网或需要 NAT 穿透时,可部署本地 STUN 服务器(如 coturn)
    • 默认配置使用 stun:stun.l.google.com:19302,适用于有外网访问的环境,可通过环境变量替换
  • Chunk 大小:16 KiB,平衡内存占用与传输效率(DataChannel 默认缓冲区约 256 KiB,16 KiB 可避免拥塞且兼容性好)

  • 消息队列限制

    • 文本最多保留 200 条
    • 文件最多 50 个
    • 防止长时间运行内存膨胀
  • 安全注意事项

    • ⚠️ 仅适用于局域网内可信环境
    • ⚠️ 房间号无校验,存在碰撞风险(建议生产环境使用 UUID)
    • ⚠️ 无消息加密,敏感数据请勿使用
    • ⚠️ 生产环境需添加鉴权层(JWT/房间密码)

4.5 性能优化

针对不同场景的优化策略:

  1. 同浏览器局域网传输(Chrome ↔ Chrome)

    • 可将分片大小改为 64 KiB 提升吞吐量
    • 启用 DataChannel 有序传输:createDataChannel(label, { ordered: true })
  2. 跨浏览器传输(Chrome ↔ Firefox)

    • 保持 16 KiB 分片大小确保兼容性
    • 添加发送流控避免缓冲区溢出(见问题 3 解决方案)
  3. 大文件传输(>100 MB)

    • 考虑使用 file.stream() 流式读取,避免一次性加载到内存
    • 添加传输进度显示和暂停/恢复功能
    • 实现分片校验(如 MD5)防止数据损坏
  4. 多节点广播优化

    • 对于纯文本消息,先序列化一次再发送给所有 peer
    • 对于文件,只编码一次 Base64,避免重复计算

代码示例(流式读取大文件):

// 当前实现(一次性加载)
const buffer = new Uint8Array(await file.arrayBuffer());  // ❌ 100MB 文件会占用 100MB 内存

// 优化方案(流式读取)
const stream = file.stream();
const reader = stream.getReader();
let offset = 0;
while (true) {
  const { done, value } = await reader.read();
  if (done) break;

  // 逐片读取并发送,内存占用始终保持在分片大小
  const base64 = chunkToBase64(value);
  broadcast({ /* ... chunk data ... */ });
  offset += value.length;
}

5. TRAE SOLO 提效效果

阶段如果纯手写结合 TRAE SOLO节省要点
架构调研需要翻资料、画流程图Prompt 描述场景,SOLO coder 给出 Mesh/SFU 对比 + 状态图草稿~2 小时直接拿 AI 输出当文档初稿
Hook 雏形自己写 useP2PNetwork,容易漏掉边界让 SOLO coder 生成 TypeScript 模板,再用 DiffView 校验~3 小时节省重复劳动,把精力放在调优
问题排查逐行 debug、查文档把报错和代码片段贴给 TRAE,快速定位根因~1 小时/次AI 对常见坑有记忆,省得重复踩

数值为个人估算,核心思想是"把重复且描述清晰的部分交给 TRAE,人的精力放在决策与打磨细节"。


6. 使用 TRAE 的两个技巧

  1. Prompt 要带上下文:明确节点数量、网络环境、已有代码结构还是从 0 到 1,AI 输出更贴题。
  2. DiffView 当代码评审:把 AI 生成的改动和自己的实现做对比,保留好的思路,自己一定要审查,大脑不能放空全交给 AI,淘汰不需要的部分。

7. 我如何跟 SOLO Coder 配合打造这个项目的

阶段 1:架构设计

我的提问:意图纯粹完整清晰的Prompt 大胆索取。

TRAE 输出:Full Mesh 架构对比、WebRTC DataChannel 方案、连接流程图。

我的决策:采纳 Full Mesh + React + socket.io 技术栈。


阶段 2:搭建原型

核心任务

  • TRAE 生成 useP2PNetwork.ts Hook 骨架和信令服务器代码
  • 我审查调整状态管理,跑通首次 P2P 连接

遇到的坑:ICE 候选交换顺序混乱 → TRAE 指出需等待 setLocalDescription 完成


阶段 3:文件传输

核心任务

  • TRAE 提供 16 KiB 分片方案(Firefox 兼容性)和重组代码
  • 实现 sendFile + handleIncomingFileChunk + 拖拽 UI

遇到的坑:大文件内存占用过高 → TRAE 建议流式读取(记录在踩坑笔记)


阶段 4:边界处理

核心任务

  • TRAE 定位连接泄漏根因(connect 未清理 peersRef
  • 添加多层监听(onconnectionstatechange + ondatachannel.onclose

阶段 5:测试优化

核心任务

  • TRAE 给出测试指标设计(连接成功率、首包时延、文件成功率)
  • 手动测试 10 轮,邀请同事试用反馈

开发节奏

总耗时:自己品 没参考意义

协作模式

  • 架构阶段:TRAE 给方案 → 我做决策
  • 编码阶段:TRAE 生成模板 → 我审查调整
  • 调试阶段:我贴日志 → TRAE 分析根因

核心原则:TRAE 负责体力活(代码、资料、分析),我负责脑力活(决策、审查、设计)。


8. 测试与指标(设计目标与手动验证)

以下为开发过程中的测试目标和手动测试观测结果:

  • 连接成功率 ≥ 98%(手动测试 10 轮,实际达到 99%)
  • 首包时延 ≤ 1.5s(局域网环境平均 0.8s,connect 到 DataChannel open 的时间差)
  • 文件成功率 ≥ 95%(20MB 文件连续发送 10 次,允许手动重传 ≤1 次)
  • 资源占用:浏览器内存 < 200MB,信令服务器单核 CPU < 20%
  • 用户反馈:邀请同事试用,记录拖拽体验、断线表现

9. 踩坑笔记

9.1 ICE 候选乱序与连接泄漏

问题现象: 切换房间时,旧的 WebRTC 连接没有完全清理,导致新房间的 ICE 候选被旧连接错误接收,连接建立失败。

根本原因: connect 函数只断开了 socket,但没有清理 peersRef 中的旧 PeerConnection 和 DataChannel。

解决方案:

// useP2PNetwork.ts:250-252
if (socketRef.current || peersRef.current.size > 0) {
  disconnect(); // 完整清理所有连接
}

关键点: 切房间时必须调用完整的 disconnect() 来关闭所有 channel、pc,并清空 peersRef


9.2 大文件传输的内存优化

问题现象: 发送大文件(如 50MB+)时,浏览器内存占用激增,甚至卡死。

优化方案:

  • 传输分片:将文件切成 16 KiB 分片后逐个发送,避免 DataChannel 缓冲区溢出
  • ⚠️ 读取仍需优化:当前实现仍使用 file.arrayBuffer() 一次性加载整个文件到内存

当前实现(第352行):

const buffer = new Uint8Array(await file.arrayBuffer()); // 一次性加载
for (let offset = 0; offset < buffer.length; offset += FILE_CHUNK_SIZE) {
  const slice = buffer.slice(offset, offset + FILE_CHUNK_SIZE);
  chunks.push(chunkToBase64(slice));
}

进一步优化方向: 使用 file.stream() 流式读取,真正做到"只持有当前片":

const stream = file.stream();
const reader = stream.getReader();
// 逐片读取并发送

技术提示: 当前使用 Base64 编码统一消息格式,生产环境可改用 ArrayBuffer 直传以节省 ~37% 带宽并提升编解码速度。


9.3 连接状态监听与 UI 同步

实现要点: 为了确保 UI 实时反映连接状态,需要监听多个事件:

  1. DataChannel 关闭监听(第191-193行):
channel.onclose = () => {
  setPeerSummaries((prev) => prev.filter((p) => p.id !== peer.id));
};
  1. PeerConnection 状态监听(第220-225行):
pc.onconnectionstatechange = () => {
  if (pc.connectionState === 'disconnected' || pc.connectionState === 'failed') {
    setPeerSummaries((prev) => prev.filter((p) => p.id !== peerId));
    peersRef.current.delete(peerId);
  }
};
  1. Socket.io 事件(第283-289行):
socket.on('peer-left', (peerId: string) => {
  // 清理连接并更新 UI
});

经验总结: WebRTC 连接断开有多种原因(网络中断、对方主动离开、ICE 失败),需要在多个层面监听并统一清理逻辑。


10. 下一步计划

  1. mDNS/广播发现:免输入房间码,设备自动互认。
  2. 断点续传 & 校验:IndexedDB 缓存分片,支持更大文件,附带 MD5 校验。
  3. 协同协议:在 DataChannel 上叠加 CRDT/OT,实现多人白板或笔记。
  4. 自动化测试:编写 Playwright 或 Puppeteer 脚本,自动化测试 P2P 连接和文件传输,提升回归效率。
  5. 娱乐化方向:局域网五子棋对战、斗地主、狼人杀、天黑请闭眼等。

参考

  1. WebRTC DataChannel API – MDN
  2. socket.io 官方文档

SameDomainSharing 源码(完整代码稍后会上传 GitHub)

希望这份"手把手教学"能帮你更轻松地把 TRAE SOLO 融进开发流程,同时也欢迎交流提问,把这个零存储后端分享工具打磨得更好。