Back to Cherry Studio

Cherry Studio LAN Transfer Protocol Specification

docs/references/lan-transfer-protocol.md

2.0.016.3 KB
Original Source

Cherry Studio LAN Transfer Protocol Specification

Version: 1.0 Last Updated: 2025-12

This document defines the LAN file transfer protocol between the Cherry Studio desktop client (Electron) and mobile client (Expo).


Table of Contents

  1. Protocol Overview
  2. Service Discovery (Bonjour/mDNS)
  3. TCP Connection and Handshake
  4. Message Format Specification
  5. File Transfer Protocol
  6. Heartbeat and Keep-alive
  7. Error Handling
  8. Constants and Configuration
  9. Complete Sequence Diagram
  10. Mobile Implementation Guide

1. Protocol Overview

1.1 Architecture Roles

RolePlatformResponsibility
ClientElectron DesktopScan services, initiate connections, send files
ServerExpo MobilePublish services, accept connections, receive files

1.2 Protocol Stack (v1)

┌─────────────────────────────────────┐
│     Application Layer (File Transfer)│
├─────────────────────────────────────┤
│     Message Layer (Control: JSON \n) │
│                   (Data: Binary Frame)│
├─────────────────────────────────────┤
│     Transport Layer (TCP)            │
├─────────────────────────────────────┤
│     Discovery Layer (Bonjour/mDNS)   │
└─────────────────────────────────────┘

1.3 Communication Flow Overview

1. Service Discovery → Mobile publishes mDNS service, Desktop scans and discovers
2. TCP Handshake → Establish connection, exchange device info (version=1)
3. File Transfer → Control messages use JSON, file_chunk uses binary frame chunked transfer
4. Keep-alive → ping/pong heartbeat

2. Service Discovery (Bonjour/mDNS)

2.1 Service Type

PropertyValue
Service Typecherrystudio
Protocoltcp
Full Service ID_cherrystudio._tcp

2.2 Service Publishing (Mobile)

Mobile must publish the service via mDNS/Bonjour:

typescript
{
  name: "Cherry Studio Mobile",
  type: "cherrystudio",
  protocol: "tcp",
  port: 53317,
  txt: {
    version: "1",
    platform: "ios"  // or "android"
  }
}

2.3 Service Discovery (Desktop)

Desktop scans and resolves service information:

typescript
type LanTransferPeer = {
  id: string;
  name: string;
  host?: string;
  fqdn?: string;
  port?: number;
  type?: string;
  protocol?: 'tcp' | 'udp';
  addresses: string[];
  txt?: Record<string, string>;
  updatedAt: number;
}

2.4 IP Address Selection Strategy

When a service has multiple IP addresses, prefer IPv4:

typescript
const preferredAddress = addresses.find((addr) => isIPv4(addr)) || addresses[0]

3. TCP Connection and Handshake

3.1 Connection Establishment

  1. Client establishes TCP connection using the discovered host:port
  2. Immediately sends a handshake message upon connection
  3. Waits for server handshake acknowledgment

3.2 Handshake Messages (Protocol Version v1)

Client → Server: handshake

typescript
type LanTransferHandshakeMessage = {
  type: 'handshake';
  deviceName: string;
  version: string;     // Protocol version, currently "1"
  platform?: string;   // 'darwin' | 'win32' | 'linux'
  appVersion?: string;
}

4. Message Format Specification (Mixed Protocol)

v1 uses a "control JSON + binary data frame" mixed protocol (streaming mode, no per-chunk ACK):

  • Control messages (handshake, heartbeat, file_start/ack, file_end, file_complete): UTF-8 JSON, \n delimited
  • Data messages (file_chunk): Binary frames using Magic + total length for framing, no Base64

4.1 Control Message Encoding (JSON + \n)

PropertySpecification
EncodingUTF-8
SerializationJSON
Message Delimiter\n (0x0A)

4.2 file_chunk Binary Frame Format

To solve TCP packet splitting/merging and eliminate Base64 overhead, file_chunk uses binary frames with total length:

┌──────────┬──────────┬────────┬───────────────┬──────────────┬────────────┬───────────┐
│ Magic    │ TotalLen │ Type   │ TransferId Len│ TransferId   │ ChunkIdx   │ Data      │
│ 0x43 0x53│ (4B BE)  │ 0x01   │ (2B BE)       │ (UTF-8)      │ (4B BE)    │ (raw)     │
└──────────┴──────────┴────────┴───────────────┴──────────────┴────────────┴───────────┘
FieldSizeDescription
Magic2BConstant 0x43 0x53 ("CS"), distinguishes from JSON messages
TotalLen4BBig-endian, total frame length (excluding Magic/TotalLen)
Type1B0x01 for file_chunk
TransferId Len2BBig-endian, transferId string length
TransferIdnBUTF-8 transferId (length from previous field)
ChunkIdx4BBig-endian, chunk index starting from 0
DatamBRaw file binary data (unencoded)

Total frame length calculation: TotalLen = 1 + 2 + transferIdLen + 4 + dataLen

4.3 Message Parsing Strategy

  1. Read socket data into buffer
  2. If first two bytes are 0x43 0x53 → parse as binary frame
  3. Else if first byte is { → parse as JSON + \n control message
  4. Otherwise discard 1 byte and continue loop

4.4 Message Type Summary (v1)

TypeDirectionEncodingPurpose
handshakeClient → ServerJSON+\nHandshake request (version=1)
handshake_ackServer → ClientJSON+\nHandshake response
pingClient → ServerJSON+\nHeartbeat request
pongServer → ClientJSON+\nHeartbeat response
file_startClient → ServerJSON+\nStart file transfer
file_start_ackServer → ClientJSON+\nFile transfer acknowledgment
file_chunkClient → ServerBinaryFile data chunk (no Base64, streaming, no per-chunk ACK)
file_endClient → ServerJSON+\nFile transfer end
file_completeServer → ClientJSON+\nTransfer completion result

5. File Transfer Protocol

5.1 Transfer Flow

Client (Sender)                     Server (Receiver)
     |                                    |
     |──── 1. file_start ────────────────>|
     |                                    |
     |<─── 2. file_start_ack ─────────────|
     |                                    |
     |══════ Loop: send data chunks ══════|
     |                                    |
     |──── 3. file_chunk [0] ────────────>|
     |──── 3. file_chunk [1] ────────────>|
     |      ... repeat until all sent ... |
     |                                    |
     |──── 5. file_end ──────────────────>|
     |                                    |
     |<─── 6. file_complete ──────────────|

5.2 Message Definitions

5.2.1 file_start

typescript
type LanTransferFileStartMessage = {
  type: 'file_start';
  transferId: string;    // UUID, unique transfer identifier
  fileName: string;
  fileSize: number;
  mimeType: string;
  checksum: string;      // SHA-256 hash of entire file (hex)
  totalChunks: number;
  chunkSize: number;
}

5.2.2 file_start_ack

typescript
type LanTransferFileStartAckMessage = {
  type: 'file_start_ack';
  transferId: string;
  accepted: boolean;
  message?: string;      // Rejection reason
}

5.2.3 file_chunk — Binary Frame

See section 4.2 for frame format. Data is raw file binary data. Integrity relies on file_start.checksum (full file SHA-256).

5.2.4 file_end

typescript
type LanTransferFileEndMessage = {
  type: 'file_end';
  transferId: string;
}

5.2.5 file_complete

typescript
type LanTransferFileCompleteMessage = {
  type: 'file_complete';
  transferId: string;
  success: boolean;
  filePath?: string;     // Save path (on success)
  error?: string;        // Error message (on failure)
}

5.3 Checksum

typescript
async function calculateFileChecksum(filePath: string): Promise<string> {
  const hash = crypto.createHash('sha256')
  const stream = fs.createReadStream(filePath)
  for await (const chunk of stream) {
    hash.update(chunk)
  }
  return hash.digest('hex')
}

5.4 Chunk Size

typescript
const CHUNK_SIZE = 512 * 1024 // 512KB
const totalChunks = Math.ceil(fileSize / CHUNK_SIZE)

6. Heartbeat and Keep-alive

6.1 Messages

  • ping (Client → Server): { type: 'ping', payload?: string }
  • pong (Server → Client): { type: 'pong', received: boolean, payload?: string }

6.2 Strategy

  • Send ping immediately after successful handshake to verify connection
  • Optional: periodically send heartbeats to keep the connection alive

7. Error Handling

7.1 Timeout Configuration

OperationTimeoutDescription
TCP Connection10sConnection establishment timeout
Handshake10sWaiting for handshake_ack
Transfer Complete60sWaiting for file_complete

7.2 Error Scenarios

ScenarioClient HandlingServer Handling
TCP connection failureNotify UI, allow retry-
Handshake timeoutDisconnect, notify UIClose socket
Handshake rejectedShow rejection reason-
Chunk processing failureAbort transfer, cleanupClean up temp files
Unexpected disconnectCleanup state, notify UIClean up temp files
Insufficient storage-Send accepted: false

8. Constants and Configuration

typescript
export const LAN_TRANSFER_PROTOCOL_VERSION = '1'
export const LAN_TRANSFER_SERVICE_TYPE = 'cherrystudio'
export const LAN_TRANSFER_SERVICE_FULL_NAME = '_cherrystudio._tcp'
export const LAN_TRANSFER_TCP_PORT = 53317
export const LAN_TRANSFER_CHUNK_SIZE = 512 * 1024         // 512KB
export const LAN_TRANSFER_GLOBAL_TIMEOUT_MS = 10 * 60 * 1000  // 10 minutes
export const LAN_TRANSFER_HANDSHAKE_TIMEOUT_MS = 10_000
export const LAN_TRANSFER_CHUNK_TIMEOUT_MS = 30_000
export const LAN_TRANSFER_COMPLETE_TIMEOUT_MS = 60_000

export const LAN_TRANSFER_ALLOWED_EXTENSIONS = ['.zip']
export const LAN_TRANSFER_ALLOWED_MIME_TYPES = ['application/zip', 'application/x-zip-compressed']

9. Complete Sequence Diagram

┌─────────┐                           ┌─────────┐                           ┌─────────┐
│ Renderer│                           │  Main   │                           │ Mobile  │
│  (UI)   │                           │ Process │                           │ Server  │
└────┬────┘                           └────┬────┘                           └────┬────┘
     │                                     │                                     │
     │  ═══════ Service Discovery ═════════                                      │
     │ startScan()                         │                                     │
     │────────────────────────────────────>│ mDNS browse                         │
     │                                     │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─>│
     │                                     │<─ ─ ─ service discovered ─ ─ ─ ─ ─ ─│
     │<────── onServicesUpdated ───────────│                                     │
     │                                     │                                     │
     │  ═══════ Handshake ════════════════                                       │
     │ connect(peer)                       │                                     │
     │────────────────────────────────────>│──────── TCP Connect ───────────────>│
     │                                     │──────── handshake ─────────────────>│
     │                                     │<─────── handshake_ack ──────────────│
     │                                     │──────── ping ──────────────────────>│
     │                                     │<─────── pong ───────────────────────│
     │<────── connect result ──────────────│                                     │
     │                                     │                                     │
     │  ═══════ File Transfer ════════════                                       │
     │ sendFile(path)                      │                                     │
     │────────────────────────────────────>│──────── file_start ────────────────>│
     │                                     │<─────── file_start_ack ─────────────│
     │                                     │──────── file_chunk[0] (binary) ────>│
     │<────── progress event ──────────────│                                     │
     │                                     │──────── file_chunk[1] (binary) ────>│
     │<────── progress event ──────────────│         ... repeat ...              │
     │                                     │──────── file_end ──────────────────>│
     │                                     │<─────── file_complete ──────────────│
     │<────── complete event ──────────────│                                     │

10. Mobile Implementation Guide (v1)

10.1 Required Features

  1. mDNS Service Publishing: Publish _cherrystudio._tcp service on TCP port 53317
  2. TCP Server: Listen on the specified port
  3. Message Parsing: Control messages via UTF-8 + \n JSON; data messages via binary frames (Magic+TotalLen framing)
  4. Handshake Handling: Validate handshake, send handshake_ack, respond to ping
  5. File Receiving (Streaming): Parse file_start, receive file_chunk binary frames (write to file + incremental hash), process file_end, send file_complete

React Native / Expo:

  • mDNS: react-native-zeroconf or @homielab/react-native-bonjour
  • TCP: react-native-tcp-socket
  • Crypto: expo-crypto or react-native-quick-crypto

Appendix A: TypeScript Type Definitions

Complete type definitions are located in src/shared/types/lanTransfer.ts. See the source code for the full interface definitions.

Appendix B: Version History

VersionDateChanges
1.02025-12Initial release with binary frame format and streaming transfer