简介:这份资源面向需要在Java项目中实现FTP文件传输的开发者,尤其是刚接触网络编程、希望快速跑通上传下载流程的初中级程序员。包内共2个文件,包含1个java源码与1个jar依赖包,压缩包约106KB,体积轻量,便于直接导入工程参考。源码围绕Apache Commons Net库展开,覆盖FTPClient连接与登录、主动与被动模式切换、二进制传输模式设置、retrieveFile下载与storeFile上传、目录切换与列表获取、断开连接及FTPException、IOException异常处理等关键环节,并附有可直接运行的示例代码,帮助读者理解各API的调用顺序与参数含义。目前已有251人学习下载,适合作为FTP功能模块的入门模板,也可在此基础上扩展超时设置、重试机制与SSL/TLS安全传输等进阶配置。
1. 从一台老服务器说起:FTP 上传下载到底还能不能打
上周帮朋友处理一个遗留系统,对方甩过来一台 Windows Server 2008 的机器,上面跑着一个内部文件交换服务,协议是 FTP。他问我:“这年头还有人用 FTP 吗?我是不是该换成对象存储?”我没直接回答,而是先连上去跑了一遍上传下载,确认带宽、权限、被动模式端口都正常,然后告诉他:能用,而且在这个场景下比重新搭一套 HTTP 接口快得多。FTP 从 1971 年活到现在,靠的不是先进,而是简单直接——客户端连上服务端,登录、切目录、传文件,四步完事。它适合内网文件交换、设备日志回传、老旧系统对接,以及那些不想引入额外依赖的批量传输任务。如果你手头正好有一台 FTP 服务器要对接,或者想自己搭一个来传文件,这篇笔记把上传下载的完整链路、参数配置和踩坑点都拆开讲一遍。
2. FTP 协议核心机制与连接模式选型
2.1 控制连接与数据连接:两条 TCP 各干各的
FTP 跟 HTTP 最大的不同在于它用两条 TCP 连接。一条叫控制连接,默认走 21 端口,负责发命令、收响应码,比如USER、PASS、CWD、RETR、STOR都走这条线。另一条叫数据连接,专门传文件内容或目录列表,端口不固定,取决于主动还是被动模式。控制连接在整个会话期间一直保持,数据连接每传一次文件就新建一次,传完就关。这个设计带来的直接后果是:防火墙只开 21 端口往往不够,数据连接那一条如果被挡住,就会出现“登录成功但列目录卡死”的经典现象。我一般会在客户端先开debug或verbose模式,看它卡在LIST还是PASV那一步,基本就能定位是哪条连接出了问题。
2.2 主动模式与被动模式:选错了就是连不上
主动模式(PORT)的逻辑是:客户端告诉服务端“你来连我”,服务端从自己的 20 端口主动连客户端指定的一个高位端口。问题在于客户端如果在内网或防火墙后面,服务端根本连不进来。被动模式(PASV)反过来:客户端问服务端“你开哪个端口等我”,服务端返回一个 IP 和端口,客户端主动去连。现在绝大多数场景默认走被动模式,因为客户端通常在内网,服务端在公网或 DMZ。但被动模式也有坑:服务端返回的 IP 如果是内网地址,客户端拿到后连不上,需要在服务端配置里强制指定公网 IP 或端口范围。我一般会在服务端固定一个被动端口区间,比如 50000-50100,然后在防火墙放行这个区间,客户端侧不用改任何东西。
2.3 明文传输与 FTPS/SFTP 的边界
FTP 本身是明文协议,账号密码和文件内容在网络上裸奔。如果传输链路经过不可信网络,必须上 FTPS(FTP over TLS)或 SFTP(SSH File Transfer Protocol)。注意这两个不是一回事:FTPS 是在 FTP 基础上加 TLS 层,端口还是 21 和被动端口;SFTP 是 SSH 协议的一个子系统,通常走 22 端口,跟 FTP 命令完全不兼容。选型上,如果服务端已经跑着 SSH,直接用 SFTP 最省事;如果服务端是纯 FTP 服务且不想动架构,就开 FTPS。我一般优先推 SFTP,因为配置简单、穿透防火墙容易,而且客户端工具支持度也高。
2.4 客户端工具选型:命令行还是图形化
命令行工具里,ftp是最原始的,功能少、不支持被动模式切换(有些系统可以passive命令);lftp功能强很多,支持多线程、断点续传、镜像目录;curl适合脚本里单次上传下载。图形化工具里,FileZilla 跨平台、支持 FTPS/SFTP、有站点管理器;WinSCP 在 Windows 上跟资源管理器集成好。我一般写脚本用lftp或curl,手动操作就用 FileZilla。下面这张表是我常用的几个工具对比:
| 工具 | 协议支持 | 断点续传 | 脚本友好度 | 典型场景 |
|---|---|---|---|---|
| ftp (系统自带) | FTP | 部分 | 低 | 临时调试 |
| lftp | FTP/FTPS/SFTP | 支持 | 高 | 批量镜像、定时任务 |
| curl | FTP/FTPS/SFTP | 支持 | 高 | 单文件上传下载 |
| FileZilla | FTP/FTPS/SFTP | 支持 | 低 | 手动拖拽操作 |
| WinSCP | FTP/FTPS/SFTP | 支持 | 中 | Windows 图形化 |
3. 上传下载实操:命令行与脚本两条路
3.1 用 lftp 做批量上传:镜像目录与排除规则
lftp是我最常用的批量上传工具,因为它支持mirror命令,可以把本地目录整个同步到服务端,而且支持排除规则和断点续传。下面是一个典型的上传脚本:
#!/bin/bash # 批量上传本地目录到 FTP 服务端 # 使用 lftp 的 mirror -R 反向镜像(本地 -> 远程) HOST="ftp.example.com" USER="deploy" PASS="your_password" LOCAL_DIR="/data/release/" REMOTE_DIR="/upload/release/" lftp -c " set ftp:ssl-allow no; # 如果服务端不支持 FTPS,显式关闭 set ftp:passive-mode on; # 强制被动模式,避免主动模式连不上 set net:timeout 10; # 网络超时 10 秒 set net:max-retries 3; # 失败重试 3 次 open -u $USER,$PASS $HOST; mirror -R \ --parallel=4 \ # 4 个并发传输 --exclude-glob '*.tmp' \ # 排除临时文件 --exclude-glob '.git/' \ # 排除 .git 目录 --only-newer \ # 只传比服务端新的文件 $LOCAL_DIR $REMOTE_DIR; bye "这段脚本的逻辑是:先设置被动模式和超时参数,然后用mirror -R把本地目录反向镜像到远程。--parallel=4开 4 个并发,适合大量小文件;--only-newer避免重复传已存在的文件;--exclude-glob排除掉不需要上传的临时文件和版本控制目录。参数上,net:timeout和net:max-retries根据网络质量调整,内网可以设短一点,公网建议 15 秒以上。如果服务端支持 FTPS,把ftp:ssl-allow改成yes并加上证书验证参数即可。
3.2 用 curl 做单文件上传下载:一行命令搞定
如果只是传一两个文件,curl比lftp更直接。上传:
# 上传单个文件到 FTP 服务端 # -T 指定本地文件,-u 指定用户名密码 curl -T /data/report.csv \ -u deploy:your_password \ ftp://ftp.example.com/upload/report.csv \ --ftp-pasv \ # 被动模式 --ftp-create-dirs # 自动创建远程目录下载:
# 下载单个文件,-o 指定保存路径 curl -o /data/backup.zip \ -u deploy:your_password \ ftp://ftp.example.com/backup/backup.zip \ --ftp-pasv \ -C - # 断点续传,从已下载位置继续-C -是断点续传的关键,如果下载中断,重新执行同一条命令会从断点继续,不用重头来。--ftp-create-dirs在上传时如果远程目录不存在会自动创建,省掉手动建目录的步骤。注意curl默认不验证 FTPS 证书,如果服务端是自签证书,需要加-k跳过验证,但生产环境建议配好 CA 证书。
3.3 Python 脚本实现上传下载:ftplib 与 paramiko 的选择
如果要在 Python 里集成 FTP 操作,标准库ftplib够用,但只支持 FTP 和 FTPS,不支持 SFTP。SFTP 要用paramiko。下面是一个ftplib上传下载的示例:
import ftplib import os def ftp_upload(host, user, passwd, local_path, remote_dir): """上传文件到 FTP 服务端""" try: # 连接并登录,timeout 防止卡死 ftp = ftplib.FTP(host, timeout=15) ftp.login(user, passwd) # 切换到被动模式,避免主动模式被防火墙拦截 ftp.set_pasv(True) # 切换远程目录,不存在则创建 try: ftp.cwd(remote_dir) except ftplib.error_perm: ftp.mkd(remote_dir) ftp.cwd(remote_dir) # 以二进制模式上传,避免文件损坏 with open(local_path, 'rb') as f: ftp.storbinary(f'STOR {os.path.basename(local_path)}', f) ftp.quit() print(f"上传成功: {local_path}") except Exception as e: print(f"上传失败: {e}") def ftp_download(host, user, passwd, remote_path, local_path): """从 FTP 服务端下载文件""" try: ftp = ftplib.FTP(host, timeout=15) ftp.login(user, passwd) ftp.set_pasv(True) with open(local_path, 'wb') as f: ftp.retrbinary(f'RETR {remote_path}', f.write) ftp.quit() print(f"下载成功: {local_path}") except Exception as e: print(f"下载失败: {e}") # 调用示例 ftp_upload('ftp.example.com', 'deploy', 'your_password', '/data/report.csv', '/upload') ftp_download('ftp.example.com', 'deploy', 'your_password', '/backup/backup.zip', '/data/backup.zip')关键点:set_pasv(True)强制被动模式;storbinary和retrbinary用二进制模式传输,避免文本模式下换行符被转换导致文件损坏;timeout参数防止网络异常时脚本永久挂起。如果服务端是 SFTP,把ftplib换成paramiko的SFTPClient,连接端口改成 22,其他逻辑类似。
3.4 定时任务与日志记录:让传输可追溯
生产环境里 FTP 传输通常要定时跑,并且记录日志。我一般用cron加 shell 脚本,脚本里把lftp的输出重定向到日志文件,同时记录时间戳和退出码:
#!/bin/bash # /etc/cron.daily/ftp_sync.sh LOG="/var/log/ftp_sync_$(date +%Y%m%d).log" echo "[$(date '+%Y-%m-%d %H:%M:%S')] 开始同步" >> $LOG lftp -c " set ftp:passive-mode on; set net:timeout 15; set net:max-retries 3; open -u deploy,your_password ftp.example.com; mirror -R --parallel=4 --only-newer /data/release/ /upload/release/; bye " >> $LOG 2>&1 echo "[$(date '+%Y-%m-%d %H:%M:%S')] 同步结束,退出码: $?" >> $LOG日志里能看到每次同步的开始结束时间、传输了哪些文件、有没有报错。如果退出码非 0,可以配合监控告警发通知。注意日志文件要定期清理,避免磁盘写满。
4. 避坑指南:FTP 上传下载的五个血泪教训
4.1 列目录卡死但登录正常
现象:客户端能登录,但执行ls或dir时卡住,最后超时断开。原因:数据连接被防火墙拦截,通常是主动模式下服务端连不回来,或者被动模式下服务端返回的 IP 是内网地址。解决:客户端强制被动模式(lftp里set ftp:passive-mode on,ftplib里set_pasv(True));服务端配置里指定公网 IP 和被动端口范围,并在防火墙放行。
4.2 上传后文件大小对不上
现象:本地文件 10MB,上传到服务端只有 9.8MB,或者下载回来打不开。原因:用了文本模式(ASCII)传输二进制文件,换行符被转换。解决:始终用二进制模式,ftplib用storbinary/retrbinary,lftp默认二进制,curl加--ftp-file不需要额外设置但注意不要用-B以外的模式。
4.3 中文文件名乱码
现象:上传中文名文件后,服务端显示乱码,或者下载时找不到文件。原因:客户端和服务端编码不一致,常见是客户端 UTF-8、服务端 GBK。解决:统一用 UTF-8,lftp里set ftp:charset utf-8,ftplib在 Python 3 里默认 UTF-8 但需要服务端支持OPTS UTF8 ON。如果服务端不支持,只能避免中文文件名。
4.4 大文件传输中途断开
现象:传几百 MB 以上的文件时,传到一半连接断开,重传又从零开始。原因:网络不稳定或服务端超时设置太短。解决:用支持断点续传的工具,lftp的mirror默认支持续传,curl加-C -,ftplib需要自己实现REST命令。同时调大服务端和客户端的超时参数。
4.5 权限拒绝但账号密码没错
现象:登录成功,但上传时报550 Permission denied。原因:服务端用户对目标目录没有写权限,或者被动模式端口被 SELinux 拦截。解决:检查服务端目录权限和属主,确认 FTP 用户有写权限;如果是 Linux,检查 SELinux 是否允许 FTP 写操作(setsebool -P ftpd_full_access on);如果是 vsftpd,检查write_enable=YES和local_umask设置。
5. 进阶技巧:用 lftp 做增量同步与校验
5.1 增量同步:只传变化的文件
lftp的mirror命令自带增量能力,核心参数是--only-newer和--ignore-time。--only-newer只传比服务端新的文件,适合发布场景;--ignore-time忽略时间戳,只比大小,适合服务端时间不准的情况。我一般还会加--delete删除服务端多余的文件,保持两端一致:
lftp -c " set ftp:passive-mode on; open -u deploy,your_password ftp.example.com; mirror -R --parallel=4 --only-newer --delete /data/release/ /upload/release/; bye "注意--delete有风险,如果本地目录被误清空,服务端也会被清空。我一般先跑一次--dry-run看会删哪些文件,确认无误再去掉--dry-run正式执行。
5.2 传输后校验:比对文件大小和哈希
FTP 协议本身不提供哈希校验,但可以用SIZE命令比对文件大小,或者下载回来算 MD5。lftp里可以用ls列出远程文件大小,跟本地比对:
# 本地文件大小 local_size=$(stat -c %s /data/release/report.csv) # 远程文件大小 remote_size=$(lftp -c " set ftp:passive-mode on; open -u deploy,your_password ftp.example.com; ls /upload/release/report.csv; bye " | awk '{print $5}') if [ "$local_size" -eq "$remote_size" ]; then echo "大小一致,传输成功" else echo "大小不一致,本地 $local_size 远程 $remote_size" fi如果对完整性要求更高,可以在上传前算本地 MD5,上传后从服务端下载回来再算一次 MD5 比对。虽然多一次下载,但对于关键文件值得。
5.3 连接复用与并发控制
lftp支持在一个会话里连续执行多条命令,避免反复登录。--parallel控制并发数,但不要设太大,一般 4 到 8 就够了,设太大反而会因为服务端连接数限制导致失败。如果服务端有连接数限制,可以在lftp里设set net:limit-total-rate限制总带宽,避免把带宽跑满影响其他服务。
从那以后我每次配 FTP 传输,都强制先跑一遍被动模式加二进制模式,再拿一个小文件试传试下,确认没问题才上批量脚本。希望帮到你。
本文还有配套的精品资源,点击获取