Ruby标准库中的Net::FTP提供了完整的FTP客户端能力,但实际开发中围绕文件上传下载、目录遍历以及被动模式异常处理仍然有不少细节需要留意。很多项目只是简单调用putbinaryfile或getbinaryfile,一旦部署到有防火墙或NAT网关的环境,就会出现连接超时、命令无响应等问题。本文将通过可运行的代码示例,逐步拆解这些场景的实现方式与排错思路。

使用Net::FTP建立连接与登录
Net::FTP是Ruby标准库自带的FTP客户端实现,不需要额外安装gem。它的基本用法是创建Net::FTP实例,然后依次调用connect、login方法。connect方法负责建立控制连接,默认端口是21。login方法接受用户名和密码,如果匿名登录可以省略密码。连接成功后,可以设置passive属性来决定使用主动模式还是被动模式。
以下是一个最基础的连接示例,包含了异常捕获和超时设置。Net::FTP的open_timeout和read_timeout可以避免连接阶段无限挂起。open_timeout控制TCP连接建立的超时时间,read_timeout控制读取服务器响应时的等待时间。在生产环境中,这两个参数建议都显式设置,否则默认的60秒可能让调用方等待过久。
require 'net/ftp'
ftp = Net::FTP.new
ftp.open_timeout = 10
ftp.read_timeout = 30
begin
ftp.connect('ftp.ippipp.com', 21)
ftp.login('username', 'password')
ftp.passive = true
puts '连接成功'
rescue SocketError, Errno::ETIMEDOUT, Net::FTPPermError => e
puts "连接或登录失败: #{e.message}"
ensure
ftp.close unless ftp.closed?
end
这里有两点需要注意。第一,ftp.passive = true必须在登录之后、数据操作之前设置,否则某些服务器可能在登录后立即开始数据传输,导致模式切换失效。第二,closed?方法可以用来判断控制连接是否已经关闭,避免在ensure块中重复关闭引发异常。如果FTP服务器支持TLS,则需要使用Net::FTPTLS子类而不是普通的Net::FTP。
文件上传与下载的完整实现
Net::FTP为文件传输提供了多种方法,其中最常用的是putbinaryfile和getbinaryfile。这两个方法分别对应二进制模式的上传和下载,适合图片、压缩包、可执行文件等非文本内容。对于纯文本文件,可以使用puttextfile和gettextfile,它们会处理换行符转换,但在跨平台场景下容易引入编码问题,因此大多数项目统一使用二进制模式更安全。
下面这段代码展示了如何上传一个本地文件到FTP服务器,并在上传完成后验证远程文件大小。注意putbinaryfile的第一个参数是本地文件路径,第二个参数是远程文件名,可以使用相对路径或绝对路径。如果远程目录不存在,需要先调用ftp.mkdir创建目录,否则上传会抛出Net::FTPPermError。
local_file = '/data/backup/app.tar.gz'
remote_file = 'backup/app.tar.gz'
begin
ftp.chdir('backup')
rescue Net::FTPPermError
ftp.mkdir('backup')
ftp.chdir('backup')
end
ftp.putbinaryfile(local_file, remote_file, 1024 * 1024)
remote_size = ftp.size(remote_file)
local_size = File.size(local_file)
if remote_size == local_size
puts '上传完成且大小校验通过'
else
puts "大小不一致: 本地#{local_size}, 远程#{remote_size}"
end
下载的流程与上传类似。使用getbinaryfile将远程文件保存到本地路径。如果本地路径存在同名文件,方法会直接覆盖,不会给出确认。因此下载前最好检查本地文件是否需要备份。另外,下载大文件时建议设置一个合理的块大小,默认值是1024字节,改为1024 * 1024可以减少循环次数,提升传输效率。
remote_file = 'backup/app.tar.gz'
local_file = '/tmp/download/app.tar.gz'
FileUtils.mkdir_p(File.dirname(local_file))
ftp.getbinaryfile(remote_file, local_file, 1024 * 1024)
if File.exist?(local_file)
puts "下载成功: #{local_file}, 大小: #{File.size(local_file)} 字节"
end
如果只是单纯下载文本文件,并且希望在读取时逐行处理,可以使用ftp.gettextfile配合一个块。不过要注意,gettextfile内部会将以换行符进行转换,如果FTP服务器返回的是CRLF,Ruby可能会将其转换为本机的换行符,这在Linux环境下可能表现为每行末尾多出多余的r字符。因此对内容敏感的场景,先用二进制模式下载到临时文件,再用Ruby的File处理会更加可控。
目录遍历与文件列表获取
Net::FTP提供了nlst和list两个方法获取目录内容。其中nlst返回一个文件名数组,只包含文件或目录名,不包含大小、修改时间等元数据。list返回原始的行字符串数组,格式与Unix的ls -l输出类似,需要自行解析才能得到文件大小和日期。对于需要递归遍历整个FTP目录树的场景,通常使用nlst配合chdir和递归调用。
下面的代码实现了一个递归遍历FTP目录的函数,将发现的文件路径收集到一个数组中。为了避免无限递归,新增了深度限制参数。每个目录先进入,列出子项,判断哪些是目录,再继续递归。判断目录的方法没有标准API,一种常见做法是尝试ftp.chdir(item)并捕获异常,如果成功则说明是目录,再返回上级目录继续遍历。
def list_all_files(ftp, current_dir = '.', max_depth = 5, depth = 0)
return [] if depth > max_depth
files = []
ftp.chdir(current_dir)
ftp.nlst.each do |item|
next if item == '.' || item == '..'
path = "#{current_dir}/#{item}"
begin
ftp.chdir(item)
files.concat(list_all_files(ftp, path, max_depth, depth + 1))
ftp.chdir('..')
rescue Net::FTPPermError
files << path
end
end
files
end
ftp.chdir('/')
all_files = list_all_files(ftp)
puts all_files
这种实现有一个缺点,chdir(item)在主动模式下可能因为数据连接问题而抛出异常,即使目标确实是目录。为了提高判断的准确性,可以结合ftp.list的输出,如果行首字符是d则代表目录,如果是-则代表普通文件。解析list输出的格式依赖服务器,常见的是Unix格式,但Windows FTP服务器的输出格式有所不同。在跨平台项目中,建议封装一个兼容层,只依赖nlst并捕获异常,牺牲一点精确性换取通用性。
另一个值得注意的点是,nlst返回的文件名可能包含中文或空格,调用chdir或传输方法时务必保留原始字符串,不要做额外的strip或编码转换。某些FTP服务器在UTF-8环境下会返回乱码,如果Ruby脚本运行环境的默认编码不是UTF-8,文件操作也可能报编码错误。可以在脚本开头设置Encoding.default_external = 'UTF-8'来减少此类问题。
被动模式异常处理与常见陷阱
被动模式是FTP客户端最常用的连接方式,因为它对客户端防火墙友好。Net::FTP默认将passive属性设置为true,也就是说在默认情况下,数据连接会由客户端向服务器指定的IP和端口发起。然而很多网络环境会修改FTP服务器返回的IP地址,或者防火墙只允许特定端口范围,导致客户端发起的数据连接一直超时。
典型的异常症状是,控制连接正常,可以登录、可以执行nlst或list,但在执行上传或下载时,卡在227 Entering Passive Mode之后毫无响应,最终抛出Errno::ETIMEDOUT或Net::FTPTempError。这是因为服务器返回的被动模式IP可能是内网地址,客户端无法直接访问。解决办法之一是强制使用主动模式,即设置ftp.passive = false。但主动模式要求客户端对外开放数据端口,这在云服务器或容器环境中可能同样受限。
begin
ftp.putbinaryfile(local_file, remote_file)
rescue Errno::ETIMEDOUT, Net::FTPTempError => e
puts "被动模式传输超时,尝试切换主动模式: #{e.message}"
ftp.passive = false
begin
ftp.putbinaryfile(local_file, remote_file)
puts '主动模式上传成功'
rescue Net::FTPError => ex
puts "主动模式也失败: #{ex.message}"
end
end
有些FTP服务器只支持被动模式而不允许主动模式,切换后可能直接返回500 Illegal PORT command。这种情况下只能让服务器管理员配置正确的被动模式IP和端口范围。客户端还可以通过设置ftp.passive = true后,检查ftp.last_response来确认服务器返回的状态码和地址信息。如果返回的地址与服务器主机名解析出的公网IP不一致,说明存在NAT穿透问题。
另一个隐蔽的坑是Net::FTP在被动模式下,每次数据操作后会自动关闭数据连接,但控制连接保持打开。频繁的小文件传输会消耗大量数据连接,如果服务器有连接数限制,就会出现间歇性失败。可以通过复用同一个FTP实例并保持控制连接来缓解,但不要并发使用同一个实例,因为Net::FTP不是线程安全的。如果需要在多线程中上传下载,每个线程应创建独立的Net::FTP对象。
总结来说,开发稳定的Ruby FTP客户端需要关注三层问题:控制连接的建立与超时设置、数据连接模式的选择与降级策略、以及目录遍历和文件操作的异常处理。被动模式异常是部署阶段最常见的问题,通过理解Net::FTP的默认行为和捕获具体的异常类型,可以构建出更加健壮的FTP自动化工具。
Ruby FTP客户端被动模式文件上传下载修改时间:2026-08-19 11:13:13