简介:这份资源面向需要在 Java 项目中实现 FTP 文件传输的开发者,尤其是刚接触 Apache Commons Net 库、希望快速跑通上传下载流程的初中级程序员。压缩包共 2 个文件,包含 1 个 java 源码与 1 个 jar 依赖包,整体约 106KB,体积轻量,便于直接导入工程调试。源码围绕 FTPClient 的连接、登录、被动模式设置、二进制传输模式切换、文件上传下载、目录切换与断开连接等环节展开,并附有异常处理与示例调用逻辑,可帮助读者理解 FTP 交互的完整链路。已有 252 人学习下载,适合作为 FTP 功能模块的起步参考,也可在此基础上扩展超时设置、重试机制与 SSL/TLS 安全传输等进阶配置。
1. FTP 传文件这件事,真正难的不是连上去
很多人对 FTP 的印象还停留在“填个地址、输账号密码、拖拽上传”,真到项目里才发现,能连上只是第一步。某开发者接手一个模拟项目时,需求写得很朴素:把本地生成的报表文件定时传到服务器,再从服务器拉回一批归档文件。结果第一版脚本在测试环境跑得好好的,换到另一台机器就卡在被动模式上,日志里只有一行超时,排查了大半天。这类问题在 FTP 场景里非常典型——协议本身不复杂,但模式选择、编码、权限、断点续传这些细节,任何一个没处理对,都会让脚本变成“玄学”。
这份资源围绕“FTP 从服务器上传下载文件”这个核心动作,把连接建立、目录切换、文件传输、异常处理串成一条可复现的链路。它适合两类人:一类是刚接触文件传输、需要快速跑通一个可用脚本的新手;另一类是有经验、但想补齐被动模式、二进制传输、超时重试这些边界处理的从业者。下面按“先跑通、再讲透、最后避坑”的顺序拆开说,代码可以直接抄,参数可以按自己的环境改。
2. 把连接和传输跑通:最小可用脚本与参数拆解
2.1 选 FTP 还是 SFTP,先把这个判断做掉
动手之前有一个选型问题必须先说清楚,否则后面全是白费功夫。FTP 是明文协议,账号密码和数据在网络上是裸奔的;SFTP 走的是加密通道,端口通常是 22。很多内网环境、老设备、嵌入式系统只开了 FTP,这时候没得选;但只要对端支持 SFTP,我一般会优先用 SFTP,原因不是“更高级”,而是省掉后面一堆安全整改的麻烦。
判断方法很简单:先确认对端服务端口。21 端口通、22 端口不通,基本就是纯 FTP;两个都通,优先走 SFTP。这份资源聚焦的是 FTP 本身,因为大量存量系统和设备仍然只提供 FTP 接口,把它的脾气摸清楚,遇到只能走 FTP 的场景才不至于翻车。下面所有代码都基于 Python 标准库ftplib,不依赖第三方包,换台机器就能跑。
2.2 最小上传下载脚本,逐行讲清楚
先给一个能直接运行的最小版本,把“连上、切目录、传文件、断开”这条主线走通。代码里的注释标了每个关键动作,后面再逐段拆参数。
import ftplib import os HOST = "192.168.1.100" # 服务器地址,换成自己的 PORT = 21 # FTP 默认端口 USER = "deploy" # 账号 PASS = "change_me" # 密码,实际项目别硬编码 REMOTE_DIR = "/data/report" # 远端目录 LOCAL_FILE = "./report_2024.csv" REMOTE_NAME = "report_2024.csv" def upload(): # 1. 建立连接,timeout 必须设,否则网络异常时会一直挂住 ftp = ftplib.FTP() ftp.connect(HOST, PORT, timeout=30) # 2. 登录 ftp.login(USER, PASS) # 3. 切到目标目录,目录不存在会抛错,后面避坑章节会讲 ftp.cwd(REMOTE_DIR) # 4. 以二进制模式上传,避免换行符被改写 with open(LOCAL_FILE, "rb") as f: ftp.storbinary(f"STOR {REMOTE_NAME}", f, blocksize=8192) # 5. 优雅退出,别直接 close ftp.quit() def download(): ftp = ftplib.FTP() ftp.connect(HOST, PORT, timeout=30) ftp.login(USER, PASS) ftp.cwd(REMOTE_DIR) # 下载同样用二进制模式 with open("./downloaded.csv", "wb") as f: ftp.retrbinary("RETR report_2024.csv", f.write, blocksize=8192) ftp.quit() if __name__ == "__main__": upload() download()逻辑说明:connect的timeout参数是整个脚本的“后悔药”,不设的话,对端不响应时进程会无限等待,定时任务直接堆积。storbinary和retrbinary里的blocksize控制每次读写的数据块大小,默认 8192 字节,内网可以调到 32768 提升吞吐,跨公网反而建议保持默认或更小,减少单次超时的影响面。STOR是上传命令,RETR是下载命令,这两个是 FTP 协议里最核心的动词,记住它们比记一堆封装函数有用。
参数怎么改:HOST、PORT、USER、PASS按实际环境填;REMOTE_DIR用绝对路径最稳,相对路径依赖登录后的默认目录,不同服务器行为不一致。密码不要写死在代码里,常见做法是读环境变量或配置文件,这一点在后面的进阶章节会展开。
2.3 主动模式与被动模式:卡住九成新手的第一个坑
上面脚本能跑通的前提是对端允许被动模式。FTP 有主动(PORT)和被动(PASV)两种数据连接方式,区别在于谁发起数据连接。主动模式是服务器回头连客户端,客户端如果在 NAT 或防火墙后面,这个回连基本进不来;被动模式是客户端再开一个连接去连服务器,穿透性更好。所以现代环境几乎都用被动模式,ftplib默认也是被动模式。
但“默认被动”不等于“一定成功”。有些服务器被动模式返回的地址是内网 IP,客户端拿到后连不上,表现就是登录成功、列目录卡死。这时候需要手动干预:
ftp = ftplib.FTP() ftp.connect(HOST, PORT, timeout=30) ftp.login(USER, PASS) # 强制使用被动模式,并忽略服务器返回的地址,改用控制连接的对端地址 ftp.set_pasv(True) ftp.makepasv = lambda: (ftp.host, ftp.port) # 部分场景下的兜底写法这段兜底写法不是万能药,它的作用是当服务器在 NAT 后面、被动模式返回了不可达地址时,强制用控制连接的地址去建数据连接。如果对端明确只支持主动模式(极少见,但老设备有),才需要ftp.set_pasv(False),同时确保客户端所在网络允许入站连接。判断当前用的是哪种模式,看日志里PASV或PORT命令的响应即可,这是排查连接类问题的第一现场。
3. 目录、编码与断点续传:让脚本在真实环境里站住
3.1 目录操作与文件存在性判断
真实项目里很少直接往根目录扔文件,通常要先建目录、判断文件是否已存在、避免覆盖。ftplib提供了cwd、mkd、nlst、size这几个方法,组合起来就能做存在性检查。
def ensure_dir(ftp, path): # 逐级创建目录,已存在则跳过 parts = [p for p in path.split("/") if p] current = "" for p in parts: current += "/" + p try: ftp.cwd(current) except ftplib.error_perm: ftp.mkd(current) ftp.cwd(current) def remote_exists(ftp, filename): # nlst 返回目录下文件名列表,适合文件不多的目录 try: names = ftp.nlst() return filename in names except ftplib.error_perm: return Falseensure_dir的思路是逐级cwd,失败就mkd再进,这样不管中间缺几级都能补上。remote_exists用nlst拉全量列表再比对,目录里文件成千上万时效率不高,更稳的做法是直接ftp.size(filename),能拿到大小说明文件存在,抛error_perm说明不存在。注意size在 ASCII 模式下可能返回不准,所以传输一律用二进制模式,这一点前面已经强调过。
3.2 中文文件名与编码问题
中文文件名是 FTP 的经典翻车点。FTP 协议本身没有规定文件名编码,服务器可能用 UTF-8,也可能用 GBK,客户端按 UTF-8 解码失败就会抛UnicodeDecodeError。ftplib在 Python 3 里默认用 UTF-8 编码命令,遇到老服务器需要手动指定:
ftp = ftplib.FTP() ftp.encoding = "gbk" # 老服务器常见,按实际响应调整 ftp.connect(HOST, PORT, timeout=30) ftp.login(USER, PASS)怎么判断该用哪种编码?先看服务器返回的目录列表,如果中文显示成乱码,就换编码试。常见组合是 UTF-8 和 GBK 两种,个别环境用 latin-1。这里没有银弹,只能按对端实际情况调。我的习惯是在配置里留一个encoding字段,部署到新环境先跑一次列目录,看中文是否正常,再决定传文件。文件名里带空格、井号、百分号的情况也要注意,STOR命令后的文件名最好做一次转义或加引号,避免被服务器解析成多个参数。
3.3 断点续传与传输完整性校验
大文件传到一半断了,从头再来既费时又费带宽。FTP 支持断点续传,上传用APPE追加或REST指定偏移,下载用REST加RETR。ftplib里对应的是storbinary配合rest参数,以及retrbinary前先sendcmd("REST offset")。
def resume_download(ftp, remote_name, local_path): local_size = os.path.getsize(local_path) if os.path.exists(local_path) else 0 with open(local_path, "ab") as f: # 追加模式 # REST 告诉服务器从哪个字节开始传 ftp.retrbinary(f"RETR {remote_name}", f.write, rest=local_size)rest参数就是断点位置,本地已有多少字节就从哪里续。上传方向的续传稍微麻烦一点,需要先ftp.size(remote_name)拿到远端已有大小,再用storbinary的rest参数从该偏移写。注意不是所有服务器都支持REST,部分老设备会返回502,这时候只能整文件重传,脚本里要做降级处理。
传完之后建议做一次大小比对,这是最便宜也最有效的完整性校验:
remote_size = ftp.size(remote_name) local_size = os.path.getsize(LOCAL_FILE) assert remote_size == local_size, f"大小不一致: 远端{remote_size} 本地{local_size}"哈希校验更严格,但 FTP 协议本身不提供远端哈希,只能传完后把文件拉回来算,成本高,一般用大小比对加业务层校验就够了。
4. 避坑与排查:五条血泪经验
4.1 现象:登录成功但列目录卡死
原因:被动模式返回了客户端不可达的地址,数据连接建不起来。解决:先确认set_pasv(True),再尝试用控制连接地址覆盖makepasv;如果对端在 NAT 后且无法改配置,联系运维开放被动端口范围,或在客户端侧做地址映射。排查时看PASV命令的响应里返回的 IP 和端口,和实际能连通的地址对比,一眼就能看出问题。
4.2 现象:中文文件名变成乱码或抛 UnicodeDecodeError
原因:客户端与服务器编码不一致。解决:设置ftp.encoding,UTF-8 和 GBK 各试一次,以列目录显示正常为准。注意编码要在connect之前设置,登录后再改对已建立的连接不一定生效。
4.3 现象:上传的 CSV 在服务器上多出空行或换行错乱
原因:用了 ASCII 模式传输,协议层做了换行符转换。解决:一律用storbinary/retrbinary,不要用storlines/retrlines传二进制或结构化数据。文本文件如果确实需要行模式,也要确认两端换行符约定一致。
4.4 现象:定时任务跑几天后进程堆积、连接数暴涨
原因:异常路径下没有关闭连接,quit没执行到。解决:用try/finally包住传输逻辑,finally里判断连接状态再quit,失败则close。同时给connect设timeout,给整个任务设外层超时,避免单次卡死拖垮调度。
ftp = ftplib.FTP() try: ftp.connect(HOST, PORT, timeout=30) ftp.login(USER, PASS) # ... 传输逻辑 finally: try: ftp.quit() except Exception: ftp.close()4.5 现象:小文件正常,大文件传到一半报 426 或超时
原因:数据连接空闲超时,或服务器对单次连接传输量有限制。解决:调小blocksize让数据持续流动,或在业务层把大文件切片分多次传;同时确认服务器侧的 idle timeout 配置,必要时让运维放宽。426 是“连接关闭、传输中止”,看到这个错误先查网络稳定性和超时设置,而不是怀疑账号权限。
5. 进阶:把脚本变成可配置、可重试的传输组件
跑通单次传输只是起点,真实项目里更需要一个能配置、能重试、能观测的组件。我的习惯是把连接参数、目录、编码、重试次数全部外置到配置文件,代码里只留逻辑。下面是一个精简版的重试封装,核心是“失败分类 + 指数退避”,避免网络抖动时疯狂重连把对端打挂。
import time import ftplib def with_retry(func, retries=3, base_delay=2): for attempt in range(1, retries + 1): try: return func() except (ftplib.error_temp, OSError) as e: # 临时性错误才重试,权限类错误重试没意义 if attempt == retries: raise time.sleep(base_delay * attempt)这里只对error_temp(4xx 临时错误)和OSError(网络层)重试,error_perm(5xx 永久错误,比如权限不足、目录不存在)直接抛出,因为重试也不会变好。base_delay * attempt是简化的退避,生产环境可以换成带随机抖动的指数退避,减少多任务同时重连的冲击。
配置外置可以用一个简单的字典或 YAML,关键字段和推荐值如下:
| 配置项 | 推荐值 | 说明 |
|---|---|---|
| timeout | 30 秒 | 连接与读写超时,跨公网可放宽到 60 |
| blocksize | 8192~32768 | 内网取大,公网取小 |
| retries | 3 | 临时错误重试次数 |
| encoding | utf-8 / gbk | 按对端实际调整 |
| pasv | True | 除非对端明确只支持主动模式 |
验证方法也很直接:故意把HOST改错,看是否在timeout内失败而不是挂死;故意传一个不存在的远端目录,看是否抛error_perm而不是无限重试;传一个几百 MB 的文件,中途断网再恢复,看断点续传是否生效。这三步走完,组件的边界基本就清楚了。
从那以后我每次写 FTP 相关脚本,都会先把timeout、encoding、pasv这三个参数显式写出来,哪怕它们和默认值一样——因为默认值会随环境和库版本变化,写出来才是给自己留的排查线索。希望帮到你。
本文还有配套的精品资源,点击获取