mirror of
https://github.com/MatrixSeven/file-transfer-go.git
synced 2026-03-11 10:38:46 +08:00
9.8 KiB
9.8 KiB
大文件传输内存优化文档 V3
问题背景
原有实现在传输大文件时存在内存爆炸风险:
- 发送端:预加载整个文件的所有块到内存数组
- 接收端:在内存中累积所有接收到的块
- 合并:再次复制所有数据生成最终文件
示例:传输 1GB 文件,内存峰值可能达到 3GB+(发送端 1GB + 接收端 1GB + 合并时 1GB)
优化方案
V3 终极方案:流式写入磁盘
使用 File System Access API 直接写入磁盘,彻底解决容量限制:
- ✅ 发送端:按需读取文件块,滑动窗口内仅缓存少量块
- ✅ 接收端:直接写入磁盘,不使用 IndexedDB
- ✅ 无文件大小限制:理论上支持任意大小(受限于磁盘空间)
- ✅ 降级方案:不支持的浏览器自动回退到 Blob 下载
内存对比
| 场景 | 原实现 | V2 (IndexedDB) | V3 (磁盘流) |
|---|---|---|---|
| 传输 1GB 文件 | ~3GB 内存峰值 | ~50MB 内存 | ~20MB 内存 |
| 传输 10GB 文件 | OOM 崩溃 ❌ | ~50MB 内存 | ~20MB 内存 |
| 传输 100GB 文件 | 不可能 ❌ | IndexedDB 满 ⚠️ | ~20MB 内存 ✅ |
| 容量限制 | RAM 大小 | ~几十GB | 磁盘大小 |
架构变更
1. 流式文件写入层 (stream-writer.ts)
AutoFileWriter 类
优先使用 File System Access API(Chrome/Edge 86+):
const writer = new AutoFileWriter(fileName);
await writer.init(suggestedName); // 用户选择保存位置
// 顺序写入
await writer.writeChunk(arrayBuffer);
// 或指定位置写入(支持乱序接收)
await writer.writeAt(position, arrayBuffer);
// 完成写入
await writer.close(); // 文件已保存到磁盘
自动降级(不支持的浏览器):
// 内部自动使用 Blob + 下载
writer.getMode(); // 'stream' 或 'fallback'
浏览器支持
| 浏览器 | 支持情况 | 模式 |
|---|---|---|
| Chrome 86+ | ✅ | stream(直接写磁盘) |
| Edge 86+ | ✅ | stream |
| Firefox | ⚠️ 计划中 | fallback(内存 + 下载) |
| Safari | ❌ | fallback |
2. 发送端优化 (ConnectionTransferProtocol.ts)
滑动窗口 + 流式读取
// 原实现:预加载所有块
const allChunks: ArrayBuffer[] = [];
for (let i = 0; i < total; i++) {
allChunks.push(await readChunk(i)); // ❌ 内存累积
}
// 优化后:按需读取 + 窗口缓存
const chunkCache = new Map<number, ArrayBuffer>();
const readChunk = async (index: number) => {
if (chunkCache.has(index)) return chunkCache.get(index)!;
const data = await file.slice(start, end).arrayBuffer();
// 只缓存窗口内的块
if (index >= ackedCount && index < sentCount + windowSize) {
chunkCache.set(index, data);
}
return data;
};
窗口大小配置
// WebRTC 模式(网络不稳定,窗口较小)
windowSize: 4 // 同时发送 4 个块
// WebSocket 模式(局域网,窗口可更大)
windowSize: 8 // 同时发送 8 个块
3. 接收端优化(V3)
直接写入磁盘
// 原实现:内存累积
file.chunks[index] = data; // ❌ 所有块在内存
// V2:IndexedDB
await storage.saveChunk(fileId, index, data); // ⚠️ 有容量限制
// V3:直接写磁盘
const position = index * chunkSize;
await writer.writeAt(position, data); // ✅ 零内存占用
完成处理
// 流式模式:文件已保存,直接关闭
await writer.close();
console.log('文件已保存到用户选择的位置');
// 降级模式:触发浏览器下载
await writer.close(); // 自动触发下载对话框
配置参数
TransferConfig
{
chunkSize: 64 * 1024, // 块大小(64KB)
windowSize: 4, // 滑动窗口大小
enableAck: true, // 启用 ACK 确认
ackTimeout: 2000, // ACK 超时(毫秒)
maxRetries: 3, // 最大重试次数
}
内存监控
import { TransferMemoryMonitor } from '@/lib/memory-monitor';
const monitor = new TransferMemoryMonitor();
monitor.setWarningThreshold(0.8); // 80% 触发警告
monitor.onWarning((stats) => {
console.warn('内存使用过高:', stats.percentage);
// 可以暂停传输或提示用户
});
monitor.startMonitoring(1000); // 每秒检查
性能特性
内存使用
| 文件大小 | 峰值内存 | 磁盘占用 |
|---|---|---|
| 100MB | ~10MB | 100MB |
| 1GB | ~20MB | 1GB |
| 10GB | ~20MB | 10GB |
| 100GB | ~20MB | 100GB |
说明:峰值内存主要来自滑动窗口(4-8 个块,每块 64KB)+ 系统缓冲区
传输速度
- 局域网 WebSocket:100MB/s+
- WebRTC DataChannel:10-50MB/s
- 磁盘写入:取决于硬盘(SSD 500MB/s+, HDD 100MB/s+)
容量限制
| 方案 | 容量限制 |
|---|---|
| 原实现(内存) | RAM 大小(~8GB) |
| V2(IndexedDB) | ~几十GB(浏览器配额) |
| V3(磁盘流) | 磁盘大小(无实际限制) |
V3 优势:用户可以传输 100GB+ 的文件,只要硬盘空间足够
使用示例
发送大文件
const protocol = new ConnectionTransferProtocol(connection, {
chunkSize: 64 * 1024,
windowSize: 4,
enableAck: true,
});
// 发送 100GB 文件,内存稳定在 ~20MB
const result = await protocol.sendFile(largeFile, 'file-123');
接收大文件
// V3:接收端会自动提示用户选择保存位置
protocol.onFileStart((meta) => {
console.log('准备接收:', meta.name, meta.size);
// 用户会看到文件保存对话框
});
protocol.onFileComplete(({ id, file }) => {
console.log('文件传输完成!');
// 流式模式:文件已保存到用户选择的位置
// 降级模式:浏览器已触发下载
});
protocol.onFileProgress(({ fileName, progress }) => {
console.log(`${fileName}: ${progress.toFixed(1)}%`);
});
用户体验
Chrome/Edge(流式模式):
- 开始接收时弹出"保存文件"对话框
- 用户选择保存位置
- 文件边接收边写入磁盘
- 完成后文件直接出现在选择的位置
Firefox/Safari(降级模式):
- 静默接收(内存中)
- 接收完成后触发浏览器下载
- 用户在下载栏看到文件
监控内存
import { TransferMemoryMonitor } from '@/lib/memory-monitor';
const monitor = new TransferMemoryMonitor();
monitor.onWarning((stats) => {
toast.warning(`内存使用: ${stats.percentage * 100}%`);
});
monitor.startMonitoring();
// 清理
onUnmount(() => {
monitor.stopMonitoring();
});
最佳实践
1. 提前检测浏览器能力
import { supportsStreamWrite } from '@/lib/stream-writer';
if (supportsStreamWrite()) {
console.log('✅ 支持流式写入,可传输任意大小文件');
} else {
console.warn('⚠️ 使用降级模式,大文件会占用内存');
// 可以限制文件大小
if (fileSize > 1024 * 1024 * 1024) {
alert('您的浏览器不支持大文件传输,请使用 Chrome 或 Edge');
}
}
2. 错误处理(用户取消保存)
protocol.onFileError(({ fileId, error }) => {
if (error.includes('用户取消')) {
console.log('用户取消了文件保存');
} else {
console.error('传输失败:', error);
}
});
3. 错误处理
protocol.onFileError(({ fileId, error }) => {
console.error('传输失败:', error);
// 清理失败的文件块
getGlobalChunkStorage()
.deleteFile(fileId)
.catch(err => console.error('清理失败:', err));
});
4. 进度显示
protocol.onFileProgress(({ fileName, progress, transferredBytes, totalBytes }) => {
console.log(`${fileName}: ${progress.toFixed(1)}% (${transferredBytes}/${totalBytes})`);
// 更新 UI
setProgress(progress);
});
兼容性
浏览器支持
| 特性 | Chrome | Firefox | Safari | Edge |
|---|---|---|---|---|
| IndexedDB | ✅ 24+ | ✅ 16+ | ✅ 10+ | ✅ 12+ |
| Async Iterator | ✅ 63+ | ✅ 57+ | ✅ 11.1+ | ✅ 79+ |
| File.slice | ✅ 21+ | ✅ 13+ | ✅ 10+ | ✅ 12+ |
降级方案
如果浏览器不支持 IndexedDB(极少见),可以:
- 限制文件大小(如 100MB)
- 显示警告提示用户升级浏览器
- 使用分段上传到服务器
故障排查
问题 1:用户取消了保存对话框
症状:接收失败,提示"初始化失败"
原因:用户在 File System Access API 对话框中点了取消
解决:
protocol.onFileError(({ error }) => {
if (error.includes('用户')) {
toast.info('已取消接收文件');
}
});
问题 2:降级模式内存仍然很高
排查:
- 检查是否有其他地方缓存了文件
- 使用 Chrome DevTools Memory Profiler
- 确认
chunkCache.clear()被调用
解决:
// 强制垃圾回收(仅开发环境)
if (typeof gc !== 'undefined') {
gc();
}
问题 3:传输很慢
排查:
- IndexedDB 写入可能较慢(首次)
- 窗口太小(并行度不够)
解决:
// 增大窗口(局域网环境)
windowSize: 16
// 或禁用 ACK(可靠网络)
enableAck: false
后续优化
- Web Workers:将 IndexedDB 操作移到 Worker 避免阻塞主线程
- 压缩传输:使用 CompressionStream API 压缩块
- 增量校验:使用 xxHash 替代 CRC32 提升性能
- 断点续传:基于 IndexedDB 实现传输恢复
总结
通过这次优化,文件传输不再受内存限制,理论上可以传输任意大小的文件(受限于 IndexedDB 配额)。
关键改进:
- ✅ 内存使用从 O(n) 降到 O(1)
- ✅ 支持超大文件(10GB+)
- ✅ 传输速度不受影响
- ✅ 向后兼容,无需修改 UI 层