# 文件快传(Chuan)— 系统架构与技术设计文档 > 最后更新:2025-08-02 --- ## 一、项目总览 **文件快传(Chuan)** 是一个基于 **WebRTC P2P** 的文件传输应用。核心设计理念:**所有实际数据(文件、文字、桌面画面)均通过 WebRTC 点对点直传,服务器仅承担信令中继和房间管理职责**。 ### 技术栈 | 层级 | 技术选型 | |------|---------| | 后端 | Go 1.21 · chi/v5 路由 · gorilla/websocket | | 前端 | Next.js 15 · React 19 · TypeScript · Tailwind CSS 4 | | 状态管理 | Zustand 5 + React useState/useRef | | UI 组件 | shadcn/ui (Radix UI) · Lucide Icons | | P2P 通信 | WebRTC DataChannel(文件/文字)· MediaStream(桌面共享)| | 部署 | 双模式:Next.js 开发模式 / Go 嵌入前端静态文件 | ### 核心特性 | 功能 | 实现方式 | |------|---------| | 文件传输 | WebRTC DataChannel · 256KB 分块 · CRC32 校验 · ACK 确认 | | 文本消息 | WebRTC DataChannel · 实时双向同步 · 打字状态 | | 桌面共享 | WebRTC MediaStream · getDisplayMedia · renegotiation | | ICE 配置 | 默认 5 个 STUN · 支持自定义 STUN/TURN · localStorage 持久化 | --- ## 二、系统架构 ### 2.1 整体架构图 ``` ┌──────────────────────────────────────────────────────────────────────┐ │ Go 后端 (:8080) │ │ ┌──────────────┐ ┌───────────────────┐ ┌─────────────────────┐ │ │ │ REST API │ │ WebSocket 信令 │ │ 静态文件服务 │ │ │ │ /api/* │ │ /api/ws/webrtc │ │ go:embed frontend │ │ │ │ │ │ /ws/webrtc │ │ │ │ │ │ - create-room │ │ │ │ SPA 回退 index.html │ │ │ │ - room-info │ │ 纯中继 转发消息 │ │ │ │ │ └──────────────┘ └───────────────────┘ └─────────────────────┘ │ │ │ │ │ ┌───────────────┼───────────────┐ │ │ │ 内存房间管理 (sync.RWMutex) │ │ │ │ map[string]*WebRTCRoom │ │ │ │ 1小时过期 · 5分钟定时清理 │ │ │ └───────────────────────────────┘ │ └──────────────────────────────────────────────────────────────────────┘ │ WebSocket │ WebSocket │ (信令: offer/answer/ice-candidate) │ ▼ ▼ ┌───────────────┐ WebRTC P2P ┌───────────────┐ │ 发送方浏览器 │ ◄══════════════════════════► │ 接收方浏览器 │ │ │ DataChannel (文件/文字) │ │ │ Next.js App │ MediaStream (桌面共享) │ Next.js App │ └───────────────┘ └───────────────┘ ``` ### 2.2 数据流向 ``` 1. 控制平面 (HTTP): 浏览器 ──── REST API ────→ Go 后端 (房间创建/查询) 2. 信令平面 (WS): 浏览器 ←──── WebSocket ──→ Go 后端 (SDP/ICE 交换) 3. 数据平面 (P2P): 浏览器 ◄═══ DataChannel ══► 浏览器 (文件/文字直传) 4. 媒体平面 (P2P): 浏览器 ◄═══ MediaStream ══► 浏览器 (桌面画面直传) ``` **关键设计:数据平面完全绕过服务器,文件和文字内容不经服务器中转。** --- ## 三、后端设计 ### 3.1 目录结构 ``` cmd/ ├── main.go # 入口:参数解析 → 配置 → 路由 → 启动服务器 ├── config.go # 配置管理:命令行 > 环境变量 > .chuan.env > 默认值 ├── router.go # chi 路由注册 + 中间件 + 前端静态服务 └── server.go # HTTP Server 封装 + 优雅关闭 (SIGINT/SIGTERM) internal/ ├── handlers/ │ └── handlers.go # HTTP/WebSocket 请求处理(薄层,委托给 service) ├── models/ │ └── models.go # 数据模型:WebRTCRoom、WebRTCClient、RoomStatus ├── services/ │ └── webrtc_service.go # 核心信令服务:房间管理 + WebSocket 消息转发 └── web/ └── frontend.go # go:embed 嵌入前端 + SPA 回退 ``` ### 3.2 API 端点 | 方法 | 路径 | 功能 | 请求/响应 | |------|------|------|-----------| | `POST` | `/api/create-room` | 创建房间 | `{}` → `{success, code, message}` | | `GET` | `/api/room-info?code=XXX` | 查询房间状态 | → `{success, status: RoomStatus}` | | `GET` | `/api/webrtc-room-status?code=XXX` | 同上(别名)| 同上 | | `WS` | `/api/ws/webrtc?code=&role=` | WebRTC 信令 | WebSocket 双向 | | `WS` | `/ws/webrtc?code=&role=` | 同上(兼容路径)| 同上 | | `GET` | `/*` | 前端静态文件 | SPA 回退 | ### 3.3 房间管理 - **房间代码**:6 位,字符集 `123456789ABCDEFGHIJKLMNPQRSTUVWXYZ`(排除 0 和 O,避免混淆) - **房间容量**:最多 2 人(1 sender + 1 receiver) - **过期策略**:创建后 1 小时过期,每 5 分钟后台清理 - **存储方式**:纯内存 `map[string]*WebRTCRoom` + `sync.RWMutex`,无数据库 ### 3.4 WebSocket 信令协议 #### 连接 URL ``` ws[s]://host/api/ws/webrtc?code=ROOM_CODE&role=sender|receiver&channel=shared ``` #### 服务端 → 客户端 | type | payload | 触发时机 | |------|---------|---------| | `peer-joined` | `{ role }` | 对方加入房间 | | `disconnection` | `{ role, message }` | 对方断开连接 | | `error` | `{ message }` | 房间不存在 / 已满 / 参数无效 | #### 客户端 ↔ 客户端(经服务端纯转发) | type | payload | 说明 | |------|---------|------| | `offer` | `RTCSessionDescription` | SDP Offer | | `answer` | `RTCSessionDescription` | SDP Answer | | `ice-candidate` | `RTCIceCandidate` | ICE 候选地址 | **服务端对 offer/answer/ice-candidate 消息不做任何解析,仅中继转发给房间内另一方。** --- ## 四、前端设计(重点) ### 4.1 目录结构 ``` chuan-next/src/ ├── app/ # Next.js App Router │ ├── page.tsx # 入口页(SSG) │ ├── layout.tsx # 根布局(字体、Toast、Umami 统计) │ ├── globals.css # 全局样式 + 动画定义 │ ├── HomePage.tsx # 主页面组件(Tab 管理 + WebRTC 状态) │ ├── HomePageWrapper.tsx # Suspense 包裹(SSR 兼容) │ └── api/ # Next.js API Routes(开发模式代理) │ ├── create-room/route.ts # → GO_BACKEND_URL/api/create-room │ ├── room-info/route.ts # → GO_BACKEND_URL/api/room-info │ ├── room-status/route.ts # → GO_BACKEND_URL/api/room-status │ ├── create-text-room/route.ts │ ├── get-text-content/route.ts │ └── update-files/route.ts │ ├── components/ # UI 组件 │ ├── WebRTCFileTransfer.tsx # 文件传输整合组件(659行) │ ├── WebRTCTextImageTransfer.tsx # 文字+图片传输整合组件 │ ├── DesktopShare.tsx # 桌面共享整合组件 │ ├── Hero.tsx # 页头(标题、GitHub 链接) │ ├── webrtc/ # WebRTC 功能子组件 │ │ ├── WebRTCFileUpload.tsx # 文件发送 UI │ │ ├── WebRTCFileReceive.tsx # 文件接收 UI │ │ ├── WebRTCTextSender.tsx # 文字发送 UI │ │ ├── WebRTCTextReceiver.tsx # 文字接收 UI │ │ ├── WebRTCDesktopSender.tsx # 桌面共享发送 UI │ │ ├── WebRTCDesktopReceiver.tsx # 桌面共享接收 UI │ │ └── WebRTCSettings.tsx # ICE 服务器配置 UI │ └── ui/ # 基础 UI 组件 (shadcn/ui) │ ├── button.tsx # 按钮(6种 variant) │ ├── tabs.tsx # 标签页(Radix Tabs) │ ├── input.tsx # 输入框 │ ├── card.tsx # 卡片 │ ├── dialog.tsx # 对话框 │ ├── progress.tsx # 进度条 │ ├── textarea.tsx # 文本域 │ ├── toast.tsx # Toast (Radix) │ ├── toast-simple.tsx # 轻量 Toast (自定义) │ ├── toaster.tsx # Toast 渲染 │ └── confirm-dialog.tsx # 确认对话框 │ ├── hooks/ # 自定义 Hooks(核心逻辑层) │ ├── index.ts # 统一导出 │ ├── connection/ # WebRTC 连接管理 │ │ ├── useSharedWebRTCManager.ts # 整合入口 │ │ ├── useWebRTCConnectionCore.ts # 核心连接(WS + PeerConnection) │ │ ├── useWebRTCDataChannelManager.ts # DataChannel 管理 + 消息路由 │ │ ├── useWebRTCTrackManager.ts # MediaStream 轨道管理 │ │ ├── useWebRTCStateManager.ts # Zustand store 封装 │ │ ├── useRoomConnection.ts # 加入房间逻辑 │ │ ├── useConnectionState.ts # 连接状态变化处理 │ │ └── useWebRTCSupport.ts # 浏览器 WebRTC 检测 │ ├── file-transfer/ # 文件传输业务 │ │ ├── useFileTransferBusiness.ts # 核心:分块传输 + CRC32 + ACK │ │ ├── useFileListSync.ts # 文件列表实时同步 │ │ └── useFileStateManager.ts # 文件状态(选中/下载/进度) │ ├── text-transfer/ # 文字传输业务 │ │ └── useTextTransferBusiness.ts # 实时文字同步 + 打字状态 │ ├── desktop-share/ # 桌面共享业务 │ │ └── useDesktopShareBusiness.ts # getDisplayMedia + 轨道管理 │ ├── settings/ # 设置 │ │ ├── useIceServersConfig.ts # ICE 服务器配置 + localStorage │ │ └── useWebRTCConfigSync.ts # 配置变更监听 │ └── ui/ # UI 相关 │ ├── webRTCStore.ts # Zustand 全局状态 │ ├── useURLHandler.ts # URL 参数管理 │ ├── useTabNavigation.ts # Tab 切换管理 │ └── useConfirmDialog.ts # 确认对话框 Hook │ ├── lib/ # 工具库 │ ├── config.ts # 环境配置(API URL / WS URL 动态计算) │ ├── utils.ts # cn() 样式合并 │ ├── client-api.ts # ClientAPI 封装 │ ├── api-utils.ts # apiFetch() 统一请求 │ └── webrtc-support.ts # WebRTC 支持检测 │ └── types/ └── index.ts # 全局类型定义 ``` ### 4.2 组件层次结构 ``` RootLayout (layout.tsx) └─ ToastProvider (toast-simple.tsx) └─ HomePageWrapper (Suspense) └─ HomePage ├─ Hero # 页头 ├─ WebRTCUnsupportedModal # WebRTC 不支持提示 ├─ ConfirmDialog # Tab 切换确认 └─ Tabs (5个标签) │ ├─ [Tab: webrtc] WebRTCFileTransfer │ ├─ 模式切换 (发送/接收) │ ├─ [发送模式] WebRTCFileUpload │ │ ├─ 拖拽上传区域 / 文件列表 │ │ ├─ RoomInfoDisplay (取件码 + 二维码 + 复制) │ │ ├─ ConnectionStatus (连接状态) │ │ └─ 传输进度条 │ └─ [接收模式] WebRTCFileReceive │ ├─ 取件码输入 (6位) │ ├─ ConnectionStatus (连接状态) │ └─ 文件列表 + 下载按钮 │ ├─ [Tab: message] WebRTCTextImageTransfer │ ├─ 模式切换 (发送/接收) │ ├─ [发送模式] WebRTCTextSender │ │ ├─ Textarea 编辑器 │ │ ├─ 图片发送按钮 │ │ └─ RoomInfoDisplay │ └─ [接收模式] WebRTCTextReceiver │ ├─ 取件码输入 │ └─ 实时文字显示 + 图片接收 │ ├─ [Tab: desktop] DesktopShare │ ├─ 模式切换 (共享/查看) │ ├─ [共享模式] WebRTCDesktopSender │ │ └─ 选择屏幕 → 开始共享 │ └─ [查看模式] WebRTCDesktopReceiver │ └─ DesktopViewer (video 标签) │ ├─ [Tab: wechat] WeChatGroup │ └─ 微信群二维码 │ └─ [Tab: settings] WebRTCSettings └─ ICE 服务器配置表单 ``` ### 4.3 Hooks 架构(核心设计) 前端逻辑层采用 **分层 Hooks 架构**,自底向上组合: ``` ┌─────────────────────────────┐ │ Zustand Store │ ← 最底层:全局状态 │ (webRTCStore.ts) │ │ isConnected, isConnecting │ │ currentRoom, error │ └──────────────┬──────────────┘ │ ┌──────────────────────┼──────────────────────┐ │ │ │ ┌────────▼────────┐ ┌─────────▼──────────┐ ┌───────▼─────────┐ │ StateManager │ │ DataChannelManager │ │ TrackManager │ │ (Zustand 封装) │ │ (消息路由 + 收发) │ │ (媒体轨道管理) │ └────────┬────────┘ └─────────┬──────────┘ └───────┬─────────┘ │ │ │ └──────────────────────┼──────────────────────┘ │ ┌──────────────▼──────────────┐ │ ConnectionCore │ ← WebSocket + PeerConnection │ (useWebRTCConnectionCore) │ 信令处理 + ICE 交换 └──────────────┬──────────────┘ │ ┌──────────────▼──────────────┐ │ SharedWebRTCManager │ ← 整合入口(4合1) │ (useSharedWebRTCManager) │ 返回统一 WebRTCConnection └──────────────┬──────────────┘ │ ┌────────────────────────┼────────────────────────┐ │ │ │ ┌────────▼────────┐ ┌──────────▼──────────┐ ┌────────▼────────┐ │ FileTransfer │ │ TextTransfer │ │ DesktopShare │ │ Business │ │ Business │ │ Business │ │ (文件分块+校验) │ │ (实时同步文字) │ │ (屏幕采集+推流) │ └────────┬────────┘ └──────────┬──────────┘ └────────┬────────┘ │ │ │ └────────────────────────┼────────────────────────┘ │ ┌──────────────▼──────────────┐ │ 业务级组件 (TSX) │ │ WebRTCFileTransfer.tsx 等 │ └─────────────────────────────┘ ``` #### 4.3.1 连接层 — `useWebRTCConnectionCore` **职责**:管理 WebSocket 信令连接 + RTCPeerConnection 生命周期。 ``` connect(code, role) │ ├─ 1. WebSocket 连接到 ws://host/api/ws/webrtc?code=XXX&role=YYY │ ├─ 2. 收到 "peer-joined" → 创建 RTCPeerConnection │ └─ ICE 服务器从 getIceServersConfig() 获取 │ ├─ 3. Sender 创建 DataChannel → 创建 Offer │ Receiver 等待 ondatachannel │ ├─ 4. offer/answer/ice-candidate 通过 WebSocket 交换 │ ├─ 5. ICE 连接建立 → DataChannel open → 连接完成 │ └─ 6. 断开时:发送 disconnection → 清理 PeerConnection ``` #### 4.3.2 数据通道层 — `useWebRTCDataChannelManager` **职责**:管理 DataChannel 消息的路由分发。 **核心设计:多 channel 路由** 所有 JSON 消息通过 `channel` 字段路由到不同的业务处理器: ```typescript // 消息格式 { channel: "file-transfer" | "text-transfer" | "desktop-share", type: "具体消息类型", payload: { ... } } ``` ``` DataChannel.onmessage │ ├─ typeof data === 'string' → JSON.parse │ ├─ channel === 'file-transfer' → fileTransferHandler │ ├─ channel === 'text-transfer' → textTransferHandler │ └─ channel === 'desktop-share' → desktopShareHandler │ └─ typeof data === ArrayBuffer → 二进制数据 └─ 优先路由到 'file-transfer' handler(文件块数据) ``` **注意**:三个功能(文件/文字/桌面)共享同一个 DataChannel(`shared-channel`),而非各自创建独立通道。 #### 4.3.3 轨道管理层 — `useWebRTCTrackManager` **职责**:管理 MediaStream 轨道(仅桌面共享使用)。 - `addTrack(track, stream)` — 添加视频/音频轨道 - `removeTrack(sender)` — 移除轨道 - `createOfferNow()` — 添加轨道后触发重协商 - `onTrack` 回调 — 接收远程媒体流 ### 4.4 状态管理设计 采用 **三层状态模型**: | 层级 | 技术 | 用途 | 示例 | |------|------|------|------| | 全局共享状态 | Zustand | WebRTC 连接状态 | `isConnected`, `currentRoom`, `error` | | 组件级状态 | React useState | UI 交互状态 | `selectedFiles`, `mode`, `progress` | | 引用状态 | React useRef | 回调/缓冲区/定时器 | `receiveBufferRef`, `messageHandlers`, 防抖 timer | #### Zustand Store 结构 ```typescript interface WebRTCState { // 连接状态 isConnected: boolean; // 总体连接状态 isConnecting: boolean; // 正在连接中 isWebSocketConnected: boolean; // WebSocket 层连接 isPeerConnected: boolean; // PeerConnection 层连接 // 错误状态 error: string | null; canRetry: boolean; // 房间信息 currentRoom: { code: string; role: 'sender' | 'receiver' } | null; // Actions updateState(partial: Partial): void; setCurrentRoom(room: { code: string; role: string } | null): void; resetToInitial(): void; } ``` ### 4.5 文件传输详细设计 #### 4.5.1 DataChannel 消息协议 **file-transfer channel 消息类型:** | 方向 | type | payload | 说明 | |------|------|---------|------| | S→R | `file-list` | `FileInfo[]` | 发送方文件列表(实时同步) | | R→S | `file-request` | `{ fileId, fileName }` | 接收方请求下载 | | S→R | `file-metadata` | `{ id, name, size, type }` | 文件元信息 | | S→R | `file-chunk-info` | `{ fileId, chunkIndex, totalChunks, checksum }` | 块元信息(含 CRC32) | | S→R | *(二进制)* | `ArrayBuffer` (≤256KB) | 文件块数据 | | R→S | `file-chunk-ack` | `{ fileId, chunkIndex, success, checksum }` | 块确认(含校验结果) | | S→R | `file-complete` | `{ fileId }` | 文件传输完成 | #### 4.5.2 传输参数 | 参数 | 值 | 说明 | |------|-----|------| | 块大小 | 256 KB | 每个 chunk 的最大字节数 | | 最大重试 | 5 次 | 单个 chunk 校验失败后的重试上限 | | 退避策略 | 指数退避 | 重试间隔指数增长 | | 校验算法 | CRC32 | 每个 chunk 独立校验 | | 流控 | 自适应 | 根据平均传输速度动态调整发送间隔 | | DataChannel 配置 | `ordered: true, maxRetransmits: 3` | 有序可靠传输 | #### 4.5.3 完整传输时序 ``` 发送方 信令服务器 接收方 │ │ │ │ 1. POST /api/create-room │ │ │ ──────────────────────────────────► │ │ │ ◄── {success: true, code: "AB12CD"} │ │ │ │ │ │ 2. WS connect (role=sender) │ │ │ ═══════════════════════════════════► │ │ │ │ │ │ │ 3. GET /api/room-info?code=... │ │ │ ◄────────────────────────────── │ │ │ ── {success, status} ─────────► │ │ │ │ │ │ 4. WS connect (role=receiver) │ │ │ ◄═══════════════════════════════ │ │ │ │ │ 5. ◄── peer-joined ── │ ── peer-joined ──► │ │ │ │ │ 6. 创建 PeerConnection + DataChannel │ │ 7. 创建 SDP Offer │ │ ═══ offer ═══════════════════════► │ ═══ offer ═══════════════════► │ │ │ │ │ │ 8. 创建 PeerConnection │ │ │ 9. 设置 Remote/Local SDP │ │ ◄═══ answer ══════════════════════ │ ◄═══ answer ═════════════════ │ │ │ │ │ 10. ICE Candidate 交换 ◄═══════════╋══════════════════════════════► │ │ │ │ │ ══════════ DataChannel OPEN ═══════╪══════════════════════════════ │ │ │ │ │ 11. file-list ─────────────────────┼────────────────────────────────► │ │ [{id, name, size, type}, ...] │ │ │ │ │ │ ◄─────────────────────────────────┼──── 12. file-request ────────── │ │ {fileId, fileName} │ │ │ │ │ │ 13. file-metadata ────────────────┼────────────────────────────────► │ │ {id, name, size, type} │ │ │ │ │ │ ╔══ 循环每个 chunk ═══════════════╪════════════════════════════════╗ │ │ ║ 14. file-chunk-info ───────────┼──────────────────────────────► ║ │ │ ║ {fileId, chunkIndex, │ ║ │ │ ║ totalChunks, checksum} │ ║ │ │ ║ │ ║ │ │ ║ 15. [ArrayBuffer 256KB] ───────┼──────────────────────────────► ║ │ │ ║ │ ║ │ │ ║ ◄───────────────────────────────┼── 16. file-chunk-ack ─────── ║ │ │ ║ {fileId, chunkIndex, │ success, checksum} ║ │ │ ║ │ ║ │ │ ║ [如果 success=false, 指数退避重试,最多5次] ║ │ │ ╚══════════════════════════════════╪══════════════════════════════╝ │ │ │ │ │ 17. file-complete ────────────────┼────────────────────────────────► │ │ {fileId} │ │ │ │ 18. 组装 Blob │ │ │ → File │ │ │ → 下载 │ ``` #### 4.5.4 文件列表同步机制 发送方选择文件后,文件列表会 **实时同步** 到接收方: ``` 发送方文件变更 → useFileListSync hook │ ├─ 150ms 防抖(debounce) │ ├─ 比对新旧文件列表(避免无变更的冗余同步) │ └─ 通过 DataChannel 发送 file-list 消息 │ └─ 接收方更新 UI 展示文件列表 ``` ### 4.6 文字传输详细设计 #### 4.6.1 DataChannel 消息协议 **text-transfer channel 消息类型:** | 方向 | type | payload | 说明 | |------|------|---------|------| | S→R | `text-sync` | `{ text: string }` | 实时文本内容同步 | | 双向 | `text-typing` | `{ typing: boolean }` | 打字状态指示 | #### 4.6.2 实时同步机制 ``` 发送方 Textarea 输入 │ ├─ onChange → handleTextInputChange() │ ├─ 调用 textTransfer.sendTextSync(text) │ └─ DataChannel 发送 { channel: "text-transfer", type: "text-sync", payload: { text } } │ ├─ 同时发送 text-typing: { typing: true } │ └─ 1秒后自动发送 text-typing: { typing: false } │ └─ 接收方 ├─ onTextSync 回调 → 更新显示文本 └─ onTyping 回调 → 显示 "对方正在输入..." 动画 ``` **图片传输**:复用文件传输的 `file-transfer` channel,图片作为文件发送。 ### 4.7 桌面共享详细设计 ``` 发送方 │ ├─ navigator.mediaDevices.getDisplayMedia({ video: true, audio: true }) │ ├─ 获取 MediaStream → 提取 video/audio Track │ ├─ trackManager.addTrack(track, stream) │ └─ peerConnection.addTrack() │ ├─ trackManager.createOfferNow() ← 触发 SDP 重协商 │ └─ 新 Offer → WebSocket → 对方 → Answer → WebSocket → 本方 │ └─ 接收方 ├─ ontrack 事件 → 获取远程 MediaStream └─