FTP 文件管理器怎么实现?从控制连接、数据连接到大文件传输架构
在前面的文章中,我们已经讨论了:
01 iOS 文件浏览器架构
02 GB 级大文件处理
03 Wi-Fi 局域网文件传输
04 SMB / NAS
05 WebDAV
这一篇继续补齐另一种非常经典的远程文件协议:
FTP
很多开发者第一次实现 FTP 时,会发现它和前面讲过的 WebDAV 很不一样。
WebDAV 很容易理解:
HTTP Request
↓
HTTP Response
比如:
GET
PUT
DELETE
MOVE
PROPFIND
大部分操作都围绕 HTTP Request / Response 展开。
FTP 则不同。
它的核心设计之一是:
控制命令和文件数据使用不同的连接。
也就是说,一个 FTP 客户端经常同时面对:
Control Connection
+
Data Connection
这也是理解 FTP 文件浏览器架构最重要的起点。
1. FTP 是什么?
FTP 全称:
File Transfer Protocol
它的目标非常明确:
在客户端和服务器之间传输文件。
典型场景:
iPhone
│
│ FTP
▼
FTP Server
│
├── Documents
├── Videos
├── Backup
└── Upload
用户可能输入:
Host:
192.168.1.100
Port:
21
Username:
user
Password:
********
连接以后就能执行:
- 浏览目录
- 下载文件
- 上传文件
- 删除文件
- 创建目录
- 重命名文件
- 移动文件
从产品层看,它和 SMB、WebDAV 都很像。
但是协议底层完全不同。
2. FTP 最大的特点:两条连接
普通 HTTP 很容易理解:
Client
│
Request
▼
Server
│
Response
▼
Client
FTP 不是这样。
FTP 通常存在:
┌─────────────────────┐
│ Control Connection │
└─────────────────────┘
+
┌─────────────────────┐
│ Data Connection │
└─────────────────────┘
可以理解为:
iPhone
│
├──── Control ──── FTP Server
│
└──── Data ─────── FTP Server
控制连接主要负责:
USER
PASS
PWD
CWD
TYPE
PASV
RETR
STOR
DELE
RNFR
RNTO
QUIT
数据连接负责:
目录列表
文件下载
文件上传
所以:
FTP 的命令和真正的大文件数据,不一定走同一条连接。
3. Control Connection 是做什么的?
FTP 客户端连接服务器后,通常首先建立控制连接。
例如:
Client
│
│ TCP
▼
Server : 21
服务器可能返回:
220 Service ready
客户端发送:
USER kitty
服务器返回:
331 Password required
客户端:
PASS ********
成功:
230 Login successful
整个认证过程都发生在:
Control Connection
上。
所以可以理解为:
Control Connection
│
├── Login
├── Change Directory
├── Delete
├── Rename
├── Prepare Download
└── Prepare Upload
4. FTP Reply Code 非常重要
和 HTTP 状态码类似,FTP 也有自己的数字响应码。
例如:
220
331
230
200
227
150
226
425
530
550
这些数字不是随便显示的。
例如:
220
通常表示服务准备就绪。
230
登录成功。
530
认证失败或未登录。
550
常见于文件不存在或权限问题。
所以 FTP Client 不能只是:
收到一行字符串
↓
看看有没有 "OK"
而应该建立:
FTP Reply Parser
例如:
struct FTPReply {
let code: Int
let message: String
}
然后:
Raw TCP Text
↓
FTPReplyParser
↓
Code + Message
↓
Business Error
5. FTP 目录浏览是怎么工作的?
用户进入:
/Documents
UI 想要的是:
report.pdf
photo.jpg
Projects/
video.mp4
但 FTP 客户端通常需要先告诉服务器:
我要获取目录列表。
可能涉及:
CWD /Documents
然后准备:
LIST
或者:
MLSD
但是目录数据不是直接在 Control Connection 上完整返回。
服务器会准备一条:
Data Connection
然后通过这条连接发送列表数据。
所以整个过程大致:
Control Connection
│
├── PASV
│
├── LIST
│
▼
Data Connection
│
▼
Directory Listing
这就是 FTP 比 HTTP 更容易让开发者困惑的地方。
6. LIST 和 MLSD 有什么区别?
FTP 很早就存在。
传统目录命令:
LIST
返回内容可能像:
-rw-r--r-- 1 user group 1024 Sep 10 12:00 report.pdf
drwxr-xr-x 2 user group 4096 Sep 09 18:00 Photos
问题在于:
LIST 的输出更偏“给人看”,格式可能因服务器不同而不同。
不同服务器可能返回:
Unix Style
Windows Style
Custom Style
解析非常麻烦。
更现代的方案是:
MLSD
它的目标更加机器友好。
例如概念上:
type=file;size=1024;modify=20260910120000; report.pdf
type=dir;modify=20260909180000; Photos
所以如果服务器支持:
MLSD 通常比 LIST 更适合程序解析。
7. FTP Provider 应该把协议模型转换成 FileItem
例如 MLSD 解析以后得到:
struct FTPEntry {
let name: String
let type: FTPEntryType
let size: Int64?
let modifiedDate: Date?
}
然后转换:
FTPEntry
↓
Mapper
↓
FileItem
最终 UI 看到的依然是:
struct FileItem {
let name: String
let isDirectory: Bool
let size: Int64?
let modifiedDate: Date?
let location: FileLocation
}
这和前面的:
Local
SMB
WebDAV
保持一致。
8. 什么是 Passive Mode?
如果实现 FTP,很快会遇到:
Active Mode
Passive Mode
现代客户端里更常见的是:
Passive Mode。
简单理解:
客户端先通过控制连接告诉服务器:
PASV
服务器返回:
我已经在某个地址和端口等你连接。
然后客户端主动创建:
Data Connection
例如:
Client
│
│ PASV
▼
Server
│
│ 227 Entering Passive Mode
▼
Client
│
└──── Connect Data Port ────► Server
这就是 Passive Mode。
9. 为什么 Passive Mode 对移动端更友好?
Active Mode 中,服务器需要反过来连接客户端。
但是 iPhone 往往处于:
Wi-Fi NAT
5G NAT
Firewall
Private Network
外部服务器未必能方便地反向连接 iPhone。
Passive Mode 则是:
iPhone
↓
主动连接服务器
对于:
- NAT
- Firewall
- 移动网络
通常更容易处理。
所以在移动端 FTP Client 中:
PASV / EPSV 是非常重要的能力。
10. EPSV 又是什么?
除了:
PASV
还有:
EPSV
也就是 Extended Passive Mode。
它更适合现代网络环境,尤其和 IPv6 兼容性有关。
概念上:
PASV
→ 返回 IP + Port
而:
EPSV
→ 更聚焦返回 Port
客户端可以继续使用当前服务器地址建立数据连接。
因此一个成熟 FTP Client 应该考虑:
EPSV
PASV
Fallback
而不是只支持一种固定模式。
11. 下载文件使用 RETR
假设用户下载:
movie.mp4
控制连接发送:
RETR movie.mp4
服务器可能返回:
150 Opening data connection
然后:
Data Connection
↓
File Bytes
↓
iPhone
传输完成:
226 Transfer complete
所以一个 FTP 下载任务包含两个维度:
Control State
+
Data Transfer State
例如:
RETR
↓
150
↓
Data Streaming
↓
EOF
↓
226
并不能只看数据连接关闭就认为:
一定成功。
还要结合控制连接的最终结果。
12. 大文件下载依然不能全部进内存
例如:
FTP Server
↓
30GB backup.zip
↓
iPhone
错误思路:
Data Connection
↓
30GB Data
↓
Memory
正确方式:
Data Connection
↓
Chunk
↓
Temporary File
↓
Chunk
↓
Temporary File
也就是说:
FTP
SMB
WebDAV
虽然协议完全不同,
但最后都应该进入相同的:
Large File Pipeline。
13. 下载进度如何计算?
首先需要尽量获取:
Remote File Size
例如:
SIZE movie.mp4
服务器可能返回:
213 10737418240
也就是:
10 GB
然后不断统计:
receivedBytes
进度:
let progress =
Double(receivedBytes) /
Double(totalBytes)
UI:
Downloading movie.mp4
6.2 GB / 10 GB
62%
所以 FTP Provider 需要提供:
Transfer Events
而不是让 UI 直接监听 Socket。
14. 上传文件使用 STOR
上传方向:
iPhone
↓
FTP Server
控制连接:
STOR report.zip
随后:
Local File
↓
Chunk
↓
Data Connection
↓
FTP Server
完成后服务器返回:
226
这和下载一样:
数据连接负责字节,控制连接负责协议状态。
15. FTP 上传大文件仍然应该使用 FileHandle
例如:
25GB archive.7z
不要:
let data = try Data(contentsOf: url)
而应该:
FileHandle
↓
Chunk
↓
Socket
↓
Chunk
↓
Socket
核心原则已经贯穿整个系列:
文件有多大,不应该让工作内存等比例增长。
16. FTP 能不能断点续传?
可以考虑使用:
REST
也就是 Restart。
例如一个 20GB 文件已经下载:
12GB
重新连接以后:
REST 12884901888
然后:
RETR movie.mp4
理论上可以从指定 Offset 继续。
流程:
Remote File 20GB
↓
Local Temp 12GB
↓
REST 12GB
↓
RETR
↓
Continue
这就是 FTP Resume 的基础。
17. Resume 不能只记 Offset
这和 SMB/WebDAV 一样。
假设:
昨天:
movie.mp4
20GB
下载了:
12GB
今天服务器上的:
movie.mp4
已经被替换。
如果直接:
REST 12GB
就可能得到:
Old File 前 12GB
+
New File 后 8GB
最终损坏。
所以恢复前应该尽可能验证:
Size
Modified Time
Remote Identifier
如果服务器能力允许,还可以加入其他校验信息。
原则:
Resume 的前提不是“Offset 还在”,而是“远程资源仍然是同一个”。
18. FTP Rename 怎么做?
FTP 通常通过两个命令:
RNFR
RNTO
例如:
RNFR report.pdf
服务器确认:
350 Ready for destination name
然后:
RNTO report-final.pdf
完成:
250 Rename successful
所以一个用户操作:
Rename
底层其实可能是:
RNFR
+
RNTO
上层 FileService 不需要关心这些细节。
19. 删除文件使用 DELE
例如:
DELE report.pdf
删除目录通常:
RMD Folder
创建目录:
MKD NewFolder
所以统一 FileProvider 接口:
func delete(_ item: FileItem) async throws
func createDirectory(...)
func rename(...)
在 FTP Provider 中分别映射成:
DELE
RMD
MKD
RNFR/RNTO
这和 WebDAV 映射成:
DELETE
MKCOL
MOVE
是完全相同的架构思想。
20. FTP Provider 不是简单包一层 Socket
如果只是:
FTPProvider
↓
Socket
很快会变复杂。
更合理的内部拆分:
FTPFileProvider
│
▼
FTPClient
│
├── ControlChannel
├── DataConnectionFactory
├── FTPReplyParser
├── DirectoryParser
└── TransferSession
分别负责:
ControlChannel
USER
PASS
CWD
RETR
STOR
DataConnectionFactory
负责:
PASV
EPSV
建立数据连接
FTPReplyParser
负责:
220
230
150
226
550
DirectoryParser
负责:
MLSD
LIST
TransferSession
负责:
Upload
Download
Progress
Cancel
Resume
这样职责更加清晰。
21. FTP 连接应该复用吗?
一般来说,控制连接可以在用户浏览同一个服务器期间复用。
例如:
Login
↓
List Folder A
↓
Enter Folder B
↓
Download File
↓
Rename File
如果每一步都:
Connect
Login
Disconnect
效率很差。
所以可以设计:
FTPConnectionManager
维护:
Disconnected
Connecting
Authenticated
Idle
Busy
Reconnecting
Failed
22. 但 FTP Server 可能主动断开连接
例如控制连接空闲太久:
421 Timeout
或者:
Connection Closed
所以:
Authenticated
不应该被理解成:
永远在线。
操作前可以检查:
Connection State
必要时:
Reconnect
↓
Authenticate
↓
Restore Working Directory
这就是为什么:
连接恢复应该是基础能力,而不是异常补丁。
23. Working Directory 是一个需要小心的状态
FTP 有:
PWD
和:
CWD
如果整个连接依赖“当前目录”:
CWD /Documents
然后多个异步任务共用同一控制连接:
Task A:
CWD /Videos
Task B:
RETR report.pdf
就可能出现非常危险的状态冲突。
因此:
不要随意让多个并发操作共享一个具有可变 Working Directory 的连接状态。
更安全的策略包括:
- 串行化命令
- 使用明确路径
- 每个 Transfer Session 独立连接
- 严格管理 Connection State
这属于典型的:
Shared Mutable State
问题。
24. 并发任务尤其要谨慎
假设同时:
Upload A
Download B
List C
Rename D
全部塞进一个控制连接:
很容易出现:
Command Response Mismatch
因为 FTP 控制协议本身是顺序交互型的。
所以可以设计:
FTPConnectionManager
│
├── Browse Connection
│
├── Transfer Connection A
│
└── Transfer Connection B
或者建立:
Connection Pool
控制:
Max Concurrent Transfers
而不是所有操作共用一条连接。
25. FTP 明文传输的问题
传统 FTP 有一个非常明显的问题:
Username
Password
Commands
Data
很多情况下都可能是:
明文传输。
如果通过不可信网络:
iPhone
↓
Public Wi-Fi
↓
FTP Server
安全风险很高。
所以现代文件客户端通常还需要考虑:
FTPS
26. FTPS 和 SFTP 不是一回事
这是非常容易混淆的地方。
FTP
FTP Protocol
FTPS
FTP
+
TLS
SFTP
不是 FTP + SSH 的简单写法。
SFTP 是:
SSH File Transfer Protocol
它运行在 SSH 体系上。
所以:
FTP ≠ SFTP
FTPS ≠ SFTP
如果产品界面支持多种协议,最好明确区分:
FTP
FTPS
SFTP
不要混为一谈。
27. FTP 的 Credential 同样应该进入 Keychain
和:
SMB
WebDAV
一样。
不要:
UserDefaults.standard.set(password)
而应该:
FTPServer
│
├── Host
├── Port
├── Username
└── CredentialID
│
▼
Keychain
这样统一:
CredentialStore
就可以服务:
SMB
WebDAV
FTP
28. FTP 的错误也要转换成业务错误
例如:
530 Login incorrect
用户真正应该看到:
用户名或密码错误。
550 File unavailable
可能显示:
文件不存在或没有访问权限。
425 Can't open data connection
可以提示:
无法建立文件传输连接,请检查服务器被动模式或网络设置。
也就是说:
FTP Reply
↓
Protocol Error
↓
RemoteFileError
↓
User Message
而不是:
Error 425
直接甩给用户。
29. 一个统一的 RemoteFileError 可以继续复用
例如:
enum RemoteFileError: Error {
case authenticationFailed
case permissionDenied
case fileNotFound
case serverUnavailable
case connectionLost
case transferFailed
case unsupportedFeature
case timeout
case insufficientStorage
case unknown(Error)
}
FTP:
530
映射:
authenticationFailed
WebDAV:
401
也映射:
authenticationFailed
SMB 的认证错误同样如此。
于是 UI 根本不需要知道:
530
401
SMB Status Code
它只处理:
authenticationFailed
这就是统一错误层的价值。
30. FTP 如何进入统一 FileProvider?
现在可以设计:
final class FTPFileProvider: FileProvider {
// FTP Client
}
最终:
FileProvider
│
├── LocalFileProvider
├── SMBFileProvider
├── WebDAVFileProvider
└── FTPFileProvider
UI:
FileBrowser
完全不用改成:
FTPFileBrowser
这非常重要。
因为用户根本不应该感受到:
你现在换协议了,所以整个操作体验都变了。
31. FTP → Local 应该是什么?
用户:
FTP Server
↓
movie.mp4
↓
Copy to iPhone
对于 UI:
Copy
底层:
FTP Provider
↓
RETR
↓
Data Stream
↓
Transfer Pipeline
↓
Local Provider
也就是:
FTP → Local
统一 Copy Operation。
32. Local → FTP 呢?
同理:
Local File
↓
Transfer Pipeline
↓
FTP STOR
↓
Server
用户依然只是:
Upload / Copy
于是协议差异全部留在:
Provider
内部。
33. FTP → WebDAV 怎么办?
这时候统一架构的优势更明显。
用户想把:
FTP Server A
上的:
backup.zip
复制到:
WebDAV Server B
最简单路线:
FTP
↓
iPhone
↓
WebDAV
架构:
FTP Provider
↓
Read Stream
↓
Transfer Pipeline
↓
Write Stream
↓
WebDAV Provider
用户完全不用关心:
RETR
PUT
只是:
Copy
34. Transfer Pipeline 应该支持背压
如果源 FTP 下载速度:
100 MB/s
目标 WebDAV 上传速度只有:
10 MB/s
如果系统不停读取:
FTP → Memory → Memory → Memory
最终缓存会不断增长。
所以真正的流式 Provider-to-Provider Transfer 需要:
Backpressure。
可以理解为:
Source
↓
Buffer
↓
Destination
如果 Destination 慢:
Source 也应该减速
而不是:
Memory 无限堆积
这其实已经进入真正的数据流系统设计了。
35. 一个完整 FTP 文件架构
可以整理成:
┌──────────────────────────────┐
│ UI │
│ File Browser / Task Center │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ FileService │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ FileOperationManager │
│ │
│ Copy │
│ Upload │
│ Download │
│ Retry │
│ Resume │
│ Progress │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ FTPFileProvider │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ FTPClient │
│ │
│ ControlChannel │
│ DataConnection │
│ ReplyParser │
│ DirectoryParser │
│ TransferSession │
└──────────────┬───────────────┘
│
▼
FTP Server
旁边还有:
CredentialStore
ConnectionManager
NetworkMonitor
RetryPolicy
ResumeMetadataStore
36. 到第六篇,远程文件架构已经基本完整
现在我们的 FileProvider 已经有:
Local
SMB
WebDAV
FTP
可以整理:
FileBrowser
│
▼
FileItem
│
▼
FileService
│
▼
FileOperationManager
│
▼
FileProvider
│
┌───────────────┼───────────────┐
▼ ▼ ▼
Local SMB WebDAV
│
▼
FTP
这几种协议完全不同。
但用户看到的仍然应该统一:
Open
Copy
Move
Rename
Delete
Download
Upload
这才是一个真正统一文件浏览器的价值。
37. TS File Explorer 为什么还需要 FTP?
从开发者角度看,FTP 是:
Control Connection
Data Connection
PASV
EPSV
RETR
STOR
REST
但是用户真正的问题可能只是:
“服务器上的文件,能不能直接在 iPhone 上下载?”
或者:
“手机里的文件,能不能直接上传到 FTP 服务器?”
例如:
FTP Server
↓
Website Backup
↓
backup.zip
用户可能只是希望:
打开 TS File Explorer
↓
FTP
↓
找到 backup.zip
↓
Download
所以和 SMB、WebDAV 一样:
FTP 对文件管理器真正增加的不是:
一个技术协议。
而是:
一个新的文件来源和文件目标。
38. 开发 FTP Client 前建议先回答这 18 个问题
如果准备实现自己的 FTP 文件模块,建议提前回答:
1. Control Connection 如何维护?
2. Reply Code 如何解析?
3. LIST 还是 MLSD?
4. 是否支持 PASV?
5. 是否支持 EPSV?
6. IPv6 怎么办?
7. Transfer 是否使用独立连接?
8. 多任务是否共用 Control Connection?
9. Working Directory 如何避免并发冲突?
10. 大文件是否 Streaming?
11. 是否支持 REST Resume?
12. Resume 前如何验证远程文件?
13. RETR/STOR 如何统计进度?
14. 连接断开如何恢复?
15. 认证信息如何保存?
16. FTP/FTPS/SFTP 是否明确区分?
17. FTP 如何映射到 FileProvider?
18. FTP 与其他 Provider 如何流式复制?
如果这些问题没有提前想清楚,
代码很容易变成:
Socket
+
大量字符串判断
+
大量页面特殊逻辑
最终很难扩展。
写在最后
FTP 是一个很适合用来理解“协议差异和业务抽象”的案例。
因为它和 WebDAV 完全不同:
WebDAV
↓
HTTP Request / Response
FTP:
Control Connection
+
Data Connection
但最终到了文件浏览器业务层:
两者都应该变成:
FileItem
+
FileProvider
+
FileOperation
这也是整个系列到目前为止最核心的一条原则:
协议可以不同,用户的文件操作体验应该统一。
目前我们已经走到:
01 iOS File Browser
↓
02 Large File IO
↓
03 Wi-Fi Transfer
↓
04 SMB / NAS
↓
05 WebDAV
↓
06 FTP
下一篇我建议不要继续堆协议。
而是进入一个更适合 TS File Explorer 搜索流量,同时又有真正技术含量的能力:
《iOS 如何实现 ZIP / RAR / 7z 解压?从压缩格式、流式解压到密码压缩架构》
因为压缩包几乎是文件浏览器最核心的用户场景之一。
下一篇可以系统讨论:
ZIP
RAR
7z
TAR
GZIP
之间到底有什么区别,以及文件管理 App 应该怎样设计统一的 ArchiveService。