mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-31 00:10:22 +08:00
chore: update docs and tests
This commit is contained in:
parent
c2c416ea93
commit
8bab2c8ebc
@ -1,6 +1,6 @@
|
|||||||
# Cherry Studio 局域网传输协议规范
|
# Cherry Studio 局域网传输协议规范
|
||||||
|
|
||||||
> 版本: 3.0
|
> 版本: 1.0
|
||||||
> 最后更新: 2025-12
|
> 最后更新: 2025-12
|
||||||
|
|
||||||
本文档定义了 Cherry Studio 桌面客户端(Electron)与移动端(Expo)之间的局域网文件传输协议。
|
本文档定义了 Cherry Studio 桌面客户端(Electron)与移动端(Expo)之间的局域网文件传输协议。
|
||||||
@ -31,7 +31,7 @@
|
|||||||
| **Client(客户端)** | Electron 桌面端 | 扫描服务、发起连接、发送文件 |
|
| **Client(客户端)** | Electron 桌面端 | 扫描服务、发起连接、发送文件 |
|
||||||
| **Server(服务端)** | Expo 移动端 | 发布服务、接受连接、接收文件 |
|
| **Server(服务端)** | Expo 移动端 | 发布服务、接受连接、接收文件 |
|
||||||
|
|
||||||
### 1.2 协议栈(v3)
|
### 1.2 协议栈(v1)
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────────────────────────┐
|
┌─────────────────────────────────────┐
|
||||||
@ -39,7 +39,6 @@
|
|||||||
├─────────────────────────────────────┤
|
├─────────────────────────────────────┤
|
||||||
│ 消息层(控制: JSON \n) │
|
│ 消息层(控制: JSON \n) │
|
||||||
│ (数据: 二进制帧) │
|
│ (数据: 二进制帧) │
|
||||||
│ v3: 流式传输,无逐块确认 │
|
|
||||||
├─────────────────────────────────────┤
|
├─────────────────────────────────────┤
|
||||||
│ 传输层(TCP) │
|
│ 传输层(TCP) │
|
||||||
├─────────────────────────────────────┤
|
├─────────────────────────────────────┤
|
||||||
@ -51,15 +50,11 @@
|
|||||||
|
|
||||||
```
|
```
|
||||||
1. 服务发现 → 移动端发布 mDNS 服务,桌面端扫描发现
|
1. 服务发现 → 移动端发布 mDNS 服务,桌面端扫描发现
|
||||||
2. TCP 握手 → 建立连接,交换设备信息(`version=3`)
|
2. TCP 握手 → 建立连接,交换设备信息(`version=1`)
|
||||||
3. 文件传输 → 控制消息使用 JSON,`file_chunk` 使用二进制帧连续发送(无需等待确认)
|
3. 文件传输 → 控制消息使用 JSON,`file_chunk` 使用二进制帧分块传输
|
||||||
4. 连接保活 → ping/pong 心跳
|
4. 连接保活 → ping/pong 心跳
|
||||||
```
|
```
|
||||||
|
|
||||||
### 1.4 安全说明
|
|
||||||
|
|
||||||
该协议默认运行在**可信局域网**环境:协议本身不提供传输加密、身份鉴权或防中间人攻击能力。若需要在不可信网络使用,建议在握手阶段增加一次性配对码/Token,并在传输层使用 TLS 或对文件数据进行端到端加密。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. 服务发现(Bonjour/mDNS)
|
## 2. 服务发现(Bonjour/mDNS)
|
||||||
@ -84,7 +79,7 @@
|
|||||||
protocol: "tcp", // 协议
|
protocol: "tcp", // 协议
|
||||||
port: 53317, // TCP 监听端口
|
port: 53317, // TCP 监听端口
|
||||||
txt: { // TXT 记录(可选)
|
txt: { // TXT 记录(可选)
|
||||||
version: "3",
|
version: "1",
|
||||||
platform: "ios" // 或 "android"
|
platform: "ios" // 或 "android"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -129,7 +124,7 @@ const preferredAddress = addresses.find((addr) => isIPv4(addr)) || addresses[0];
|
|||||||
2. 连接成功后立即发送握手消息
|
2. 连接成功后立即发送握手消息
|
||||||
3. 等待服务端响应握手确认
|
3. 等待服务端响应握手确认
|
||||||
|
|
||||||
### 3.2 握手消息(协议版本 v3)
|
### 3.2 握手消息(协议版本 v1)
|
||||||
|
|
||||||
#### Client → Server: `handshake`
|
#### Client → Server: `handshake`
|
||||||
|
|
||||||
@ -137,7 +132,7 @@ const preferredAddress = addresses.find((addr) => isIPv4(addr)) || addresses[0];
|
|||||||
type LanTransferHandshakeMessage = {
|
type LanTransferHandshakeMessage = {
|
||||||
type: "handshake";
|
type: "handshake";
|
||||||
deviceName: string; // 设备名称
|
deviceName: string; // 设备名称
|
||||||
version: string; // 协议版本,当前为 "3"
|
version: string; // 协议版本,当前为 "1"
|
||||||
platform?: string; // 平台:'darwin' | 'win32' | 'linux'
|
platform?: string; // 平台:'darwin' | 'win32' | 'linux'
|
||||||
appVersion?: string; // 应用版本
|
appVersion?: string; // 应用版本
|
||||||
};
|
};
|
||||||
@ -149,7 +144,7 @@ type LanTransferHandshakeMessage = {
|
|||||||
{
|
{
|
||||||
"type": "handshake",
|
"type": "handshake",
|
||||||
"deviceName": "Cherry Studio 1.7.2",
|
"deviceName": "Cherry Studio 1.7.2",
|
||||||
"version": "3",
|
"version": "1",
|
||||||
"platform": "darwin",
|
"platform": "darwin",
|
||||||
"appVersion": "1.7.2"
|
"appVersion": "1.7.2"
|
||||||
}
|
}
|
||||||
@ -157,10 +152,10 @@ type LanTransferHandshakeMessage = {
|
|||||||
|
|
||||||
### 4. 消息格式规范(混合协议)
|
### 4. 消息格式规范(混合协议)
|
||||||
|
|
||||||
v3 采用"控制 JSON + 二进制数据帧"的混合协议,支持流式传输:
|
v1 使用"控制 JSON + 二进制数据帧"的混合协议(流式传输模式,无 per-chunk ACK):
|
||||||
|
|
||||||
- **控制消息**(握手、心跳、file_start/ack、file_end、file_complete、file_cancel):UTF-8 JSON,`\n` 分隔
|
- **控制消息**(握手、心跳、file_start/ack、file_end、file_complete):UTF-8 JSON,`\n` 分隔
|
||||||
- **数据消息**(`file_chunk`):二进制帧,使用 Magic + 总长度做分帧,连续发送无需等待确认
|
- **数据消息**(`file_chunk`):二进制帧,使用 Magic + 总长度做分帧,不经 Base64
|
||||||
|
|
||||||
### 4.1 控制消息编码(JSON + `\n`)
|
### 4.1 控制消息编码(JSON + `\n`)
|
||||||
|
|
||||||
@ -209,26 +204,25 @@ function sendControlMessage(socket: Socket, message: object): void {
|
|||||||
3. 否则若首字节为 `{` → 按 JSON + `\n` 解析控制消息
|
3. 否则若首字节为 `{` → 按 JSON + `\n` 解析控制消息
|
||||||
4. 其它数据丢弃 1 字节并继续循环,避免阻塞。
|
4. 其它数据丢弃 1 字节并继续循环,避免阻塞。
|
||||||
|
|
||||||
### 4.4 消息类型汇总(v3)
|
### 4.4 消息类型汇总(v1)
|
||||||
|
|
||||||
| 类型 | 方向 | 编码 | 用途 |
|
| 类型 | 方向 | 编码 | 用途 |
|
||||||
| ---------------- | --------------- | -------- | ------------------------------ |
|
| ---------------- | --------------- | -------- | ----------------------- |
|
||||||
| `handshake` | Client → Server | JSON+\n | 握手请求(version=3) |
|
| `handshake` | Client → Server | JSON+\n | 握手请求(version=1) |
|
||||||
| `handshake_ack` | Server → Client | JSON+\n | 握手响应 |
|
| `handshake_ack` | Server → Client | JSON+\n | 握手响应 |
|
||||||
| `ping` | Client → Server | JSON+\n | 心跳请求 |
|
| `ping` | Client → Server | JSON+\n | 心跳请求 |
|
||||||
| `pong` | Server → Client | JSON+\n | 心跳响应 |
|
| `pong` | Server → Client | JSON+\n | 心跳响应 |
|
||||||
| `file_start` | Client → Server | JSON+\n | 开始文件传输 |
|
| `file_start` | Client → Server | JSON+\n | 开始文件传输 |
|
||||||
| `file_start_ack` | Server → Client | JSON+\n | 文件传输确认 |
|
| `file_start_ack` | Server → Client | JSON+\n | 文件传输确认 |
|
||||||
| `file_chunk` | Client → Server | 二进制帧 | 文件数据块(连续发送,无确认) |
|
| `file_chunk` | Client → Server | 二进制帧 | 文件数据块(无 Base64,流式无 per-chunk ACK) |
|
||||||
| `file_end` | Client → Server | JSON+\n | 文件传输结束 |
|
| `file_end` | Client → Server | JSON+\n | 文件传输结束 |
|
||||||
| `file_complete` | Server → Client | JSON+\n | 传输完成结果 |
|
| `file_complete` | Server → Client | JSON+\n | 传输完成结果 |
|
||||||
| `file_cancel` | Client → Server | JSON+\n | 取消传输 |
|
|
||||||
|
|
||||||
```
|
```
|
||||||
{"type":"message_type",...其他字段...}\n
|
{"type":"message_type",...其他字段...}\n
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4.5 消息发送
|
### 4.3 消息发送
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
function sendMessage(socket: Socket, message: object): void {
|
function sendMessage(socket: Socket, message: object): void {
|
||||||
@ -237,63 +231,48 @@ function sendMessage(socket: Socket, message: object): void {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4.6 消息接收与解析(v3 混合协议)
|
### 4.4 消息接收与解析
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const MAGIC = Buffer.from([0x43, 0x53]); // "CS"
|
let buffer = "";
|
||||||
let buffer = Buffer.alloc(0);
|
|
||||||
|
|
||||||
socket.on("data", (chunk: Buffer) => {
|
socket.on("data", (chunk: Buffer) => {
|
||||||
buffer = Buffer.concat([buffer, chunk]);
|
buffer += chunk.toString("utf8");
|
||||||
|
|
||||||
while (buffer.length > 0) {
|
let newlineIndex = buffer.indexOf("\n");
|
||||||
// 检查是否为二进制帧(Magic: 0x43 0x53)
|
while (newlineIndex !== -1) {
|
||||||
if (buffer.length >= 2 && buffer[0] === 0x43 && buffer[1] === 0x53) {
|
const line = buffer.slice(0, newlineIndex).trim();
|
||||||
// 需要至少 6 字节头(Magic + TotalLen)
|
buffer = buffer.slice(newlineIndex + 1);
|
||||||
if (buffer.length < 6) break;
|
|
||||||
|
|
||||||
const totalLen = buffer.readUInt32BE(2);
|
if (line.length > 0) {
|
||||||
const frameLen = 6 + totalLen; // Magic(2) + TotalLen(4) + payload
|
const message = JSON.parse(line);
|
||||||
|
handleMessage(message);
|
||||||
if (buffer.length < frameLen) break; // 等待更多数据
|
|
||||||
|
|
||||||
// 解析二进制帧
|
|
||||||
const type = buffer[6];
|
|
||||||
const transferIdLen = buffer.readUInt16BE(7);
|
|
||||||
const transferId = buffer.slice(9, 9 + transferIdLen).toString("utf8");
|
|
||||||
const chunkIndex = buffer.readUInt32BE(9 + transferIdLen);
|
|
||||||
const data = buffer.slice(13 + transferIdLen, frameLen);
|
|
||||||
|
|
||||||
handleBinaryChunk(transferId, chunkIndex, data);
|
|
||||||
buffer = buffer.slice(frameLen);
|
|
||||||
}
|
}
|
||||||
// 检查是否为 JSON 控制消息(以 '{' 开头)
|
|
||||||
else if (buffer[0] === 0x7b) {
|
|
||||||
// '{' = 0x7b
|
|
||||||
const newlineIndex = buffer.indexOf(0x0a); // '\n' = 0x0a
|
|
||||||
if (newlineIndex === -1) break; // 等待完整的 JSON 行
|
|
||||||
|
|
||||||
const line = buffer.slice(0, newlineIndex).toString("utf8").trim();
|
newlineIndex = buffer.indexOf("\n");
|
||||||
buffer = buffer.slice(newlineIndex + 1);
|
|
||||||
|
|
||||||
if (line.length > 0) {
|
|
||||||
const message = JSON.parse(line);
|
|
||||||
handleMessage(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 其他数据丢弃 1 字节,继续解析
|
|
||||||
else {
|
|
||||||
buffer = buffer.slice(1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 4.5 消息类型汇总
|
||||||
|
|
||||||
|
| 类型 | 方向 | 用途 |
|
||||||
|
| ---------------- | --------------- | ------------ |
|
||||||
|
| `handshake` | Client → Server | 握手请求 |
|
||||||
|
| `handshake_ack` | Server → Client | 握手响应 |
|
||||||
|
| `ping` | Client → Server | 心跳请求 |
|
||||||
|
| `pong` | Server → Client | 心跳响应 |
|
||||||
|
| `file_start` | Client → Server | 开始文件传输 |
|
||||||
|
| `file_start_ack` | Server → Client | 文件传输确认 |
|
||||||
|
| `file_chunk` | Client → Server | 文件数据块(流式,无 per-chunk ACK) |
|
||||||
|
| `file_end` | Client → Server | 文件传输结束 |
|
||||||
|
| `file_complete` | Server → Client | 传输完成结果 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. 文件传输协议
|
## 5. 文件传输协议
|
||||||
|
|
||||||
### 5.1 传输流程(v3 流式传输)
|
### 5.1 传输流程
|
||||||
|
|
||||||
```
|
```
|
||||||
Client (Sender) Server (Receiver)
|
Client (Sender) Server (Receiver)
|
||||||
@ -304,25 +283,23 @@ Client (Sender) Server (Receiver)
|
|||||||
|<─── 2. file_start_ack ─────────────|
|
|<─── 2. file_start_ack ─────────────|
|
||||||
| (接受/拒绝) |
|
| (接受/拒绝) |
|
||||||
| |
|
| |
|
||||||
|══════ 连续发送数据块(无确认)═══════|
|
|══════ 循环发送数据块(流式,无 ACK) ═════|
|
||||||
| |
|
| |
|
||||||
|──── 3. file_chunk [0] ────────────>|
|
|──── 3. file_chunk [0] ────────────>|
|
||||||
|
| |
|
||||||
|──── 3. file_chunk [1] ────────────>|
|
|──── 3. file_chunk [1] ────────────>|
|
||||||
|──── 3. file_chunk [2] ────────────>|
|
| |
|
||||||
| ... 连续发送所有块 ... |
|
| ... 重复直到所有块发送完成 ... |
|
||||||
|──── 3. file_chunk [N-1] ──────────>|
|
|
||||||
| |
|
| |
|
||||||
|══════════════════════════════════════
|
|══════════════════════════════════════
|
||||||
| |
|
| |
|
||||||
|──── 4. file_end ──────────────────>|
|
|──── 5. file_end ──────────────────>|
|
||||||
| (所有块已发送) |
|
| (所有块已发送) |
|
||||||
| |
|
| |
|
||||||
|<─── 5. file_complete ──────────────|
|
|<─── 6. file_complete ──────────────|
|
||||||
| (校验和验证结果) |
|
| (最终结果) |
|
||||||
```
|
```
|
||||||
|
|
||||||
> **v3 特性**: 数据块连续发送,无需等待每块确认。接收端在收到 `file_end` 后验证完整性,通过 `file_complete` 返回最终结果。
|
|
||||||
|
|
||||||
### 5.2 消息定义
|
### 5.2 消息定义
|
||||||
|
|
||||||
#### 5.2.1 `file_start` - 开始传输
|
#### 5.2.1 `file_start` - 开始传输
|
||||||
@ -352,8 +329,8 @@ type LanTransferFileStartMessage = {
|
|||||||
"fileSize": 524288000,
|
"fileSize": 524288000,
|
||||||
"mimeType": "application/zip",
|
"mimeType": "application/zip",
|
||||||
"checksum": "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456",
|
"checksum": "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456",
|
||||||
"totalChunks": 1000,
|
"totalChunks": 8192,
|
||||||
"chunkSize": 524288
|
"chunkSize": 65536
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -400,7 +377,11 @@ type LanTransferFileStartAckMessage = {
|
|||||||
- `Type` 固定 `0x01`,`Data` 为原始文件二进制数据
|
- `Type` 固定 `0x01`,`Data` 为原始文件二进制数据
|
||||||
- 传输完整性依赖 `file_start.checksum`(全文件 SHA-256);分块校验和可选,不在帧中发送
|
- 传输完整性依赖 `file_start.checksum`(全文件 SHA-256);分块校验和可选,不在帧中发送
|
||||||
|
|
||||||
#### 5.2.4 `file_end` - 传输结束
|
#### 5.2.4 `file_chunk_ack` - 数据块确认(v1 流式不使用)
|
||||||
|
|
||||||
|
v1 采用流式传输,不发送 per-chunk ACK。本节类型仅保留作为向后兼容参考,实际不会发送。
|
||||||
|
|
||||||
|
#### 5.2.5 `file_end` - 传输结束
|
||||||
|
|
||||||
**方向:** Client → Server
|
**方向:** Client → Server
|
||||||
|
|
||||||
@ -420,7 +401,7 @@ type LanTransferFileEndMessage = {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 5.2.5 `file_complete` - 传输完成
|
#### 5.2.6 `file_complete` - 传输完成
|
||||||
|
|
||||||
**方向:** Server → Client
|
**方向:** Server → Client
|
||||||
|
|
||||||
@ -431,26 +412,9 @@ type LanTransferFileCompleteMessage = {
|
|||||||
success: boolean; // 是否成功
|
success: boolean; // 是否成功
|
||||||
filePath?: string; // 保存路径(成功时)
|
filePath?: string; // 保存路径(成功时)
|
||||||
error?: string; // 错误信息(失败时)
|
error?: string; // 错误信息(失败时)
|
||||||
// v3 新增字段
|
|
||||||
errorCode?:
|
|
||||||
| "CHECKSUM_MISMATCH"
|
|
||||||
| "INCOMPLETE_TRANSFER"
|
|
||||||
| "DISK_ERROR"
|
|
||||||
| "CANCELLED";
|
|
||||||
receivedChunks?: number; // 实际接收的数据块数量
|
|
||||||
receivedBytes?: number; // 实际接收的字节数
|
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
**错误码说明:**
|
|
||||||
|
|
||||||
| 错误码 | 说明 |
|
|
||||||
| --------------------- | -------------------------- |
|
|
||||||
| `CHECKSUM_MISMATCH` | 校验和不匹配 |
|
|
||||||
| `INCOMPLETE_TRANSFER` | 传输不完整,缺少数据块 |
|
|
||||||
| `DISK_ERROR` | 磁盘写入错误或存储空间不足 |
|
|
||||||
| `CANCELLED` | 传输被取消 |
|
|
||||||
|
|
||||||
**成功示例:**
|
**成功示例:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@ -458,9 +422,7 @@ type LanTransferFileCompleteMessage = {
|
|||||||
"type": "file_complete",
|
"type": "file_complete",
|
||||||
"transferId": "550e8400-e29b-41d4-a716-446655440000",
|
"transferId": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
"success": true,
|
"success": true,
|
||||||
"filePath": "/storage/emulated/0/Documents/backup.zip",
|
"filePath": "/storage/emulated/0/Documents/backup.zip"
|
||||||
"receivedChunks": 1000,
|
|
||||||
"receivedBytes": 524288000
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -471,32 +433,7 @@ type LanTransferFileCompleteMessage = {
|
|||||||
"type": "file_complete",
|
"type": "file_complete",
|
||||||
"transferId": "550e8400-e29b-41d4-a716-446655440000",
|
"transferId": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
"success": false,
|
"success": false,
|
||||||
"error": "File checksum verification failed",
|
"error": "File checksum verification failed"
|
||||||
"errorCode": "CHECKSUM_MISMATCH",
|
|
||||||
"receivedChunks": 1000,
|
|
||||||
"receivedBytes": 524288000
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 5.2.6 `file_cancel` - 取消传输
|
|
||||||
|
|
||||||
**方向:** Client → Server
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
type LanTransferFileCancelMessage = {
|
|
||||||
type: "file_cancel";
|
|
||||||
transferId: string; // 传输 ID
|
|
||||||
reason?: string; // 取消原因
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
**示例:**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "file_cancel",
|
|
||||||
"transferId": "550e8400-e29b-41d4-a716-446655440000",
|
|
||||||
"reason": "Cancelled by user"
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -519,7 +456,7 @@ async function calculateFileChecksum(filePath: string): Promise<string> {
|
|||||||
|
|
||||||
#### 数据块校验和
|
#### 数据块校验和
|
||||||
|
|
||||||
v3 默认 **不传输分块校验和**,依赖最终文件 checksum。若需要,可在应用层自定义(非协议字段)。
|
v1 默认 **不传输分块校验和**,依赖最终文件 checksum。若需要,可在应用层自定义(非协议字段)。
|
||||||
|
|
||||||
### 5.4 校验流程
|
### 5.4 校验流程
|
||||||
|
|
||||||
@ -602,25 +539,22 @@ type LanTransferPongMessage = {
|
|||||||
|
|
||||||
### 7.1 超时配置
|
### 7.1 超时配置
|
||||||
|
|
||||||
| 操作 | 超时时间 | 说明 |
|
| 操作 | 超时时间 | 说明 |
|
||||||
| -------- | -------- | -------------------- |
|
| ---------- | -------- | --------------------- |
|
||||||
| TCP 连接 | 10 秒 | 连接建立超时 |
|
| TCP 连接 | 10 秒 | 连接建立超时 |
|
||||||
| 握手等待 | 10 秒 | 等待 `handshake_ack` |
|
| 握手等待 | 10 秒 | 等待 `handshake_ack` |
|
||||||
| 传输完成 | 60 秒 | 等待 `file_complete` |
|
| 传输完成 | 60 秒 | 等待 `file_complete` |
|
||||||
| 全局超时 | 10 分钟 | 整个文件传输超时 |
|
|
||||||
|
|
||||||
### 7.2 错误场景处理
|
### 7.2 错误场景处理
|
||||||
|
|
||||||
| 场景 | Client 处理 | Server 处理 |
|
| 场景 | Client 处理 | Server 处理 |
|
||||||
| ------------ | ------------------------ | ------------------------------- |
|
| --------------- | ------------------ | ---------------------- |
|
||||||
| TCP 连接失败 | 通知 UI,允许重试 | - |
|
| TCP 连接失败 | 通知 UI,允许重试 | - |
|
||||||
| 握手超时 | 断开连接,通知 UI | 关闭 socket |
|
| 握手超时 | 断开连接,通知 UI | 关闭 socket |
|
||||||
| 握手被拒绝 | 显示拒绝原因 | - |
|
| 握手被拒绝 | 显示拒绝原因 | - |
|
||||||
| 用户取消 | 发送 `file_cancel`,清理 | 清理临时文件 |
|
| 数据块处理失败 | 中止传输,清理状态 | 清理临时文件 |
|
||||||
| 连接意外断开 | 清理状态,通知 UI | 清理临时文件 |
|
| 连接意外断开 | 清理状态,通知 UI | 清理临时文件 |
|
||||||
| 存储空间不足 | - | 发送 `accepted: false` |
|
| 存储空间不足 | - | 发送 `accepted: false` |
|
||||||
| 校验和失败 | 显示错误信息 | 发送 `file_complete` 带错误码 |
|
|
||||||
| 数据块缺失 | 显示错误信息 | 发送 `INCOMPLETE_TRANSFER` 错误 |
|
|
||||||
|
|
||||||
### 7.3 资源清理
|
### 7.3 资源清理
|
||||||
|
|
||||||
@ -663,8 +597,8 @@ function cleanup(): void {
|
|||||||
### 8.1 协议常量
|
### 8.1 协议常量
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// 协议版本(v3 = 控制 JSON + 二进制 chunk 流式传输)
|
// 协议版本(v1 = 控制 JSON + 二进制 chunk + 流式传输)
|
||||||
export const LAN_TRANSFER_PROTOCOL_VERSION = "3";
|
export const LAN_TRANSFER_PROTOCOL_VERSION = "1";
|
||||||
|
|
||||||
// 服务发现
|
// 服务发现
|
||||||
export const LAN_TRANSFER_SERVICE_TYPE = "cherrystudio";
|
export const LAN_TRANSFER_SERVICE_TYPE = "cherrystudio";
|
||||||
@ -675,14 +609,11 @@ export const LAN_TRANSFER_TCP_PORT = 53317;
|
|||||||
|
|
||||||
// 文件传输(与二进制帧一致)
|
// 文件传输(与二进制帧一致)
|
||||||
export const LAN_TRANSFER_CHUNK_SIZE = 512 * 1024; // 512KB
|
export const LAN_TRANSFER_CHUNK_SIZE = 512 * 1024; // 512KB
|
||||||
export const LAN_TRANSFER_MAX_FILE_SIZE = 500 * 1024 * 1024; // 500MB
|
|
||||||
export const LAN_TRANSFER_GLOBAL_TIMEOUT_MS = 10 * 60 * 1000; // 10 分钟
|
export const LAN_TRANSFER_GLOBAL_TIMEOUT_MS = 10 * 60 * 1000; // 10 分钟
|
||||||
|
|
||||||
// 注意:接收端必须支持至少 512KB 的分片大小,否则会拒收并返回类似
|
|
||||||
// "Chunk size 524288 exceeds limit 65536" 的错误。
|
|
||||||
|
|
||||||
// 超时设置
|
// 超时设置
|
||||||
export const LAN_TRANSFER_HANDSHAKE_TIMEOUT_MS = 10_000; // 10秒
|
export const LAN_TRANSFER_HANDSHAKE_TIMEOUT_MS = 10_000; // 10秒
|
||||||
|
export const LAN_TRANSFER_CHUNK_TIMEOUT_MS = 30_000; // 30秒
|
||||||
export const LAN_TRANSFER_COMPLETE_TIMEOUT_MS = 60_000; // 60秒
|
export const LAN_TRANSFER_COMPLETE_TIMEOUT_MS = 60_000; // 60秒
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -702,7 +633,7 @@ export const LAN_TRANSFER_ALLOWED_MIME_TYPES = [
|
|||||||
|
|
||||||
## 9. 完整时序图
|
## 9. 完整时序图
|
||||||
|
|
||||||
### 9.1 完整传输流程(v3,流式传输)
|
### 9.1 完整传输流程(v1,流式传输)
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────┐ ┌─────────┐ ┌─────────┐
|
┌─────────┐ ┌─────────┐ ┌─────────┐
|
||||||
@ -727,7 +658,7 @@ export const LAN_TRANSFER_ALLOWED_MIME_TYPES = [
|
|||||||
│────────────────────────────────────>│ │
|
│────────────────────────────────────>│ │
|
||||||
│ │──────── TCP Connect ───────────────>│
|
│ │──────── TCP Connect ───────────────>│
|
||||||
│ │ │
|
│ │ │
|
||||||
│ │──────── handshake (v=3) ───────────>│
|
│ │──────── handshake ─────────────────>│
|
||||||
│ │ │
|
│ │ │
|
||||||
│ │<─────── handshake_ack ──────────────│
|
│ │<─────── handshake_ack ──────────────│
|
||||||
│ │ │
|
│ │ │
|
||||||
@ -744,20 +675,21 @@ export const LAN_TRANSFER_ALLOWED_MIME_TYPES = [
|
|||||||
│ │ │
|
│ │ │
|
||||||
│ │<─────── file_start_ack ─────────────│
|
│ │<─────── file_start_ack ─────────────│
|
||||||
│ │ │
|
│ │ │
|
||||||
│ │══════ 连续发送数据块(无确认)═══════│
|
│ │ │
|
||||||
|
│ │══════ 循环发送数据块 ═══════════════│
|
||||||
│ │ │
|
│ │ │
|
||||||
│ │──────── file_chunk[0] (binary) ────>│
|
│ │──────── file_chunk[0] (binary) ────>│
|
||||||
|
│<────── progress event ──────────────│ │
|
||||||
|
│ │ │
|
||||||
│ │──────── file_chunk[1] (binary) ────>│
|
│ │──────── file_chunk[1] (binary) ────>│
|
||||||
│ │──────── file_chunk[2] (binary) ────>│
|
│<────── progress event ──────────────│ │
|
||||||
│<────── progress event ──────────────│ ... 连续发送 ... │
|
│ │ │
|
||||||
│ │──────── file_chunk[N-1] (binary) ──>│
|
│ │ ... 重复 ... │
|
||||||
│ │ │
|
│ │ │
|
||||||
│ │══════════════════════════════════════│
|
│ │══════════════════════════════════════│
|
||||||
│ │ │
|
│ │ │
|
||||||
│ │──────── file_end ──────────────────>│
|
│ │──────── file_end ──────────────────>│
|
||||||
│ │ │
|
│ │ │
|
||||||
│ │ (接收端验证 checksum) │
|
|
||||||
│ │ │
|
|
||||||
│ │<─────── file_complete ──────────────│
|
│ │<─────── file_complete ──────────────│
|
||||||
│ │ │
|
│ │ │
|
||||||
│<────── complete event ──────────────│ │
|
│<────── complete event ──────────────│ │
|
||||||
@ -765,11 +697,9 @@ export const LAN_TRANSFER_ALLOWED_MIME_TYPES = [
|
|||||||
│ │ │
|
│ │ │
|
||||||
```
|
```
|
||||||
|
|
||||||
> **v3 特性**: 数据块连续发送,不等待单个确认,大幅提高传输速度。接收端在 `file_end` 后统一验证完整性。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 10. 移动端实现指南(v3 要点)
|
## 10. 移动端实现指南(v1 要点)
|
||||||
|
|
||||||
### 10.1 必须实现的功能
|
### 10.1 必须实现的功能
|
||||||
|
|
||||||
@ -791,21 +721,16 @@ export const LAN_TRANSFER_ALLOWED_MIME_TYPES = [
|
|||||||
|
|
||||||
4. **握手处理**
|
4. **握手处理**
|
||||||
|
|
||||||
- 验证 `handshake` 消息(version=3)
|
- 验证 `handshake` 消息
|
||||||
- 发送 `handshake_ack` 响应
|
- 发送 `handshake_ack` 响应
|
||||||
- 响应 `ping` 消息
|
- 响应 `ping` 消息
|
||||||
|
|
||||||
5. **文件接收(v3 流式模式)**
|
5. **文件接收(流式模式)**
|
||||||
|
|
||||||
- 解析 `file_start`,准备接收
|
- 解析 `file_start`,准备接收
|
||||||
- 接收 `file_chunk` 二进制帧,直接写入文件并增量计算哈希
|
- 接收 `file_chunk` 二进制帧,直接写入文件/缓冲并增量哈希
|
||||||
- **无需发送 `file_chunk_ack`**(v3 移除了逐块确认)
|
- v1 不发送 per-chunk ACK(流式传输)
|
||||||
- 处理 `file_end`,完成增量哈希并校验 checksum
|
- 处理 `file_end`,完成增量哈希并校验 checksum
|
||||||
- 发送 `file_complete` 结果(包含 errorCode、receivedChunks、receivedBytes)
|
- 发送 `file_complete` 结果
|
||||||
|
|
||||||
6. **取消处理**
|
|
||||||
- 监听 `file_cancel` 消息
|
|
||||||
- 清理临时文件和状态
|
|
||||||
|
|
||||||
### 10.2 推荐的库
|
### 10.2 推荐的库
|
||||||
|
|
||||||
@ -827,7 +752,7 @@ class FileReceiver {
|
|||||||
totalChunks: number;
|
totalChunks: number;
|
||||||
receivedChunks: number;
|
receivedChunks: number;
|
||||||
tempPath: string;
|
tempPath: string;
|
||||||
// v3: 边收边写文件,避免大文件 OOM
|
// v1: 边收边写文件,避免大文件 OOM
|
||||||
// stream: FileSystem writable stream (平台相关封装)
|
// stream: FileSystem writable stream (平台相关封装)
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -842,13 +767,10 @@ class FileReceiver {
|
|||||||
case "file_start":
|
case "file_start":
|
||||||
this.handleFileStart(message);
|
this.handleFileStart(message);
|
||||||
break;
|
break;
|
||||||
// v3: file_chunk 为二进制帧,不走 JSON 分支
|
// v1: file_chunk 为二进制帧,不再走 JSON 分支
|
||||||
case "file_end":
|
case "file_end":
|
||||||
this.handleFileEnd(message);
|
this.handleFileEnd(message);
|
||||||
break;
|
break;
|
||||||
case "file_cancel":
|
|
||||||
this.handleFileCancel(message);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -859,12 +781,12 @@ class FileReceiver {
|
|||||||
// 4. 发送 file_start_ack
|
// 4. 发送 file_start_ack
|
||||||
}
|
}
|
||||||
|
|
||||||
// v3: 二进制帧处理在 socket data 流中解析,随后调用 handleBinaryFileChunk
|
// v1: 二进制帧处理在 socket data 流中解析,随后调用 handleBinaryFileChunk
|
||||||
handleBinaryFileChunk(transferId: string, chunkIndex: number, data: Buffer) {
|
handleBinaryFileChunk(transferId: string, chunkIndex: number, data: Buffer) {
|
||||||
// 直接使用二进制数据,按 chunkSize/lastChunk 计算长度
|
// 直接使用二进制数据,按 chunkSize/lastChunk 计算长度
|
||||||
// 写入文件流并更新增量 SHA-256
|
// 写入文件流并更新增量 SHA-256
|
||||||
this.transfer.receivedChunks++;
|
this.transfer.receivedChunks++;
|
||||||
// v3: 无需发送 file_chunk_ack,连续接收数据块即可
|
// v1: 流式传输,不发送 per-chunk ACK
|
||||||
}
|
}
|
||||||
|
|
||||||
handleFileEnd(msg: LanTransferFileEndMessage) {
|
handleFileEnd(msg: LanTransferFileEndMessage) {
|
||||||
@ -922,13 +844,11 @@ export interface LanTransferFileStartMessage {
|
|||||||
chunkSize: number;
|
chunkSize: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// v3: file_chunk 以二进制帧传输,不是 JSON 消息
|
export interface LanTransferFileChunkMessage {
|
||||||
// 帧格式: Magic(2B) + TotalLen(4B) + Type(1B, 0x01) + TransferIdLen(2B) + TransferId(nB) + ChunkIndex(4B) + Data
|
type: "file_chunk";
|
||||||
export interface LanTransferFileChunkBinaryFrame {
|
|
||||||
type: 0x01; // 二进制帧类型标识 (file_chunk)
|
|
||||||
transferId: string;
|
transferId: string;
|
||||||
chunkIndex: number;
|
chunkIndex: number;
|
||||||
data: Buffer; // 原始二进制数据
|
data: string; // Base64 encoded (v1: 二进制帧模式下不使用)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LanTransferFileEndMessage {
|
export interface LanTransferFileEndMessage {
|
||||||
@ -936,12 +856,6 @@ export interface LanTransferFileEndMessage {
|
|||||||
transferId: string;
|
transferId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LanTransferFileCancelMessage {
|
|
||||||
type: "file_cancel";
|
|
||||||
transferId: string;
|
|
||||||
reason?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 文件传输响应消息 (Server -> Client)
|
// 文件传输响应消息 (Server -> Client)
|
||||||
export interface LanTransferFileStartAckMessage {
|
export interface LanTransferFileStartAckMessage {
|
||||||
type: "file_start_ack";
|
type: "file_start_ack";
|
||||||
@ -950,7 +864,14 @@ export interface LanTransferFileStartAckMessage {
|
|||||||
message?: string;
|
message?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// v3: 移除了 LanTransferFileChunkAckMessage(流式传输无需逐块确认)
|
// v1 流式不发送 per-chunk ACK,以下类型仅用于向后兼容参考
|
||||||
|
export interface LanTransferFileChunkAckMessage {
|
||||||
|
type: "file_chunk_ack";
|
||||||
|
transferId: string;
|
||||||
|
chunkIndex: number;
|
||||||
|
received: boolean;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface LanTransferFileCompleteMessage {
|
export interface LanTransferFileCompleteMessage {
|
||||||
type: "file_complete";
|
type: "file_complete";
|
||||||
@ -958,30 +879,18 @@ export interface LanTransferFileCompleteMessage {
|
|||||||
success: boolean;
|
success: boolean;
|
||||||
filePath?: string;
|
filePath?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
// v3 新增字段
|
|
||||||
errorCode?:
|
|
||||||
| "CHECKSUM_MISMATCH"
|
|
||||||
| "INCOMPLETE_TRANSFER"
|
|
||||||
| "DISK_ERROR"
|
|
||||||
| "CANCELLED";
|
|
||||||
receivedChunks?: number;
|
|
||||||
receivedBytes?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 常量
|
// 常量
|
||||||
export const LAN_TRANSFER_PROTOCOL_VERSION = "3";
|
|
||||||
export const LAN_TRANSFER_TCP_PORT = 53317;
|
export const LAN_TRANSFER_TCP_PORT = 53317;
|
||||||
export const LAN_TRANSFER_CHUNK_SIZE = 512 * 1024;
|
export const LAN_TRANSFER_CHUNK_SIZE = 512 * 1024;
|
||||||
export const LAN_TRANSFER_MAX_FILE_SIZE = 500 * 1024 * 1024;
|
export const LAN_TRANSFER_CHUNK_TIMEOUT_MS = 30_000;
|
||||||
// v3: 移除了 CHUNK_TIMEOUT_MS(流式传输无需逐块等待超时)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 附录 B:版本历史
|
## 附录 B:版本历史
|
||||||
|
|
||||||
| 版本 | 日期 | 变更 |
|
| 版本 | 日期 | 变更 |
|
||||||
| ---- | ------- | ------------------------------------------------------------------ |
|
| ---- | ------- | ---------------------------------------- |
|
||||||
| 1.0 | 2025-12 | 初始版本,与移动端实现对齐 |
|
| 1.0 | 2025-12 | 初始发布版本,支持二进制帧格式与流式传输 |
|
||||||
| 2.0 | 2025-12 | 引入二进制帧格式传输数据块,仍使用逐块 ACK 确认 |
|
|
||||||
| 3.0 | 2025-12 | 流式传输模式,移除 `file_chunk_ack`,增强 `file_complete` 错误诊断 |
|
|
||||||
|
|||||||
@ -162,7 +162,7 @@ export const LAN_TRANSFER_COMPLETE_TIMEOUT_MS = 60_000 // 60s - wait for file_co
|
|||||||
export const LAN_TRANSFER_GLOBAL_TIMEOUT_MS = 10 * 60 * 1000 // 10 minutes - global transfer timeout
|
export const LAN_TRANSFER_GLOBAL_TIMEOUT_MS = 10 * 60 * 1000 // 10 minutes - global transfer timeout
|
||||||
|
|
||||||
// Binary protocol constants (v3)
|
// Binary protocol constants (v3)
|
||||||
export const LAN_TRANSFER_PROTOCOL_VERSION = '3'
|
export const LAN_TRANSFER_PROTOCOL_VERSION = '1'
|
||||||
export const LAN_BINARY_FRAME_MAGIC = 0x4353 // "CS" as uint16
|
export const LAN_BINARY_FRAME_MAGIC = 0x4353 // "CS" as uint16
|
||||||
export const LAN_BINARY_TYPE_FILE_CHUNK = 0x01
|
export const LAN_BINARY_TYPE_FILE_CHUNK = 0x01
|
||||||
|
|
||||||
|
|||||||
@ -29,7 +29,7 @@ describe('connection handlers', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should use protocol version 3', () => {
|
it('should use protocol version 3', () => {
|
||||||
expect(HANDSHAKE_PROTOCOL_VERSION).toBe('3')
|
expect(HANDSHAKE_PROTOCOL_VERSION).toBe('1')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { app } from 'electron'
|
|||||||
|
|
||||||
import type { ConnectionContext } from '../types'
|
import type { ConnectionContext } from '../types'
|
||||||
|
|
||||||
export const HANDSHAKE_PROTOCOL_VERSION = '3'
|
export const HANDSHAKE_PROTOCOL_VERSION = '1'
|
||||||
|
|
||||||
const logger = loggerService.withContext('LanTransferConnection')
|
const logger = loggerService.withContext('LanTransferConnection')
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user