ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

FTP从服务器上传下载文件:被动模式、Python脚本与自动化避坑

FTP从服务器上传下载文件:被动模式、Python脚本与自动化避坑 简介面向需要在项目中落地服务器文件传输的开发者该压缩包基于FTP/SFTP协议封装了一套可复用的上传下载工具解决远程文件拉取、本地文件推送以及目录切换等日常运维需求。压缩包共2个文件主要包含一个Java源码文件和配套的jar依赖文件整体体积仅106KB结构非常精简便于直接融入现有工程。源码围绕客户端连接、登录验证、被动/主动模式差异、目录切换、文件读写与断开连接等关键环节给出可运行示例覆盖常用API调用与基本异常处理并参考JSch库实现了SFTP场景下的上传下载逻辑同时保留FTP方式所需的核心思路方便对比两种协议在实现上的异同。作为轻量级参考实现这套代码适合刚开始接触Java网络编程的开发者也可供有经验工程师快速移植到生产项目。目前已有250人学习下载实际使用时按需调整服务器地址、账号、路径即可。1. FTP从服务器上传下载文件为什么二十年协议还在干活很多人以为 FTP 已经是个过时协议但在我实际接触过的服务器里ftp从服务器上传下载文件仍然是部署应用、拉取备份、采集设备日志时最高频的操作。它不像 HTTP 有那么花哨的接口也不像对象存储有正规 SDK可它胜在服务器默认支持、客户端遍地都是、脚本也容易写。这篇文章按我自己的落地路径来先把连接参数讲透再给命令行和 Python 两套可复现的上传下载方案然后列 5 个我真实踩过的坑最后说怎么把 FTP 变成带校验和监控的自动化任务。适合正在写部署脚本、做数据迁移、对接打印机扫描上传的开发者与运维。2. FTP 客户端选型与连接前的三个参数为什么你的 FTP 总是连不上2.1 主动模式与被动模式选错就是“连接超时”FTP 和 HTTP 最大的区别在于它有两条链路控制连接走 21 端口数据连接是另外建立的。默认的“主动模式”下客户端先连上服务器的 21 端口然后告诉服务器“我打开了某一个端口你主动连过来传数据”。而“被动模式”反过来服务器打开一个随机高端口告诉客户端“你连这个端口来取数据”。现在绝大多数 FTP 客户端默认使用被动模式原因很简单客户端往往在公司 NAT 或家用路由器后面没有公网 IP服务器根本连不进来。你可能遇到的场景是用账号密码登录成功敲ls也正常但一执行get或put就卡住最后报Read timeout。这种“能登录但传不了文件”的现象八成是主动/被动模式没对上。常见做法的参数对照如下客户端 / 库设置方式默认情况curl--ftp-pasv默认主动需加参数Python ftplibftp.set_pasv(True)默认被动lftpset ftp:passive yes默认被动FileZilla站点管理器 → 传输设置默认被动如果你是运维大概率会遇到“服务器防火墙只放了 21 端口”的情况。这时客户端用被动模式服务器却无法在高端口建立数据通道只能看到超时。解决方法是把 vsftpd 配置里的pasv_min_port和pasv_max_port固定成一个段比如 40000-40100然后在防火墙和安全组里放行。注意云服务器除了系统防火墙控制台的安全组规则也必须同步放行漏一处就是玄学。2.2 编码、超时、重试三个必须写进配置的参数连接 FTP 时第一坑是中文文件名乱码。Windows 自带的 IIS FTP 服务通常按系统 ANSI 编码中文系统就是 GBK处理文件名而 Linux 下的 vsftpd 默认可能使用 UTF-8。如果你在 Windows 上用命令行 ftp 连接一个 Linux vsftpd看到的中文文件名会变成乱码反过来从 Linux 往 Windows 上传中文文件名文件对方收到也一样是乱码。解决路径并不复杂FileZilla 可以在站点管理器里强制字符集为 UTF-8 或自定义 CP936lftp 用set ftp:charset UTF-8Python 3.9 以上的 ftplib 支持直接设置ftp.encoding utf-8或gbk。我的原则是尽量让服务器端统一用 UTF-8客户端不要自己转换字节除非面对的是老旧 Windows FTP 服务。第二是超时参数。FTP 在弱网环境下经常出现控制连接正常、数据连接长时间无响应的情况。curl 可以加--connect-timeout 30 --retry 3lftp 用set net:timeout 30和set net:max-retries 3Python ftplib 在connect()时传timeout30。不要用默认的无限等待否则 crontab 里的同步任务会卡死并堆积。第三是重试。FTP 本身没有事务机制传了一半断了就断了续传也不是所有服务器都支持。所以自动化脚本必须在应用层做好重试并且要有最大重试次数否则一个坏网络会把服务器拖垮。2.3 凭据与安全从明文密码到 FTPS/SFTP 的取舍FTP 最大的硬伤是密码和文件内容都是明文传输。网络同一广播域里抓包就能看见USER和PASS这个没什么好争的。如果你只是想在后台上传下载文件我建议至少做到三点不要用 root 账号跑 FTP单独建一个低权限用户用 chroot 把该用户锁在指定的 FTP 文件夹里尽量限制来源 IP。如果你需要自己搭服务器做测试常见做法是 ubuntu 安装 ftp 服务apt install vsftpd然后改/etc/vsftpd.conf开启local_enableYES、write_enableYES、chroot_local_userYES。注意 vsftpd 默认禁止 chroot 用户在根目录有写权限所以指定 FTP 文件夹的权限要小心目录不能归 root 所有且对 ftp 用户可写。如果服务器支持 FTPS即 FTP over TLS优先用 FTPS显式 FTPS 先连 21 端口再升级加密会话常见客户端都能开。如果整条链路由你控制我更推荐 SFTP——注意 SFTP 不是 FTP over SSL它跑在 SSH 协议上端口通常是 22文件上传下载的命令和开放目录控制都和 FTP 类似。很多集成“ftp客户端”的工具如 FileZilla、WinSCP、MobaXterm 同时支持 FTP、FTPS、SFTP站点配置里直接切换协议即可不用换工具。3. 用命令行把上传下载跑通最小可用的 FTP 操作脚本3.1 手动登录与 ls 定位文件先确认服务器目录在写脚本之前我建议你至少手动用 FTP 客户端登录一次弄清登录后落在哪个目录。很多新手以为cd /data/backup里的/是根目录但在 chroot 环境下用户看到的/其实是服务器的/home/ftpuser绝对路径和相对路径的含义全变了。标准命令行登录方式ftp 192.168.1.20输入用户名密码后先用dir或ls确认能列出文件再用pwd看当前目录。lcd可以切换本地目录比如lcd /home/app/backup这样后续get下来的文件会写到这个目录。如果你发现自己登录后被固定在一个目录里这就是服务器端“指定ftp文件夹”的常见表现——用户被锁在自己的 home 目录了。这一步值得认真做因为后续脚本里所有的cd、get、put都依赖你对服务器目录结构的判断。我见过有人用ftp -n写脚本时在 chroot 环境里拼命cd /home/ftpuser/...结果在远端成了//home/ftpuser/...自然 550。3.2 下载与上传的核心命令get、put 与 mget、mput手动验证通过后就可以进入批量操作。核心命令是get和put批量版是mget和mput。以下是一段典型的交互ftp binary ftp get 20250412_backup.tar.gz ftp put app_release_v1.2.zip ftp prompt off ftp mget *.tar.gz ftp mput *.png这里的顺序和参数说明是踩坑重点binary必须放在任何传输命令之前用 ASCII 模式传二进制文件会把文件改坏尤其是.tar.gz和图片。get和put都只针对单文件不支持通配符批量必须用mget/mput。prompt off告诉 FTP 客户端在批量传输时不要一个个确认。mget *.tar.gz这种通配符是在服务器端展开的有些服务器不支持或者目录里文件太多导致展开超限先dir看到结果再匹配更稳。如果只是偶尔手动传一两个文件到这里已经够用了。但如果你要每天同步就别再敲交互命令了直接跳到下一节。3.3 用批处理/Shell 脚本实现定时同步把上面的交互变成非交互脚本常见做法是使用ftp -n加上 here-doc把命令一次性喂进去。下面这个脚本会从远端拉取所有.tar.gz文件到本地指定目录适合做备份同步#!/bin/bash FTP_HOST192.168.1.20 FTP_USERsyncuser FTP_PASS你的密码 REMOTE_DIR/data/export LOCAL_DIR/home/app/backup cd $LOCAL_DIR || exit 1 ftp -n $FTP_HOST EOF quote USER $FTP_USER quote PASS $FTP_PASS binary cd $REMOTE_DIR lcd $LOCAL_DIR prompt off mget *.tar.gz bye EOF-n参数表示不要在启动时自动登录后面用quote USER和quote PASS手动提交凭证。lcd是本地切换目录脚本开头也要先cd否则 crontab 执行时的当前目录往往不是你想要的。你可以把它加入 crontab30 2 * * * /opt/scripts/ftp_sync.sh /var/log/ftp_sync.log 21注意把密码明文写在脚本里终究不安全。一个折中做法是把密码放在一个权限 600 的文件里脚本启动时读取至少别让同主机其他用户一眼看到。如果服务器支持 lftp更好的方案是lftp -u $FTP_USER -p 21 $FTP_HOST -e set ftp:passive yes; mirror -R /local/path /remote/path; exitmirror自带增量同步、断线重连和连续性检测比裸的mget可靠得多。当然lftp 也需要单独安装。4. 用 Python 封装 FTP 上传下载断点续传与进度可控4.1 ftplib 最小实现登录、切换目录、下载命令行方案适合一次性同步但如果你想在业务代码里控制错误、打印进度、做条件判断Python 的ftplib是标准库不需要额外装第三方包。最小实现如下from ftplib import FTP ftp FTP() ftp.connect(192.168.1.20, 21, timeout30) ftp.login(syncuser, password) ftp.set_pasv(True) # 被动模式和 curl --ftp-pasv 等价 ftp.cwd(/data/backup) with open(backup.tar.gz, wb) as fp: ftp.retrbinary(RETR backup.tar.gz, fp.write) ftp.quit()逻辑说明connect()只建立 TCP 连接真正的认证在login()。set_pasv(True)对应被动模式不写的话默认也通常是被动但显式写上能让代码意图更明确。retrbinary的第二个参数是一个回调函数FTP 每收到一块数据就调用一次fp.write所以它天然适合边下载边写文件。上传是对称操作with open(app.zip, rb) as fp: ftp.storbinary(STOR app.zip, fp)这里参数是STOR而不是RETR语义就是“存储”。一个容易混淆的点retrlines和storlines是给文本文件用的会在传输过程中做换行符转换。哪怕你传的是.txt也别用它们除非你明确知道需要转换。二进制模式在这里就是指retrbinary/storbinary和命令行里的binary命令一一对应。4.2 断点续传从本地已有大小续传大文件传到一半断开最理想的做法是从断点继续而不是从头再来。FTP 的断点续传原理是先查询远端文件大小再让本地文件从已有大小开始追加服务器端通过REST命令设置传输起始偏移量。ftplib 的retrbinary支持rest参数实现如下import os from ftplib import FTP REMOTE_NAME ubuntu-22.04.iso LOCAL_NAME ubuntu-22.04.iso ftp FTP() ftp.connect(192.168.1.20, 21, timeout30) ftp.login(syncuser, password) ftp.set_pasv(True) remote_size ftp.size(REMOTE_NAME) local_size os.path.getsize(LOCAL_NAME) if os.path.exists(LOCAL_NAME) else 0 if local_size remote_size: print(文件已存在且大小一致) ftp.quit() exit(0) with open(LOCAL_NAME, ab) as fp: ftp.retrbinary(fRETR {REMOTE_NAME}, fp.write, restlocal_size) ftp.quit()这里restlocal_size的作用是先发REST offset再发RETR相当于告诉服务器“从本地这个字节位置继续”。open(LOCAL_NAME, ab)是追加模式不会覆盖已有内容。上传断点续传稍微麻烦一点因为storbinary没有直接暴露rest参数。常见做法是自己先发命令local_size os.path.getsize(bigdata.csv) ftp.sendcmd(TYPE I) ftp.sendcmd(fREST {local_size}) with open(bigdata.csv, rb) as fp: ftp.storbinary(STOR bigdata.csv, fp)注意REST不是所有 FTP 服务器都支持vsftpd 默认支持Windows IIS FTP 要看版本。续传完成后必须再比对一次ftp.size()和本地大小否则有些服务器会在你REST失败时静默从头传导致最终文件是“旧半截 新全量”的错乱状态。4.3 上传下载的命名规范与临时文件策略传输中断最隐蔽的危害不是“文件没传完”而是“对方目录里出现了一个不完整的正式文件名”。比如你把app.zip传到一半断了服务器上的app.zip已经存在且有 500MB 里的 300MB后续程序读到它就会直接崩溃。所以我在所有自动化脚本里都强制使用“先传临时名再改名”的策略。上传时REMOTE_TMP app_v1.2.zip.tmp REMOTE_FINAL app_v1.2.zip with open(app_v1.2.zip, rb) as fp: ftp.storbinary(STOR REMOTE_TMP, fp) ftp.rename(REMOTE_TMP, REMOTE_FINAL)下载也是同理本地先写成backup.tar.gz.part成功后os.rename()到最终名。这个习惯能极大减少脏数据尤其是配合监控程序扫描目录时不会把半截文件当成有效数据。文件命名上再加一条时间戳规范app_20250412_1830.zip。同一天多次部署不会互相覆盖并且通过文件名就能判断哪个是最新版本。配合定期清理.tmp/.part的策略不至于让临时文件堆满磁盘。5. 避坑FTP 上传下载的 5 个高频翻车现场5.1 现象登录成功但一传文件就卡死原因绝大多数时候是被动模式端口没放行。控制连接走 21 端口没问题但数据连接需要服务器开放一个高端口号段这个段在配置文件里没固定防火墙和安全组也不知道于是客户端的数据请求被丢弃。解决把被动端口固定为一个区间例如 vsftpd 配置pasv_min_port40000 pasv_max_port40100然后防火墙放行firewall-cmd --add-port40000-40100/tcp --permanent firewall-cmd --reload云服务器还要去控制台安全组同步放行。Windows 的 IIS FTP 也有类似“FTP 防火墙支持”的外部 IP 和端口范围设置别漏。5.2 现象中文文件名变乱码原因就是 2.2 节说的字符集不一致。客户端按 UTF-8 解析服务器发来的 GBK 字节或者反过来文件名就成了满屏问号或乱码。解决FileZilla 在站点管理器里强制字符集为 UTF-8如果用 Python在登录前设置ftp.encoding gbk或utf-8需要和服务器保持一致。lftp 用户用set ftp:charset UTF-8。最省心的做法是把所有服务器统一成 UTF-8Windows 老旧服务上尽量减少中文文件名用拼音或日期代替。5.3 现象提示“550 Permission denied”或“没有权限复制文件”这个报错在 Windows 客户端先上传到服务器时尤其常见。原因通常是目标目录对 FTP 用户没有写权限或者用户被 chroot 后试图访问职责范围以外的路径。SELinux 开启的 Linux 服务器上即使目录权限是 777也可能被放行策略拦截。解决先pwd确认自己被锁在哪个目录然后确认目标目录的属主和权限。比如chown ftpuser:ftpgroup /home/ftpuser/upload chmod 750 /home/ftpuser/uploadCentOS/RHEL 上还要检查 SELinuxsetsebool -P ftpd_full_access 1否则你会看到权限明明正确服务器却持续报 550 的诡异场景。5.4 现象下载下来的压缩包解压报错文本文件多了 ^M原因是用了 ASCII 模式传输二进制文件。ASCII 模式下客户端会把换行符转换成本地格式tar.gz、图片、Excel 这类文件一旦经过转换就废了。解决命令行交互时先敲binaryheredoc 脚本里也要确保binary在mget之前。用 Python 就坚持retrbinary/storbinary。Windows 自带 ftp 客户端特别喜欢默认走 ASCII这是它被骂的主要原因之一。5.5 现象脚本里的密码被 history 或日志泄露把密码直接写在命令行参数里bash history 会忠实记录FTP 客户端调试模式下还会把PASS xxxx回显到终端和日志文件这样的翻车现场我在同事的cleartext运维脚本里见过不止一次。解决把凭证放到~/.netrc并设置权限 600machine 192.168.1.20 login syncuser password xxxxxxPython 脚本则从环境变量读取import os ftp.login(os.getenv(FTP_USER), os.getenv(FTP_PASS))这样既不进 history也不进普通日志。CI/CD 流水线里更简单直接把密码配置成 Secret 环境变量注入。6. 进阶把 FTP 上传下载变成可监控的自动化任务6.1 校验文件完整性size 对比与 MD5下载完成后只对比文件大小不够因为相同大小也可能内容不同。FTP 协议本身没有标准哈希接口常见做法是让服务器端先生成一份md5sums.txt再连同文件一起下载最后在本地校验。import hashlib def md5_of_file(path, block1024 * 1024): m hashlib.md5() with open(path, rb) as f: while chunk : f.read(block): m.update(chunk) return m.hexdigest()用法下载任务开始时先拉取md5sums.txt等所有文件落地后遍历该文件里的记录并调用md5_of_file()做比对。不匹配就把文件移到corrupt/目录并触发重传。这一个步骤能挡住大部分网络中间层静默损坏的情况。6.2 失败重试、日志留痕与“ftp监控”自动任务一旦挂到 crontab 里最怕的就是失败后静默。推荐把下载逻辑包成一个带指数退避重试的函数import time from ftplib import FTP, error_temp def download_with_retry(host, user, passwd, remote_path, local_path, retries3, timeout30): for attempt in range(retries): try: ftp FTP() ftp.connect(host, 21, timeouttimeout) ftp.login(user, passwd) ftp.set_pasv(True) with open(local_path, wb) as fp: ftp.retrbinary(fRETR {remote_path}, fp.write) ftp.quit() return True except (error_temp, TimeoutError, ConnectionError) as e: print(fattempt {attempt 1} failed: {e}) time.sleep(2 ** attempt) return False参数说明error_temp是 ftplib 对 4xx 临时错误的异常类遇到它时等待1s、2s、4s后重试最多尝试 3 次。如果最终失败必须把失败信息写入日志文件并在日志里有明确的文件名、远端路径和执行时间。“ftp监控”这件事我的做法是每次任务结束都往日志里输出一行files: 12, bytes: 345678901, failed: false然后由一个独立的检查脚本扫描日志如果failed: true或者最近 24 小时没有新日志就调用 webhook 或邮件通知。不要依赖 FTP 服务器本身发告警多数服务端没有这个机制。我最早写 FTP 同步脚本时只打成功日志失败时一片空白结果某天备份文件没传上来我查了整天才发现是磁盘满了、日志空洞。那之后所有任务都强制写入失败日志并且加了文件数和字节数变化曲线配合定时任务持续观察。这套习惯救了我很多次希望你也能在第一次翻车前就用上。希望帮到你。本文还有配套的精品资源点击获取
返回列表