- Receiver: auto-fill and join on Retrieve tab when input empty, not in room, no URL roomId, and cachedId exists (ClipboardApp + roomIdCache) - Sender: “Use cached ID” now immediately joins the room (add onUseCached + disabled to CachedIdActionButton; wire in SendTabPanel) - UI: add ThemeToggle and integrate into Header (desktop and mobile) - Styles: replace hardcoded white with design tokens in Retrieve panel (bg-card/text-card-foreground) for dark mode - Docs: update AI playbook flows and code-map
31 KiB
PrivyDrop AI Playbook — 流程(含微方案模板,中文)
本文汇总 P2P 传输与信令重连的关键流程与消息序列,并给出简明的调试要点与“微方案模板”。用于在改动前快速对齐阶段、事件与入口文件。
1)文件传输(单文件)
序列(通过 DataChannel,发送端 ↔ 接收端):
- 发送端 →
fileMetadata(id、name、size、type、fullName、folderName)。 - 接收端 →
fileRequest(确认元信息;支持 offset 续传)。 - 发送端 → 分片流(高性能双层缓冲架构):
- StreamingFileReader 使用 32MB 批次读取 + 64KB 网络块发送
- NetworkTransmitter 使用 WebRTC 原生背压控制(bufferedAmountLowThreshold)
- 发送时嵌入元数据(chunkIndex、totalChunks、fileOffset、fileId)
- 接收端 → 完整性检查与组装(严格顺序写入或内存组装,支持断点续传)。
- 接收端 →
fileReceiveComplete(成功回执,包含 receivedSize)。 - 发送端 → MessageHandler 触发 100% 进度回调,清理发送状态。
发送侧详细流程:
- FileTransferOrchestrator.sendFileMeta() → StateManager 记录文件夹文件大小
- 接收 fileRequest → FileTransferOrchestrator.handleFileRequest()
- 初始化 StreamingFileReader(支持 startOffset 续传)
- processSendQueue() 循环:
- getNextNetworkChunk() 获取 64KB 块(批次内高效切片)
- NetworkTransmitter.sendEmbeddedChunk() 背压控制发送
- ProgressTracker.updateFileProgress() 更新进度和速度
- 等待 fileReceiveComplete 确认,清理 isSending 状态
入口:
- 发送侧:
frontend/lib/fileSender.ts(兼容层)→frontend/lib/transfer/FileTransferOrchestrator.ts(主编排器) - 关键组件:StreamingFileReader(高性能读取)、NetworkTransmitter(背压发送)、StateManager(状态管理)、ProgressTracker(进度计算)
接收侧详细流程:
- MessageProcessor.handleFileMetadata() → ReceptionStateManager 记录文件元数据
- FileReceiveOrchestrator.requestFile() → 检查断点续传(getPartialFileSize)
- 初始化接收:计算期望分片数,根据文件大小选择存储方式(内存 vs 磁盘)
- 发送 fileRequest(带 offset 参数)→ 等待发送端开始传输
- handleBinaryChunkData() 循环:
- ChunkProcessor.convertToArrayBuffer() 处理多种数据格式(Blob/Uint8Array/ArrayBuffer)
- ChunkProcessor.parseEmbeddedChunkPacket() 解析嵌入元数据包格式
- ChunkProcessor.validateChunk() 验证 fileId、chunkIndex、chunkSize
- 存储分片到 chunks 数组(或通过 SequencedDiskWriter 顺序写入磁盘)
- ProgressReporter.updateFileProgress() 节流更新进度(100ms 间隔)
- 自动完成检测:checkAndAutoFinalize() 验证分片完整性
- 根据存储方式选择最终化:
- 大文件/磁盘存储:StreamingFileWriter.finalizeWrite()
- 小文件/内存存储:FileAssembler.assembleFileFromChunks()
- 发送 fileReceiveComplete 确认,包含 receivedSize 和 receivedChunks
入口:
- 发送侧:
frontend/lib/fileSender.ts(兼容层)→frontend/lib/transfer/FileTransferOrchestrator.ts(主编排器) - 接收侧:
frontend/lib/fileReceiver.ts(兼容层)→frontend/lib/receive/FileReceiveOrchestrator.ts(主编排器) - 关键组件:StreamingFileReader(高性能读取)、NetworkTransmitter(背压发送)、ChunkProcessor(格式处理)、StreamingFileWriter(磁盘写入)、FileAssembler(内存组装)
备注:
- 发送侧:双层缓冲架构(32MB 批次+64KB 网络块),WebRTC 原生背压控制,支持断点续传
- 接收侧:严格顺序写入机制(SequencedDiskWriter),支持多种数据格式转换,智能存储选择(≥1GB 文件自动磁盘存储)
- 兼容性处理:ChunkProcessor 支持 Blob/Uint8Array/ArrayBuffer 多种格式,解决 Firefox 兼容性问题
- 进度节流:ProgressReporter 使用不同频率更新(文件 100ms,文件夹 200ms),避免 UI 过载
- 断点续传:通过 getPartialFileSize() 检查本地部分文件,fileRequest.offset 参数指定续传位置
- 调试支持:ReceptionConfig 提供详细的分片日志和进度日志,便于问题排查
2)文件传输(文件夹)
序列(对文件逐个进行单文件流程):
- 发送端 → 发送文件夹内全部文件的
fileMetadata。 - 接收端 →
folderRequest(确认开始批量传输)。 - 每个文件:按“单文件流程”执行,但单个文件完成时不标记全局 100%。
- 接收端 → 所有文件完成后发送
folderReceiveComplete。 - 发送端 → 将文件夹整体进度标记为 100%(触发最终回调)。
3)信令与重连(Socket.IO)
高层序列:
- 客户端 → REST:创建或获取
roomId(backend/src/routes/api.ts)。 - 客户端 → Socket.IO:
join房间(后端校验并绑定 socket 到房间)。 - 在房间内进行在线状态与重连协作:
- 发起方 →
initiator-online(上线/就绪,通知对端可重建连接)。 - 接收方 →
recipient-ready(表示就绪;发起方可发起 offer)。
- 发起方 →
- WebRTC 协商转发:
offer→ 后端 → 转发给对端。answer→ 后端 → 转发给对端。ice-candidate→ 后端 → 转发给对端。
重连机制细节(移动端网络切换支持):
- 双重断开检测:Socket.IO 断开触发
disconnect事件 → 标记isSocketDisconnected = true;P2P 连接断开触发disconnected状态 → 标记isPeerDisconnected = true,自动调用cleanupExistingConnection()清理资源 - 重连触发条件:仅当 socket 和 P2P 都断开时才启动重连 →
attemptReconnection(),防止重复重连;使用reconnectionInProgress标志防止并发重连 - 状态恢复机制:重连时调用
joinRoom(roomId, isInitiator, sendInitiatorOnline)恢复状态,发送方自动发送initiator-online信号,接收方响应recipient-ready - ICE 候选者队列机制:连接未就绪时缓存候选者到
iceCandidatesQueueMap,连接就绪后批量处理;支持候选者失效时的重新入队和连接状态验证 - 唤醒锁管理:连接建立时通过
WakeLockManager请求屏幕唤醒锁,连接断开时释放,优化移动端传输稳定性 - 优雅断开跟踪:
gracefullyDisconnectedPeersSet 跟踪正常断开的 peer,发送重试时跳过这些 peer,避免不必要的重试 - 数据通道发送重试:5 次重试机制,间隔从 100ms 递增到 1000ms,支持
gracefullyDisconnectedPeers检测跳过重试
后端信令与房间管理:
Socket.IO 事件处理流程:
- join 事件:IP 限流检查 → 房间存在性验证 → socket-room 绑定 → 成功响应 → 广播
ready通知新用户加入 - 重连状态同步:发送方重连时发送
initiator-online信号,接收方响应recipient-ready确认就绪状态 - 信令转发:offer/answer/ice-candidate 直接通过
socket.to(peerId).emit()转发给目标客户端,包含 from 字段标识发送者 - 断开清理:广播
peer-disconnected→ 解绑 socket-room 关系 → 空房间 15 分钟后删除
房间管理机制:
- Redis 数据结构:
room:<roomId>(Hash): 存储房间创建时间room:<roomId>:sockets(Set): 管理房间内 socket 连接socket:<socketId>(String): 存储 socket 对应的 roomId
- ID 生成策略:优先 4 位数字 ID,冲突时切换到 4 位字母数字 ID
- 幂等设计:长 ID(≥8 字符)支持重连时的房间复用
- TTL 管理:24 小时过期,活动时自动刷新
限流保护:
- 基于 Redis Sorted Set 实现 IP 限流
- 5 秒时间窗口最多允许 2 次请求
- 使用 pipeline 确保原子性操作
入口:
- 前端:
frontend/hooks/useWebRTCConnection.ts、frontend/lib/webrtc_base.ts、frontend/lib/webrtc_Initiator.ts、frontend/lib/webrtc_Recipient.ts。 - 后端:
backend/src/socket/handlers.ts(全部信令事件)、backend/src/services/room.ts、backend/src/routes/api.ts。
4)DataChannel 消息与约束(概览)
- 消息(示例命名):
fileMetadata、fileRequest、chunk、fileReceiveComplete、folderRequest、folderReceiveComplete,以及可能的流控/保活。 - 核心字段:文件/文件夹 id、索引/范围、大小、名称、可选校验信息。
- 关键约束:
- 分片大小:按浏览器/网络选择安全范围;注意通道缓冲阈值。
- 背压:检查
RTCDataChannel.bufferedAmount并按需节流。 - 完成:仅在收到
fileReceiveComplete/folderReceiveComplete后标记 100%。 - 续传:
fileRequest可设计为带 offset/range 以支持续传。
5)调试要点(凝练自历史经验)
- 下载竞争/重复计数:以
frontend/stores/fileTransferStore.ts为单一事实来源;在 Store 层提供清理 API(如clearSendProgress、clearReceiveProgress),避免组件本地删除对象导致重复计数。 - 接收方重连与房间状态:正确的状态重置;UI 严格来源于 Store;离开/重进需清理相关状态;遵循
initiator-online/recipient-ready的时序再发起 offer;重连后校验房间成员关系。 - 缓存 roomId 的重连:若存在缓存
roomId,确保依赖在线状态同步(initiator-online/recipient-ready)触发重新协商;后端需保证 socket↔room 映射在断开/重连路径上被正确清理与恢复。 - 多次传输计数:避免过度“去重”掩盖真实的二次下载;依赖正确的状态清理。
- 数据流原则:单向数据流(Store → Hooks → Components);Hooks 做适配,组件只消费不修改。
- 实用调试策略:
- 为连接状态变化与 Store 更新添加结构化日志;遇到时序/竞态可用
setTimeout(..., 0)调整更新顺序 - DataChannel 发送重试机制:
sendToPeer()支持 5 次重试,间隔 100ms→1000ms 递增;优雅断开的 peer 跳过重试 - WebRTC 数据类型兼容性:支持
ArrayBuffer/Blob/Uint8Array/TypedArray多种格式,解决 Firefox 兼容性问题 - 连接状态监控:
connectionState变化时触发相应的处理逻辑(connected/disconnected/failed/closed) - 背压控制:DataChannel 设置
bufferedAmountLowThreshold = 256KB,发送时检查bufferedAmount状态
- 为连接状态变化与 Store 更新添加结构化日志;遇到时序/竞态可用
6)前端组件系统与业务中枢协作流程
组件架构层级
App Router (page.tsx/layout.tsx)
↓
HomeClient (页面布局与SEO)
↓
ClipboardApp (顶层UI协调器)
↓
SendTabPanel/RetrieveTabPanel (功能面板)
↓
业务中枢 Hooks (状态管理与业务逻辑)
↓
Core Services (webrtcService) + Store (fileTransferStore)
ClipboardApp 顶层协调器模式
核心职责:
- 集成 5 个关键业务 hooks:useWebRTCConnection、useFileTransferHandler、useRoomManager、usePageSetup、useClipboardAppMessages
- 全局拖拽事件处理:dragenter/dragleave/dragover/drop,支持多文件和文件夹树遍历
- 双标签页状态管理:发送/接收面板切换,通过 activeTab 控制
- 统一消息系统:shareMessage/retrieveMessage 4 秒自动消失机制
Hook 层级与职责分离
useWebRTCConnection(状态桥梁):
- 计算全局传输状态(isAnyFileTransferring)
- 暴露 webrtcService 方法(broadcastDataToAllPeers、requestFile、requestFolder)
- 提供连接重置方法(resetSenderConnection、resetReceiverConnection)
useFileTransferHandler(文件与内容管理):
- 文件操作:addFilesToSend(去重)、removeFileToSend
- 下载功能:handleDownloadFile(支持文件夹压缩下载)
- 关键修复:使用
useFileTransferStore.getState()获取最新状态,避免闭包问题 - 重试机制:最大 3 次重试,50ms 间隔,详细错误日志
useRoomManager(房间生命周期管理):
- 房间操作:joinRoom(支持缓存 ID 重连)、processRoomIdInput(750ms 防抖)
- 离开保护:传输中确认提示(isAnyFileTransferring 检查)
- 状态文本:动态更新房间状态文本
- 链接生成:自动生成分享链接
usePageSetup(页面初始化):
- 国际化消息加载与错误处理
- URL 参数处理:roomId 自动提取并触发加入房间(200ms 延迟确保 DOM 就绪)
- 引荐来源追踪(trackReferrer)
useClipboardAppMessages(消息管理):
- 分离式消息状态:shareMessage(发送相关)和 retrieveMessage(接收相关)
- 统一消息显示接口:putMessageInMs(message, isShareEnd, displayTimeMs)
- 自动清理机制:4 秒后自动清空消息状态
面板组件特化设计
SendTabPanel 发送面板:
- 房间 ID 双模式生成:4 位数字(后端 API 生成)和 UUID(前端 crypto API)
- 富文本编辑器集成(动态导入,SSR 禁用)
- 文件上传处理和文件列表管理
- 分享链接生成与二维码显示
RetrieveTabPanel 接收面板:
- File System Access API 集成:目录选择和直接保存到磁盘
- 富文本内容渲染(dangerouslySetInnerHTML)
- 文件请求和下载状态管理
- 保存位置选择和大文件/文件夹提示
FileListDisplay 文件列表:
- 智能文件/文件夹分组和统计显示
- 多浏览器下载策略:Chrome 自动下载,其他浏览器手动保存提示
- 下载计数统计和传输进度跟踪
- 断点续传和存储方式显示(内存/磁盘)
关键用户体验优化
- 下载状态闭包修复:
useFileTransferHandler.ts:110使用useFileTransferStore.getState()获取最新状态 - 房间 ID 输入防抖:
useRoomManager.ts:247使用 lodash debounce 750ms 延迟验证 - 传输中离开保护:
useRoomManager.ts:164,218检查isAnyFileTransferring状态并显示确认对话框 - 缓存 ID 重连:
useRoomManager.ts:91检测长 ID(≥8 字符)自动发送initiator-online - 文件夹压缩下载:
useFileTransferHandler.ts:89使用 JSZip 动态创建 ZIP 文件 - 全局拖拽优化:ClipboardApp 使用 dragCounter 防止拖拽状态误判,支持 webkitGetAsEntry 文件树遍历
- 剪贴板兼容性:useClipboardActions 支持现代 navigator.clipboard API 和 document.execCommand 降级方案
- 富文本安全处理:useRichTextToPlainText 服务端渲染安全,客户端 DOM 转换处理块级元素
- 站内导航不中断(同一标签页):依赖
frontend/stores/fileTransferStore.ts(Zustand 单例)与frontend/lib/webrtcService.ts(服务单例)。App Router 页面切换不打断传输且保留已选择/已接收内容。注意不要在路由切换副作用中调用webrtcService.leaveRoom()或重置 Store;刷新/新标签不在保证范围内。 - 切到接收端自动加入(缓存ID):当用户切换到接收端、未在房间、URL 无
roomId、输入框为空且本地存在缓存 ID 时,自动填充并直接调用加入房间以提升体验。入口:frontend/components/ClipboardApp.tsx(监听activeTab变化,读取frontend/lib/roomIdCache.ts)。 - 发送端“使用缓存ID”即刻加入:发送端在
SendTabPanel点击“使用缓存ID”后会立即调用加入房间(而非仅填充输入框)。入口:frontend/components/ClipboardApp/CachedIdActionButton.tsx(onUseCached回调)+frontend/components/ClipboardApp/SendTabPanel.tsx。 - 深色主题切换:提供单按钮 Light/Dark 切换,入口:
frontend/components/web/ThemeToggle.tsx;集成位置:frontend/components/web/Header.tsx(桌面与移动);局部样式从硬编码颜色迁移为设计令牌(例如接收面板使用bg-card text-card-foreground)。
前端组件架构特化
富文本编辑器模块:
- RichTextEditor:主编辑器组件,支持 contentEditable、图片粘贴、格式化工具、SSR 禁用
- 工具栏组件分离:BasicFormatTools(粗体/斜体/下划线)、FontTools(字体/大小/颜色)、AlignmentTools(对齐)、InsertTools(链接/图片/代码块)
- 类型安全设计:完整的 TypeScript 类型定义(FormatType、AlignmentType、FontStyleType、CustomClipboardEvent)
- 编辑器 Hooks:useEditorCommands(命令执行)、useSelection(选择管理)、useStyleManagement(样式管理)
网站页面组件设计:
- Header 响应式导航:桌面端水平导航+移动端汉堡菜单,集成 GitHub 链接和语言切换器
- Footer 国际化:动态版权年份、多语言支持链接显示,使用 languageDisplayNames 配置
- FAQSection 灵活配置:支持工具页面/独立页面切换、标题级别控制、自动 FAQ 数组生成
- 内容展示组件:HowItWorks(步骤动画+视频)、SystemDiagram(架构图)、KeyFeatures(图标+特性说明)
UI 组件库架构:
- 基于 Radix UI:Button(CVA 多变体系统)、Accordion(手风琴)、Dialog(模态对话框)、Select、DropdownMenu
- 设计系统一致性:统一的 cn 工具函数、主题色彩系统、动画过渡效果
- 组件组合模式:DialogHeader/DialogFooter/DialogTitle/DialogDescription 组合设计
- 懒加载优化:LazyLoadWrapper 使用 react-intersection-observer,支持 rootMargin 配置防止布局跳动
通用组件工具化:
- clipboard_btn:WriteClipboardButton/ReadClipboardButton 分离设计,集成 useClipboardActions hook,支持国际化消息
- TableOfContents:支持中文标题 ID 生成、滚动跟踪、层级缩进、IntersectionObserver 监听
- JsonLd SEO:多类型数据支持、suppressHydrationWarning、数组/单对象处理
- AutoPopupDialog/YouTubePlayer:业务场景封装,复用性设计
数据流模式
- 单向数据流:Store → Hooks → Components
- 状态管理集中化:所有状态通过
useFileTransferStore统一管理 - 错误处理标准化:统一的消息提示机制(putMessageInMs)
- 国际化集成:useLocale + getDictionary 提供多语言支持
7)背压与分片策略深度分析
发送侧双层缓冲架构
设计原理:
- 文件读取层:4MB 分片减少 FileReader 调用,8 个分片组成 32MB 批次
- 网络传输层:64KB 小块适配 WebRTC DataChannel 限制,避免 sendData failed 错误
- 性能优化:批次内高效切片,一次 FileReader.read()产生 512 个网络块
配置参数:
TransferConfig.FILE_CONFIG = {
CHUNK_SIZE: 4194304, // 4MB - 文件读取分片
BATCH_SIZE: 8, // 8个分片 = 32MB批次
NETWORK_CHUNK_SIZE: 65536, // 64KB - WebRTC安全发送大小
};
背压控制机制:
- DataChannel 阈值:
bufferedAmountLowThreshold = 256KB(Initiator)和512KB(NetworkTransmitter) - 最大缓冲限制:
maxBuffer = 1MB,超过时等待背压释放 - 异步等待策略:监听
bufferedamountlow事件,支持超时机制(10 秒)
嵌入元数据包格式:
[4字节长度][JSON元数据][实际数据块]
- 每个网络块都包含:chunkIndex、totalChunks、fileOffset、fileId、isLastChunk
- 接收端可独立解析,无需依赖额外状态
接收侧智能存储策略
存储选择逻辑:
ReceptionConfig.shouldSaveToDisk(fileSize, hasSaveDirectory);
- 内存存储:文件 < 1GB 且未指定保存目录
- 磁盘存储:文件 ≥ 1GB 或用户选择了保存目录
- 缓冲管理:最多缓存 100 个分片(约 6.4MB)
分片验证机制:
- 格式兼容:支持 ArrayBuffer/Blob/Uint8Array/TypedArray 多种格式
- 完整性检查:验证 fileId、chunkIndex、chunkSize 一致性
- Firefox 兼容:Blob size 检测和转换错误处理
严格顺序写入:
- SequencedDiskWriter:确保分片按序写入磁盘,支持大文件流式处理
- 断点续传:通过
getPartialFileSize()检查本地部分文件 - 自动完成检测:
checkAndAutoFinalize()验证分片完整性
性能优化细节
发送侧优化:
- 批量读取:32MB 批次减少 I/O 操作,提升大文件读取性能
- 网络适配:64KB 块平衡传输效率与浏览器兼容性
- 背压响应:利用 WebRTC 原生背压控制,避免数据丢失
接收侧优化:
- 格式转换:ChunkProcessor 统一处理多种数据格式
- 进度节流:文件 100ms、文件夹 200ms 间隔更新,避免 UI 过载
- 内存管理:小文件内存组装,大文件直接写入磁盘
错误处理:
- 发送重试:NetworkTransmitter 返回 boolean 状态,支持上层重试逻辑
- 转换容错:Blob conversion failed 时返回 null,不中断整体传输
- 超时保护:文件完成 30 秒超时,优雅关闭 5 秒超时
调试与监控
开发环境日志:
- 分片跟踪:每 100 个分片或最后分片记录详细信息
- 背压监控:缓冲区大小变化和等待时间统计
- 性能指标:传输速度、批次处理时间、格式转换耗时
生产环境优化:
- 条件日志:
ENABLE_CHUNK_LOGGING和ENABLE_PROGRESS_LOGGING开关 - 错误上报:关键错误通过
postLogToBackend发送到后端 - 性能采样:通过
performance.now()精确测量耗时
9)断点续传深度分析
断点续传核心机制
续传检测与状态恢复:
- 发送侧初始化:
StreamingFileReader constructor(file, startOffset)支持从任意偏移量开始 - 接收侧检测:
StreamingFileWriter.getPartialFileSize()通过 File System Access API 检查部分文件 - 状态同步:fileRequest 消息包含 offset 参数,通知发送方从指定位置继续传输
分片索引计算:
// 统一的分片计算逻辑
const startChunk = Math.floor(startOffset / chunkSize);
const expectedChunks = Math.ceil((fileSize - startOffset) / chunkSize);
ChunkRangeCalculator 统一计算器
设计目的:确保发送端和接收端使用完全相同的分片计算逻辑
getChunkRange(fileSize, startOffset, chunkSize) {
const startChunk = Math.floor(startOffset / chunkSize);
const endChunk = Math.floor((fileSize - 1) / chunkSize);
return { startChunk, endChunk, totalChunks: endChunk - startChunk + 1 };
}
关键方法:
getRelativeChunkIndex():绝对索引转相对索引,用于接收端数组映射isChunkIndexValid():验证分片索引是否在预期范围内calculateExpectedChunks():计算预期分片数量,与 ReceptionConfig 保持一致
接收侧续传流程
部分文件检测:
- 目录准备:
createFolderStructure()确保目标目录存在 - 文件查询:通过
getFileHandle(fileName, {create: false})检查文件是否存在 - 大小获取:
file.getFile()获取当前文件大小作为续传起点
续传决策逻辑:
// FileReceiveOrchestrator.ts
const offset = await this.streamingFileWriter.getPartialFileSize(
fileInfo.name,
fileInfo.fullName
);
if (offset === fileInfo.size) {
// 文件已完整,跳过传输
return;
}
if (offset > 0) {
// 发现部分文件,准备续传
// 发送包含 offset 的 fileRequest
}
发送侧续传响应
续传准备:
- 重置读取器:
StreamingFileReader.reset(startOffset)从新的偏移量开始 - 批次调整:
currentBatchStartOffset和totalFileOffset同步更新 - 分片索引:
startChunkIndex记录传输起始点,用于边界检测
续传日志:
const chunkRange = ChunkRangeCalculator.getChunkRange(
fileSize,
startOffset,
chunkSize
);
postLogToBackend(
`[SEND-SUMMARY] File: ${file.name}, offset: ${startOffset}, startChunk: ${chunkRange.startChunk}, endChunk: ${chunkRange.endChunk}`
);
续传的优势与限制
优势:
- 带宽节省:避免重新传输已接收的数据
- 时间效率:大文件传输中断后可快速恢复
- 用户体验:网络波动不会导致传输进度完全丢失
限制与注意点:
- 文件一致性:依赖文件内容未发生变化,续传前应验证文件大小/修改时间
- 存储位置:仅在使用 File System Access API 选择保存目录时支持
- 浏览器兼容:File System Access API 主要支持 Chrome/Edge,其他浏览器降级为内存存储
调试支持:
- 详细日志:开发环境下记录续传起点、分片范围、预期传输量
- 错误处理:文件访问失败时回退到从头开始传输
- 状态跟踪:Store 层记录续传状态和实际接收大小
10)重连与状态一致性深度分析
WebRTC 基础层重连机制
双重断开检测架构:
// webrtc_base.ts
private isSocketDisconnected = false; // Socket.IO 连接状态
private isPeerDisconnected = false; // P2P 连接状态
private gracefullyDisconnectedPeers = new Set(); // 优雅断开的 peer 列表
重连触发条件:仅当 Socket.IO 和 P2P 连接都断开时才启动重连:
// 避免重复重连:socket 断开 ≠ P2P 断开
if (
this.isSocketDisconnected &&
this.isPeerDisconnected &&
!this.reconnectionInProgress
) {
this.attemptReconnection();
}
ICE 候选者队列管理
候选者缓存策略:
- 连接未就绪时:候选者缓存到
iceCandidatesQueueMap,按 peerId 分组 - 连接就绪后:批量处理缓存的候选者,按序添加到 RTCPeerConnection
- 失效处理:候选者失效时重新入队,验证连接状态后重试
实现细节:
private iceCandidatesQueue = new Map<string, RTCIceCandidate[]>();
// 缓存候选项直到连接就绪
if (dataChannel?.readyState !== 'open') {
this.queueIceCandidate(candidate, peerId);
} else {
this.addIceCandidate(candidate, peerId);
}
数据通道发送重试机制
5 次重试策略:
async sendToPeer(data: string | ArrayBuffer, peerId: string): Promise<boolean> {
for (let attempt = 1; attempt <= 5; attempt++) {
try {
dataChannel.send(data);
return true;
} catch (error) {
if (this.gracefullyDisconnectedPeers.has(peerId)) {
return false; // 跳过已优雅断开的 peer
}
if (attempt === 5) throw error;
await new Promise(resolve => setTimeout(resolve, attempt * 100)); // 100ms→1000ms
}
}
}
重试间隔递增:100ms → 200ms → 300ms → 400ms → 500ms,最大 5 次尝试
房间管理层的重连支持
幂等性设计:
- 长 ID 重连:≥8 字符的 roomId 支持断线重连时复用房间
- 短 ID 限制:4 位数字 ID 断线后需重新生成房间,避免冲突
缓存 ID 重连优化:
// useRoomManager.ts
if (roomId.length >= 8) {
// 长ID自动发送 initiator-online 信号
this.sendInitiatorOnline();
}
状态同步序列:
- 发送方重连:
initiator-online信号通知接收方准备重建连接 - 接收方响应:
recipient-ready确认就绪状态 - WebRTC 协商:重新开始 offer/answer/ICE 候选者交换
- 传输恢复:在新的 DataChannel 上恢复文件传输
状态一致性保证机制
Store 层单一事实来源:
// fileTransferStore.ts
export const useFileTransferStore = create<TransferState>((set, get) => ({
sendProgress: new Map(),
receiveProgress: new Map(),
// 提供清理 API 避免重复计数
clearSendProgress: (fileId: string) =>
set((state) => {
const newProgress = new Map(state.sendProgress);
newProgress.delete(fileId);
return { sendProgress: newProgress };
}),
}));
连接状态机:
type ConnectionStatus = 'connecting' | 'connected' | 'disconnected' | 'failed' | 'closed';
// 状态变更时触发相应处理
connectionStateChangeHandler(status: ConnectionStatus) {
switch (status) {
case 'connected':
this.gracefullyDisconnectedPeers.clear(peerId);
this.resetReconnectionState();
break;
case 'disconnected':
case 'failed':
this.cleanupExistingConnection(peerId);
break;
}
}
移动端优化策略
唤醒锁管理:
// WakeLockManager
async requestWakeLock(): Promise<void> {
try {
this.wakeLock = await navigator.wakeLock.request('screen');
this.wakeLock.addEventListener('release', () => {
this.wakeLock = null;
});
} catch (error) {
console.warn('Wake lock request failed:', error);
}
}
网络切换适应:
- 连接检测:监听
connectionstatechange事件检测网络质量变化 - 自动重连:
connectionState: 'disconnected' | 'failed' | 'closed'时均触发重连流程(统一走 attemptReconnection) - 状态恢复:重连成功后恢复房间状态和传输进度
移动端后台/前台切换补充策略:
- socket 连接恢复自动入房:
socket.on('connect')时,若已持有roomId且(lastJoinedSocketId !== socket.id或!isInRoom),则强制重新joinRoom(roomId, isInitiator, isInitiator);发送端会自动广播initiator-online,接收端回复recipient-ready。 - 身份追踪:成功
joinRoom后记录lastJoinedSocketId = socket.id,用以检测“后台恢复时 socketId 更换”的情形。 - 门槛放宽:
attemptReconnection只要满足“roomId存在,且满足任一:P2P 断开 / socket 断开 / socketId 改变”,即可发起重连;不再强依赖“socket 与 P2P 同时断开”。
重连调试要点
关键日志点:
- 双重断开检测:记录 Socket.IO 和 P2P 断开的具体时间戳
- 候选者队列:统计缓存的 ICE 候选者数量和处理时间
- 发送重试:记录重试次数、间隔和最终结果
- 状态恢复:追踪
initiator-online→recipient-ready的时序
常见问题诊断:
- 重复重连:检查
reconnectionInProgress标志和gracefullyDisconnectedPeers集合 - 候选者失效:验证
iceConnectionState和iceGatheringState状态 - 状态不一致:确认 Store 层的进度清理和连接状态同步
11)微方案模板(用于小改动前的对齐)
标题:<简述>
背景/问题
- 要解决的用户场景或缺陷是什么?
目标与非目标
- 本次改动包含与不包含的范围?
影响文件与消息
- 代码:列出关键文件(如
frontend/lib/webrtc_base.ts、backend/src/socket/handlers.ts)。 - 协议:列出将修改的 DataChannel 消息/字段。
状态机/流程变化
- 增删改的阶段;给出简要时序或步骤。
测试与回归清单
- 单测/集成(如适用)、手测场景、性能/边界、重连。
需要更新的文档
code-map.md(如出现新的入口)flows.md(流程/消息/约束变化)- 相关架构或部署文档(如涉及)