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