ftp 命令详解与参数实测(Ubuntu 22.04)
ftp(tnftp)逐参数实测详解:-n/-v/-i/-g 四选项与 user/open/close/ls/dir/lcd/cd/get/put/mget/mput/delete/rename/mkdir/rmdir/binary/ascii/prompt/status 交互命令族全流程,pyftpdlib 本地服务端 220/230/226 响应码逐行解读,binary 与 ascii 模式 md5 对照实证,含通配符 550、Password 吞行等踩坑与明文风险替代方案,全部回显在 Ubuntu 22.04 实测通过。
ftp 命令详解与参数实测(Ubuntu 22.04)
目标:全面掌握 ftp 的每一个常用参数与交互命令——
-n/-v/-i/-g四个客户端选项、登录与目录/传输/维护三类交互命令族(user/open/close/ls/dir/lcd/cd/get/put/mget/mput/delete/rename/mkdir/rmdir/binary/ascii/prompt/status/bye/quit)、binary 与 ascii 传输模式的 md5 对照实证,以及通配符展开依赖服务端、非交互下 Password 吞行等真实踩坑,全部命令与回显已在 Ubuntu 22.04 实测通过。
一、环境信息
- 操作系统:Ubuntu 22.04.5 LTS(x86_64)
- ftp 实现:tnftp(NetBSD ftp 增强版),
/usr/bin/ftp经 alternatives 指向/usr/bin/tnftp - ftp 包版本:20210827-4build1(
ftp包本身是过渡包,真实程序由tnftp包提供,见 2.3) - FTP 演示服务端:pyftpdlib 2.1.0(pip3 安装),监听 127.0.0.1:2121,根目录
/root/demo/ftpshare,匿名可读写 - 本地演示目录:
/root/demo/ftp(上传源文件与下载落盘目录) - 系统时区:UTC(回显中的时间即 UTC)
Ubuntu 22.04 默认不带 ftp 客户端,需先安装。安装过程实测(节选):
$ apt-get install -y ftp
Setting up tnftp (20210827-4build1) ...
update-alternatives: using /usr/bin/tnftp to provide /usr/bin/ftp (ftp) in auto mode
Setting up ftp (20210827-4build1) ...
注意第一行与 alternatives 提示:名为 ftp 的包实际安装的是 tnftp,并通过 alternatives 把 /usr/bin/ftp 软链接到 /usr/bin/tnftp。
演示用的 FTP 服务端用 pyftpdlib 搭建(一行命令起一个匿名读写服务)。安装与启动实测:
$ pip3 install pyftpdlib
Successfully built pyftpdlib
Installing collected packages: pyftpdlib
Successfully installed pyftpdlib-2.1.0
$ mkdir -p /root/demo/ftpshare/pub /root/demo/ftp
$ python3 -m pyftpdlib -p 2121 -d /root/demo/ftpshare -w
-p 2121 指定监听端口(避开特权端口)、-d 指定 FTP 根目录、-w 授予匿名用户写权限(不加则匿名只读,启动日志会给出 RuntimeWarning 提示写权限已授予)。启动后服务端日志(节选):
[I 2026-10-16 00:33:23] concurrency model: async
[I 2026-10-16 00:33:23] masquerade (NAT) address: None
[I 2026-10-16 00:33:23] passive ports: None
[I 2026-10-16 00:33:23] >>> starting FTP server on 0.0.0.0:2121, pid=23222 <<<
passive ports: None 表示被动模式端口由内核临时分配;端口监听验证:
$ ss -tln | grep ':2121 '
LISTEN 0 100 0.0.0.0:2121 0.0.0.0:*
二、命令概览
2.1 语法与参数总览
ftp 是交互式文件传输客户端:连接 FTP 服务端后进入 ftp> 命令行,用 get/put 等命令在本地与远端之间传文件。参数总览(本篇实测覆盖的四个选项 + 常用参数,均出自 usage 实测):
| 参数 | 作用 | 本篇实测节 |
|---|---|---|
host [port] | 启动时直接连接远端(port 缺省 21) | 3.2 |
-n | 不自动登录(抑制启动时的自动 login,配合 user 命令手工登录) | 3.1 |
-v | 显示全部响应(包括服务端各应答码与传输统计) | 3.2 |
-i | 多文件传输时关闭交互确认提示(mget/mput 不再逐个询问) | 3.3 |
-g | 关闭文件名展开(通配符按字面处理) | 3.4 |
-4/-6 | 强制 IPv4/IPv6 | —(与 telnet 同理) |
💡 技术拓展:FTP 的数据连接有「主动(PORT)」与「被动(PASV)」两种建立方式,客户端选项只影响控制行为,数据连接方式由客户端配置与服务端能力协商——
模式 谁发起数据连接 典型问题 主动 PORT 服务端主动连客户端指定端口 客户端侧防火墙/NAT 挡住入站 被动 PASV 客户端连服务端协商出的端口 服务端需开放被动端口范围 增强被动 EPSV PASV 的 IPv6 友好升级版,单端口参数 老服务端不支持 本环境实测采用被动族协议:tnftp 默认
Passive mode: on,且对 pyftpdlib 发出的是 EPSV(回显229 Entering extended passive mode,见 3.2),这正是 status 输出中Use of EPSV/EPRT cmds for IPv4: on的效果。内网排障时若数据连接卡死,优先怀疑被动端口范围未放行。
2.2 帮助形态:--help 的实测回显
tnftp 不认识 --help 长选项,传入后会按 usage 报错——这份 usage 就是它的帮助,完整照抄:
$ ftp --help
ftp: invalid option -- '-'
usage: ftp [-46AadefginpRtVv] [-N NETRC] [-o OUTPUT] [-P PORT] [-q QUITTIME]
[-r RETRY] [-s SRCADDR] [-T DIR,MAX[,INC]] [-x XFERSIZE]
[[USER@]HOST [PORT]]
[[USER@]HOST:[PATH][/]]
[file:///PATH]
[ftp://[USER[:PASSWORD]@]HOST[:PORT]/PATH[/][;type=TYPE]]
[http://[USER[:PASSWORD]@]HOST[:PORT]/PATH]
[https://[USER[:PASSWORD]@]HOST[:PORT]/PATH]
...
ftp -u URL FILE ...
ftp -?
usage 显示 tnftp 除了传统 HOST [PORT] 形态,还支持 URL 直取(ftp://、http://、https://、file://)与 ftp -u URL FILE 上传形态,是一个「ftp + 小型取文件器」合体客户端。本文聚焦传统交互用法。
2.3 版本信息
ftp 自身无 --version 选项,版本从包管理器与 status 命令两处确认:
$ which ftp; readlink -f $(which ftp)
/usr/bin/ftp
/usr/bin/tnftp
$ dpkg -S $(readlink -f $(which ftp))
tnftp: /usr/bin/tnftp
$ dpkg -l ftp tnftp | tail -2
ii ftp 20210827-4build1 all dummy transitional package for tnftp
ii tnftp 20210827-4build1 amd64 enhanced ftp client
$ apt policy ftp 2>/dev/null | head -3
ftp:
Installed: 20210827-4build1
Candidate: 20210827-4build1
ftp 包明确标注 dummy transitional package for tnftp——真实客户端是 tnftp 20210827-4build1。交互会话中 status 命令也会给出程序版本(见 3.5):Version: tnftp 20210827。
三、参数逐项实测
本章演示数据:服务端根目录 /root/demo/ftpshare 下放 demo.txt 与 pub/readme.txt;本地目录 /root/demo/ftp 下放上传源文件。以下会话均通过管道喂入命令(非交互场景),这也是最能暴露「自动化陷阱」的实测方式:
$ printf 'hello ftp demo line1\nline2\n' > /root/demo/ftpshare/demo.txt
$ printf 'in pub dir\n' > /root/demo/ftpshare/pub/readme.txt
$ printf 'local file for upload\n' > /root/demo/ftp/up.txt
3.1 -n:不自动登录(配合 user 命令)
ftp 启动连接后默认按 ~/.netrc 自动登录;-n 关闭该行为,登录动作交给会话内的 user 命令。先看一个实测出的陷阱——user anonymous 不带内联密码时,非交互(stdin 非终端)场景下 Password: 提示会从 stdin 吞掉下一行命令:
$ printf 'user anonymous\ndir\nget demo.txt\nbye\n' | ftp -n 127.0.0.1 2121
Password:
会话只回显了 Password:,dir/get 都没执行——第二行 dir 被当作密码吃掉了。规范写法是把密码内联在 user 命令里(user 用户名 密码):
$ printf 'user anonymous ftpdemo\npwd\nsystem\nbye\n' | ftp -n -v 127.0.0.1 2121
Connected to 127.0.0.1.
220 pyftpdlib 2.1.0 ready.
331 Username ok, send password.
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
Remote directory: /
215 UNIX Type: L8
221 Goodbye.
user anonymous ftpdemo 一次喂全用户名与密码,登录立即成功(230)。pyftpdlib 的匿名登录对任意密码放行,ftpdemo 只是示例口令。
💡 要点:脚本化 ftp 一律
-n+user 用户名 密码内联形式;把密码单独占一行会给非交互场景埋雷(见五、Q2)。
3.2 -v:显示全部响应
不加 -v 时 tnftp 静默执行;-v 把每个服务端应答码与传输统计全部打出来,是解读 FTP 协议过程的关键开关。全流程实测(连接 → 登录 → 列目录 → 下载 → 上传 → 列目录 → 退出):
$ printf 'user anonymous ftpdemo\nlcd /root/demo/ftp\ndir\nget demo.txt local-demo.txt\nput up.txt\nls\nbye\n' | ftp -n -v 127.0.0.1 2121
Connected to 127.0.0.1.
220 pyftpdlib 2.1.0 ready.
331 Username ok, send password.
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
Local directory now: /root/demo/ftp
229 Entering extended passive mode (|||51315|).
125 Data connection already open. Transfer starting.
-rw-r--r-- 1 root root 27 Oct 16 00:33 demo.txt
drwxr-xr-x 2 root root 4096 Oct 16 00:33 pub
226 Transfer complete.
local: local-demo.txt remote: demo.txt
229 Entering extended passive mode (|||52439|).
125 Data connection already open. Transfer starting.
226 Transfer complete.
27 bytes received in 00:00 (507.06 KiB/s)
local: up.txt remote: up.txt
229 Entering extended passive mode (|||40221|).
125 Data connection already open. Transfer starting.
226 Transfer complete.
22 bytes sent in 00:00 (79.86 KiB/s)
229 Entering extended passive mode (|||32809|).
125 Data connection already open. Transfer starting.
-rw-r--r-- 1 root root 27 Oct 16 00:33 demo.txt
drwxr-xr-x 2 root root 4096 Oct 16 00:33 pub
-rw-r--r-- 1 root root 22 Oct 16 00:33 up.txt
226 Transfer complete.
221 Goodbye.
逐行解读这份「FTP 协议教科书」:
220 pyftpdlib 2.1.0 ready.——服务端问候(220 = 服务就绪),顺带暴露了服务端实现与版本。331 Username ok, send password.——用户名受理,等密码(331)。230 Login successful.——登录成功(230)。Remote system type is UNIX.+Using binary mode to transfer files.——客户端探测到远端系统类型,默认进入 binary 传输模式。Local directory now: /root/demo/ftp——lcd切换本地目录成功。- 每次列目录/传输前都有
229 Entering extended passive mode (|||端口|).——EPSV 被动模式协商数据连接端口(2.1 的拓展说明)。 125 Data connection already open. Transfer starting.——数据连接就绪开始传输(125);列目录/传文件完成后226 Transfer complete.(226 = 关闭数据连接,传输成功)。local: local-demo.txt remote: demo.txt+27 bytes received...——get 的本地/远端名映射与速率统计;put 对应22 bytes sent...。221 Goodbye.——服务端确认退出(221)。
登录后服务端可见刚上传的 up.txt(22 字节),与本地一致。落盘验证:
$ ls -l /root/demo/ftp/local-demo.txt /root/demo/ftp/up.txt; head -1 /root/demo/ftp/local-demo.txt
-rw-r--r-- 1 root root 27 Oct 16 00:33 /root/demo/ftp/local-demo.txt
-rw-r--r-- 1 root root 22 Oct 16 00:33 /root/demo/ftp/up.txt
hello ftp demo line1
local-demo.txt 27 字节、内容与服务端 demo.txt 一致——下载真实落盘。
💡 要点:
-v的价值在「看到服务端真实回复码」——331/230(登录)、226(传输完成)、550(文件不存在)等三位码是 FTP 的通用语言,排障时-v回显可直接对照 RFC 959 判定问题出在认证、路径还是传输。脚本化场景用nlist等机器友好命令即可,人排障时必开-v。
3.3 -i:多文件传输关闭交互提示
mget/mput 逐文件询问 [anpqy?]? 是交互好手、脚本天敌;-i 从命令行层面关掉提示。批量上传三个文件实测:
$ printf 'user anonymous ftpdemo\nbinary\nmput m1.txt m2.txt m3.txt\nls\nbye\n' | ftp -n -v -i 127.0.0.1 2121
Connected to 127.0.0.1.
220 pyftpdlib 2.1.0 ready.
331 Username ok, send password.
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
200 Type set to: Binary.
local: m1.txt remote: m1.txt
229 Entering extended passive mode (|||55137|).
125 Data connection already open. Transfer starting.
226 Transfer complete.
9 bytes sent in 00:00 (75.76 KiB/s)
local: m2.txt remote: m2.txt
229 Entering extended passive mode (|||35033|).
125 Data connection already open. Transfer starting.
226 Transfer complete.
9 bytes sent in 00:00 (81.37 KiB/s)
local: m3.txt remote: m3.txt
229 Entering extended passive mode (|||42045|).
125 Data connection already open. Transfer starting.
226 Transfer complete.
11 bytes sent in 00:00 (86.62 KiB/s)
229 Entering extended passive mode (|||50683|).
125 Data connection already open. Transfer starting.
-rw-r--r-- 1 root root 9 Oct 16 00:33 m1.txt
-rw-r--r-- 1 root root 9 Oct 16 00:33 m2.txt
-rw-r--r-- 1 root root 11 Oct 16 00:33 m3.txt
drwxr-xr-x 2 root root 4096 Oct 16 00:33 pub
-rw-r--r-- 1 root root 22 Oct 16 00:33 up.txt
226 Transfer complete.
221 Goodbye.
三个文件逐个上传,全程零询问。会话内的 prompt 命令可达到同样效果(且能动态切换),实测回显:
$ printf 'user anonymous ftpdemo\nprompt\nmput m*.txt\nbye\n' | ftp -n 127.0.0.1 2121
Interactive mode off.
Interactive mode off. 即提示已关闭,之后的 mput 不再询问。
💡 要点:
-i与prompt二选一即可;脚本固定用-i,交互中临时批量用prompt(再敲一次prompt恢复询问)。
3.4 -g:关闭文件名展开(通配符按字面处理)
-g 让 mget/mput 的参数不再做通配展开,按字面文件名处理。实测对不存在的字面名 demo*.txt 执行 mget:
$ printf 'user anonymous ftpdemo\nmget demo*.txt\nbye\n' | ftp -n -g 127.0.0.1 2121
mget demo*.txt [anpqy?]? No such file or directory.
两处信息:mget demo*.txt [anpqy?]? 是多文件传输的逐文件询问提示(本会话未加 -i,故仍有提示;stdin 来的下一行被当作应答);随后 No such file or directory. 说明 demo*.txt 被按字面查找(-g 生效——星号不再当通配符),自然找不到。
💡 要点:
-g是客户端侧开关,但它能否「按字面找文件」还取决于服务端是否支持 LIST 通配(见 3.7 节实测)。日常批传输保持默认 glob 展开即可;要传「名字里真带星号」的奇葩文件时才需要-g,属于罕见但要知道的保命开关。
3.5 交互命令族:open / close / user / status
不带 host 参数启动(ftp -n),用 open 命令建立连接,用 close 挂断但留在 ftp 提示符内——适合一台客户端轮询多个服务端。实测把 3.1~3.4 用过的目录/维护命令串起来(本节先演示连接生命周期):
$ printf 'open 127.0.0.1 2121\nuser anonymous ftpdemo\nmkdir demo2026\ncd demo2026\npwd\ncdup\npwd\nclose\nquit\n' | ftp -n -v
Connected to 127.0.0.1.
220 pyftpdlib 2.1.0 ready.
331 Username ok, send password.
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
257 "/demo2026" directory created.
250 "/demo2026" is the current directory.
Remote directory: /
250 "/" is the current directory.
Remote directory: /
221 Goodbye.
解读:mkdir demo2026 返回 257 "/demo2026" directory created.(257 = 路径创建成功);cd 后 250 "/demo2026" is the current directory.;pwd 回显 Remote directory: /demo2026;cdup 回到根(Remote directory: /);close 挂断连接(本会话随后 quit 退出,221 由服务端发出)。若不退出而继续 open 另一台主机,即可在同一个 ftp 进程里切换服务端。
status 是排障第一站,一条命令看尽当前配置:
$ printf 'user anonymous ftpdemo\nstatus\nbye\n' | ftp -n 127.0.0.1 2121
Connected and logged into 127.0.0.1.
No proxy connection.
Gate ftp: off, server (none), port ftpgate.
Passive mode: on; fallback to active mode: on.
Mode: stream; Type: binary; Form: non-print; Structure: file.
Verbose: off; Bell: off; Prompting: on; Globbing: on.
Store unique: off; Receive unique: off.
Preserve modification times: on.
Case: off; CR stripping: on.
Ntrans: off.
Nmap: off.
Hash mark printing: off; Mark count: 1024; Progress bar: off.
Get transfer rate throttle: off; maximum: 0; increment 1024.
Put transfer rate throttle: off; maximum: 0; increment 1024.
Socket buffer sizes: send 16384, receive 131072.
Use of PORT cmds: on.
Use of EPSV/EPRT cmds for IPv4: on.
Use of EPSV/EPRT cmds for IPv6: on.
Command line editing: off.
Version: tnftp 20210827
关键行解读:Passive mode: on; fallback to active mode: on.——默认被动模式,失败自动回退主动;Type: binary——当前传输模式;Verbose: off; Prompting: on; Globbing: on.——三个可被 -v/-i/-g(或 prompt/glob 命令)翻转的开关及其默认值;CR stripping: on——ascii 模式下回车剥离开关(与 3.8 直接相关);末行 Version: tnftp 20210827——版本号来源之一。
💡 要点:
status一条命令把「当前在哪、什么模式、什么开关」全量铺开,是接手别人会话的第一动作;user 命令支持不断开重登录(换身份继续用同一控制连接)。这四个命令都不碰文件,纯会话管理,放心随手敲。
3.6 目录与文件维护族:ls / dir / nlist / rename / delete / rmdir
列目录三兄弟的差别:ls 精简列表、dir 详细长列表(权限/属主/大小/时间,即 3.2 看到的格式)、nlist 仅文件名(无总量的裸列表)。维护命令实测(接 3.5 创建的 demo2026 与已有 demo.txt):
$ printf 'user anonymous ftpdemo\nrename demo.txt demo-renamed.txt\ndelete demo-renamed.txt\nrmdir demo2026\nbye\n' | ftp -n -v 127.0.0.1 2121
Connected to 127.0.0.1.
220 pyftpdlib 2.1.0 ready.
331 Username ok, send password.
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
350 Ready for destination name.
250 Renaming ok.
250 File removed.
250 Directory removed.
221 Goodbye.
解读:rename 是两段式——先 350 Ready for destination name.(服务端等待新名字),确认后 250 Renaming ok.;delete 返回 250 File removed.;rmdir 返回 250 Directory removed.。nlist 的裸列表形态实测(另一会话,远端此时还剩 m1/m2/m3 等文件):
m1.txt
m2.txt
m3.txt
mixed.bin
mixed_a.bin
pub
up.txt
年度报告.txt
nlist 输出没有权限/大小列,适合脚本逐行解析。lcd(切本地目录)在 3.2 已实测:Local directory now: /root/demo/ftp。
💡 要点:
ls/dir输出给人类看,nlist输出给脚本看(裸文件名、一行一个)——批量下载「先 nlist 拿名单再逐个 get」是最稳的自动化模式。rename的两段式(350 等新名 → 250 完成)在-v下可见,脚本里看到 350 不是卡住,是正常中间态。
3.7 传输族:get / put / mget / mput
单文件 get/put 已在 3.2 全流程实测。mput 批量上传见 3.3;mget 单文件下载实测(-i 关提示):
$ printf 'user anonymous ftpdemo\nmget m1.txt\nbye\n' | ftp -n -v -i 127.0.0.1 2121
Connected to 127.0.0.1.
220 pyftpdlib 2.1.0 ready.
331 Username ok, send password.
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
229 Entering extended passive mode (|||53977|).
125 Data connection already open. Transfer starting.
226 Transfer complete.
9 bytes received in 00:00 (75.76 KiB/s)
221 Goodbye.
mget 支持「远端名 本地名」两段式(同 get)与多文件列表。通配符形态在本环境实测失败:
$ printf 'user anonymous ftpdemo\nmget m*.txt\nbye\n' | ftp -n -v -i 127.0.0.1 2121
Connected to 127.0.0.1.
220 pyftpdlib 2.1.0 ready.
331 Username ok, send password.
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
No such file or directory.
221 Goodbye.
没有出现任何 229/125 数据连接痕迹——失败发生在远端通配展开阶段。用 ls m*.txt 直接验证服务端行为:
$ printf 'user anonymous ftpdemo\nls m*.txt\nmget m*.txt\nbye\n' | ftp -n -v 127.0.0.1 2121
Connected to 127.0.0.1.
220 pyftpdlib 2.1.0 ready.
331 Username ok, send password.
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
229 Entering extended passive mode (|||56313|).
550 No such file or directory.
No such file or directory.
221 Goodbye.
结论:tnftp 的远端通配展开是把含通配符的模式原样发给服务端 LIST/NLST,由服务端做匹配;pyftpdlib 2.1.0 不支持在 LIST 参数里通配,按字面找 m*.txt 于是回 550 No such file or directory.。在 vsftpd/proftpd 等支持 glob 的服务端上同样的命令可以正常工作——这是服务端能力差异,不是客户端语法错误。批量下载的稳妥写法:显式文件名列表(mget m1.txt m2.txt m3.txt),或先 nlist 拿到名单再逐个 get。
💡 要点:mget/mput 的逐文件确认提示(
mget 文件名 [anpqy?]?)可按a=全部继续、n=跳过、q=中止;脚本里要么提前-i关确认,要么用printf 'a\n' | ftp喂应答。传输大文件时hash命令可开启#进度条(每 2048 字节一格),避免「卡住还是正常」的猜测。
3.8 binary 与 ascii 模式:md5 对照实证
FTP 定义了两种传输类型:binary(Image 型,字节原样搬运)与 ascii(文本型,两端按本地换行惯例翻译 \n ↔ \r\n)。构造一个同时含 CRLF、LF 与随机字节的样本,同一文件用两种模式各上传、各下载一轮,md5 与字节数对照:
$ md5sum /root/demo/ftp/mixed.bin
511472b8ef33a22babc8341c9eb42a95 /root/demo/ftp/mixed.bin
$ printf 'user anonymous ftpdemo\nbinary\nput mixed.bin\nascii\nput mixed.bin mixed_a.bin\nbinary\nget mixed.bin got_bin.bin\nascii\nget mixed.bin got_ascii.bin\nbye\n' | ftp -n -v 127.0.0.1 2121
Connected to 127.0.0.1.
220 pyftpdlib 2.1.0 ready.
331 Username ok, send password.
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
200 Type set to: Binary.
local: mixed.bin remote: mixed.bin
229 Entering extended passive mode (|||43747|).
125 Data connection already open. Transfer starting.
226 Transfer complete.
281 bytes sent in 00:00 (1.46 MiB/s)
200 Type set to: ASCII.
local: mixed.bin remote: mixed_a.bin
229 Entering extended passive mode (|||49997|).
125 Data connection already open. Transfer starting.
226 Transfer complete.
287 bytes sent in 00:00 (2.13 MiB/s)
200 Type set to: Binary.
local: got_bin.bin remote: mixed.bin
229 Entering extended passive mode (|||56501|).
125 Data connection already open. Transfer starting.
226 Transfer complete.
281 bytes received in 00:00 (4.39 MiB/s)
200 Type set to: ASCII.
local: got_ascii.bin remote: mixed.bin
229 Entering extended passive mode (|||59081|).
125 Data connection already open. Transfer starting.
226 Transfer complete.
284 bytes received in 00:00 (5.41 MiB/s)
221 Goodbye.
注意字节数已经不同:binary put 281、ascii put 287(客户端把 LF 展开成 CRLF 多发 6 字节)、ascii get 284。md5 终判:
$ md5sum /root/demo/ftpshare/mixed.bin /root/demo/ftpshare/mixed_a.bin /root/demo/ftp/mixed.bin /root/demo/ftp/got_bin.bin /root/demo/ftp/got_ascii.bin
511472b8ef33a22babc8341c9eb42a95 /root/demo/ftpshare/mixed.bin
511472b8ef33a22babc8341c9eb42a95 /root/demo/ftpshare/mixed_a.bin
511472b8ef33a22babc8341c9eb42a95 /root/demo/ftp/mixed.bin
511472b8ef33a22babc8341c9eb42a95 /root/demo/ftp/got_bin.bin
8546c1cc90a5810562ccb8589800eedf /root/demo/ftp/got_ascii.bin
$ wc -c /root/demo/ftp/mixed.bin /root/demo/ftp/got_bin.bin /root/demo/ftp/got_ascii.bin
281 /root/demo/ftp/mixed.bin
281 /root/demo/ftp/got_bin.bin
278 /root/demo/ftp/got_ascii.bin
840 total
$ od -c /root/demo/ftp/got_ascii.bin | head -3
0000000 l i n e 1 \n l i n e 2 \n l i n e
0000020 3 \n 002 E 264 034 0 e O ' $ 006 322 @ 377 253
0000040 v 367 7 j 177 270 220 | 264 221 336 351 344 315 K
结论全部落在数据上:
- binary 模式字节级一致:四份 md5 相同(原文件、binary 上传的服务端副本、binary 下载的两份)。
- ascii 模式两端都在做换行翻译:上传时客户端
\n → \r\n展开(281 → 287 字节上线,服务端归一化存储后 md5 与原文件相同);下载时服务端把\r\n → \n剥离(281 → 278 字节,od 可见开头line1 \n line2的裸 LF,md5 改变)。翻译规则随客户端/服务端实现而异(本环境实测为 tnftp 客户端展开、pyftpdlib 服务端归一化),跨平台文本交换之外一律不该用 ascii。 - 实践铁律:压缩包/图片/音视频/数据库导出等任何非纯文本,必须
binary(tnftp 默认就是 binary,见 3.2 的Using binary mode);忘切 ascii 又传二进制是 FTP 时代的经典损坏事故。
💡 要点:本节 md5 对照是「ascii 模式会损坏二进制」的实证——换行翻译在两端各做一次,任何一端规则不同文件即损坏。铁律:连上先看
Using binary mode(tnftp 默认即 binary),看到Using ascii mode立刻敲binary;status里的Type: binary是传输前的最后一道核对。
3.9 中文文件名
UTF-8 中文文件名在本环境全链路可用:上传、列目录、按原名下载回读,实测:
$ printf 'user anonymous ftpdemo\nput 年度报告.txt\nls\nget 年度报告.txt 年度报告-回传.txt\nbye\n' | ftp -n -v 127.0.0.1 2121
Connected to 127.0.0.1.
220 pyftpdlib 2.1.0 ready.
331 Username ok, send password.
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
local: 年度报告.txt remote: 年度报告.txt
229 Entering extended passive mode (|||58405|).
125 Data connection already open. Transfer starting.
226 Transfer complete.
29 bytes sent in 00:00 (214.54 KiB/s)
229 Entering extended passive mode (|||33341|).
125 Data connection already open. Transfer starting.
-rw-r--r-- 1 root root 9 Oct 16 00:33 m1.txt
-rw-r--r-- 1 root root 9 Oct 16 00:33 m2.txt
-rw-r--r-- 1 root root 11 Oct 16 00:33 m3.txt
-rw-r--r-- 1 root root 281 Oct 16 00:33 mixed.bin
-rw-r--r-- 1 root root 281 Oct 16 00:33 mixed_a.bin
drwxr-xr-x 2 root root 4096 Oct 16 00:33 pub
-rw-r--r-- 1 root root 22 Oct 16 00:33 up.txt
-rw-r--r-- 1 root root 29 Oct 16 00:33 年度报告.txt
226 Transfer complete.
local: 年度报告-回传.txt remote: 年度报告.txt
229 Entering extended passive mode (|||33251|).
125 Data connection already open. Transfer starting.
226 Transfer complete.
29 bytes received in 00:00 (171.63 KiB/s)
221 Goodbye.
$ ls -l /root/demo/ftp/年度报告-回传.txt; cat /root/demo/ftp/年度报告-回传.txt
-rw-r--r-- 1 root root 29 Oct 16 00:33 /root/demo/ftp/年度报告-回传.txt
中文内容测试
第二行
服务端列表出现 年度报告.txt(29 字节),回传文件内容逐字一致。前提是两端 UTF-8 编码一致(本环境默认 UTF-8);跨编码环境传输中文文件名会乱码,建议归档传输一律改用英文/拼音文件名。
💡 要点:中文文件名能否活着取决于「两端编码协商」——FTP 本身没有编码声明机制,RFC 2640 的 UTF-8 能力位只是尽力而为;跨编码(如老 Windows GBK 服务端)传输时乱码无解。工程铁律:跨系统传输的文件名一律 ASCII 化,内容文件再考虑编码。
3.10 登录失败形态(530)
错误凭据的响应形态,排障时据此区分「密码错」与「网络不通」:
$ printf 'user nosuchuser wrongpass\nbye\n' | ftp -n -v 127.0.0.1 2121
Connected to 127.0.0.1.
220 pyftpdlib 2.1.0 ready.
331 Username ok, send password.
530 Authentication failed.
Login failed.
221 Goodbye.
530 Authentication failed. 是服务端的认证拒绝码,客户端随即提示 Login failed.;连接本身是通的(220/331 正常),问题只出在凭据。
💡 要点:登录失败排障先看三位响应码——220 出现说明 TCP 通、服务活着;331 说明用户名被接受、卡在密码;530 才是认证拒绝。若连 220 都没有,问题在网络/防火墙层面,与凭据无关。FTP 响应码是排障的第一现场,
-v模式(3.2 节)就是为它准备的。
3.11 技术拓展:FTP 与现代文件传输协议对比
FTP 是最古老的文件传输协议之一,现代替代方案横向对比——
| 协议 | 传输通道 | 安全性 | 适用场景 |
|---|---|---|---|
| FTP(本文) | 控制 21 + 数据 20/随机口 | 明文,无加密 | 内网临时交换、教学理解协议机制 |
| FTPS | FTP + TLS 显式加密 | 加密(证书) | 沿用 FTP 语义但需加密的老系统 |
| SFTP | SSH 单通道(22) | 加密(SSH) | 现代文件交换的事实标准 |
| scp | SSH 单通道 | 加密 | 简单的单次拷贝(无目录浏览) |
| rsync over SSH | SSH + 增量算法 | 加密 + 断点续传 | 大批量/可恢复的目录同步 |
💡 关键差异一句话:FTP 双通道(控制+数据分离)的设计是 PASV/NAT 问题的根源,SFTP 单通道天生对防火墙友好。本文以 ftp 为主线(Ubuntu 22.04 自带 tnftp 客户端)实测协议行为本身;生产文件交换一律用 SFTP/scp/rsync,FTP 仅建议存活于完全隔离的内网环境。
四、实战示例
4.1 一条命令完成「登录 → 下载 → 上传 → 退出」
最常用的自动化单行形态,-n -v -i 三件套 + user 内联凭据 + 显式命令序列:
$ printf 'user anonymous ftpdemo\nbinary\nlcd /root/demo/ftp\nget demo.txt\nput up.txt\nbye\n' | ftp -n -v -i 127.0.0.1 2121
Connected to 127.0.0.1.
220 pyftpdlib 2.1.0 ready.
331 Username ok, send password.
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
200 Type set to: Binary.
Local directory now: /root/demo/ftp
local: demo.txt remote: demo.txt
229 Entering extended passive mode (|||49179|).
125 Data connection already open. Transfer starting.
226 Transfer complete.
27 bytes received in 00:00 (160.77 KiB/s)
local: up.txt remote: up.txt
229 Entering extended passive mode (|||37441|).
125 Data connection already open. Transfer starting.
226 Transfer complete.
22 bytes sent in 00:00 (91.03 KiB/s)
221 Goodbye.
4.2 批量同步:显式文件名列表的 mput/mget
规避 3.7 的通配符 550 问题,批量传输用显式列表 + -i:
$ printf 'user anonymous ftpdemo\nmget m1.txt m2.txt m3.txt\nbye\n' | ftp -n -v -i 127.0.0.1 2121
Connected to 127.0.0.1.
220 pyftpdlib 2.1.0 ready.
331 Username ok, send password.
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
local: m1.txt remote: m1.txt
229 Entering extended passive mode (|||44741|).
125 Data connection already open. Transfer starting.
226 Transfer complete.
9 bytes received in 00:00 (69.20 KiB/s)
local: m2.txt remote: m2.txt
229 Entering extended passive mode (|||33787|).
125 Data connection already open. Transfer starting.
226 Transfer complete.
9 bytes received in 00:00 (79.18 KiB/s)
local: m3.txt remote: m3.txt
229 Entering extended passive mode (|||51595|).
125 Data connection already open. Transfer starting.
226 Transfer complete.
11 bytes received in 00:00 (103.29 KiB/s)
221 Goodbye.
4.3 站点目录维护:mkdir → cd → 上传 → 清理
发布一个新版本目录再清理的组合(rename/delete/rmdir 响应码解读见 3.5/3.6):
$ printf 'user anonymous ftpdemo\nmkdir release-2026\ncd release-2026\nput up.txt\nls\ndelete up.txt\ncdup\nrmdir release-2026\nbye\n' | ftp -n -v -i 127.0.0.1 2121
Connected to 127.0.0.1.
220 pyftpdlib 2.1.0 ready.
331 Username ok, send password.
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
257 "/release-2026" directory created.
250 "/release-2026" is the current directory.
local: up.txt remote: up.txt
229 Entering extended passive mode (|||35899|).
125 Data connection already open. Transfer starting.
226 Transfer complete.
22 bytes sent in 00:00 (159.14 KiB/s)
229 Entering extended passive mode (|||39621|).
125 Data connection already open. Transfer starting.
-rw-r--r-- 1 root root 22 Oct 16 00:39 up.txt
226 Transfer complete.
250 File removed.
250 "/" is the current directory.
250 Directory removed.
221 Goodbye.
完整生命周期逐段可读:257 "/release-2026" directory created.(建目录)→ put 上传(226)→ ls 确认目录内 up.txt(22 字节)→ 250 File removed.(清空文件)→ 250 "/" is the current directory.(cdup 回根)→ 250 Directory removed.(删空目录)。注意最后一环:rmdir 只能删空目录,若跳过 delete 直接 rmdir,服务端会回 550。
五、注意事项与常见问题
Q1:ftp 到底安全吗?用户名密码会被看到吗? 不安全。FTP 控制连接全程明文,登录的用户名与口令就是普通的 base64/明文文本,同网段抓包(tcpdump/Wireshark)可直接还原,数据文件同样裸奔。生产环境应换用加密传输——
💡 技术拓展:常见加密文件传输方案对比——
方案 底层 典型命令 适用场景 sftp SSH 通道(22 端口) sftp user@host主流首选,复用 SSH 凭据/密钥 scp SSH 通道 scp file user@host:/path简单单次复制 rsync over ssh SSH 通道 rsync -av dir/ user@host:/dir/大量文件增量同步 FTPS FTP + TLS(显式 21) 需支持 FTPS 的客户端 必须沿用 FTP 服务端的场合 SFTP+ntfy/WebDAV+HTTPS HTTPS 通道 curl 生态 网关友好、只开 443 运维侧建议:关闭明文 FTP 服务(vsftpd 停用或限内网),文件传输一律 sftp/rsync;本文实测 ftp 客户端的价值在于理解 FTP 协议与维护历史内网服务。
Q2:脚本里 user anonymous 之后为什么后续命令没执行?
非交互(stdin 为管道)时,Password: 提示会从 stdin 吞掉下一行当作密码(见 3.1 实测)。解法:密码内联在 user 命令里(user 用户名 密码),不要单独占一行;交互终端下则无此问题(密码从 /dev/tty 读取)。
Q3:mget *.txt 为什么报 No such file or directory?
tnftp 的远端通配展开依赖服务端在 LIST/NLST 里做匹配;pyftpdlib 2.1.0 不支持通配参数,回 550(见 3.7 实测)。vsftpd/proftpd 上通常可用。稳妥写法:显式文件名列表,或先 nlist 取名单再逐个 get。
Q4:ascii 模式传输后 md5 变了,文件坏了吗?
不是损坏,是 ascii 模式的换行翻译在起作用(3.8 有完整字节级实证)。需要字节级一致就返回 binary 模式;只有确认两端都要做换行惯例转换的纯文本才用 ascii。
Q5:登录成功但传文件卡住,什么原因?
多半是被动模式数据连接被防火墙挡住:控制连接(21)通了,数据连接协商的端口进不来。先 status 看 Passive mode 状态,再检查服务端被动端口范围(pyftpdlib 启动日志 passive ports: None 表示内核临时分配)与中间防火墙放行;客户端也可尝试切换主动/被动模式对比测试(2.1 拓展表)。
六、参考链接
- tnftp 项目主页(NetBSD ftp):https://manpages.ubuntu.com/manpages/jammy/en/man1/tnftp.1.html
- FTP 协议规范 RFC 959(传输类型/应答码定义):https://www.rfc-editor.org/rfc/rfc959
- FTP 安全问题备忘 RFC 2577:https://www.rfc-editor.org/rfc/rfc2577
- pyftpdlib 官方文档(演示服务端用法):https://pyftpdlib.readthedocs.io/
- FTPS 与 FTP 安全扩展 RFC 4217:https://www.rfc-editor.org/rfc/rfc4217