Ruby的标准库Net::FTP用起来非常方便,几行代码就能完成文件上传下载。但不少人在用它传输图片、zip压缩包时遇到过诡异的文件损坏问题:下载下来的图片打不开,压缩包解压报错,文件大小还比源文件小了一些。如果排查一圈发现网络、权限、磁盘都没问题,那大概率是踩中了FTP的ASCII模式陷阱——换行符转换悄悄改写了二进制数据。这篇文章就来把这个坑彻底讲清楚。

FTP协议中的两种传输模式是怎么工作的
FTP协议在设计之初就定义了两种数据传输模式:ASCII模式和二进制模式(也叫Image模式)。ASCII模式的设计意图是解决不同操作系统之间换行符不统一的问题。Windows系统用\r\n表示换行,Unix和Linux用\n,老式Mac系统用\r。当客户端以ASCII模式接收数据时,FTP客户端库会按照约定把服务端发来的CRLF序列转换成本地系统的换行符;发送时则做相反的转换。
这个机制对纯文本文件来说很贴心,但对二进制文件就是灾难。一张JPEG图片、一个zip压缩包,它们的数据流中任何位置都可能出现0x0D 0x0A这样的字节序列,这只是一种巧合的字节组合,和换行毫无关系。可ASCII模式的转换逻辑不区分语义,只要看到CRLF就动手转换。如果在Linux上下载Windows服务器传来的文件,CRLF会被替换成LF,文件直接少了一个字节,后续所有数据的偏移量全部错位,文件自然就废了。更麻烦的是,损坏往往发生在文件中间的某些位置,文件头可能还是完好的,一些宽松的查看器甚至能打开半张图,这让排查变得更加困难。
还有一个容易忽视的细节:ASCII模式转换是逐块进行的,如果CRLF恰好被切在两个数据块的边界上,也就是\r在上一块末尾、\n在下一块开头,某些实现处理不当还会产生额外的转换错误。所以结论很明确:传输任何非纯文本内容,必须使用二进制模式。
Net::FTP中各方法的行为差异与默认陷阱
Ruby的Net::FTP在老版本中默认传输模式是ASCII,从Ruby 2.5之后虽然默认行为有所调整,但理解各个方法仍然很重要。先看一个典型的踩坑代码:
require 'net/ftp'
ftp = Net::FTP.new
ftp.connect('192.168.0.1', 21)
ftp.login('user', 'password')
# 危险写法:不确定当前模式,直接下载
ftp.get('photo.jpg', 'photo_local.jpg')
ftp.close问题在于get方法会根据当前会话的传输模式决定行为,而模式是可以被之前的方法调用改变的。Net::FTP提供的方法分两类:一类是gettextfile和puttextfile,它们强制使用ASCII模式,专门用于文本传输,会执行换行符转换;另一类是getbinaryfile和putbinaryfile,它们强制使用二进制模式,原样传输每一个字节。
而通用的get、put、getbinaryfile的别名等方法的行为取决于当前的模式状态。Net::FTP实例上有binary和text两个方法可以切换模式,调用ftp.binary会把后续传输切换为二进制模式。一段代码里如果先调用了gettextfile下载了一个配置文件,模式就被切到了ASCII,紧接着用get去下载压缩包,就中招了。来看正确的写法:
require 'net/ftp'
ftp = Net::FTP.new
ftp.connect('192.168.0.1', 21)
ftp.login('user', 'password')
# 方式一:显式声明二进制模式
ftp.binary
ftp.get('archive.zip', 'archive_local.zip')
# 方式二:使用明确的二进制方法,更推荐
ftp.getbinaryfile('archive.zip', 'archive_local.zip')
ftp.close我更推荐方式二,也就是始终使用getbinaryfile和putbinaryfile这两个语义明确的方法。它们不依赖会话当前的模式状态,代码可读性也更好,半年后回来看代码的人一眼就能明白传输意图。此外要注意,gettextfile和puttextfile在执行完毕后会把会话模式切回(或保持在)对应状态,这种隐式的模式切换是很多意外损坏的根源。
如何检测和防范文件被悄悄改写
ASCII模式造成的损坏有个隐蔽特点:传输过程不报任何错误,返回码正常,文件也完整落地了,只是内容变了。所以事后检测非常重要,最直接的手段是对比文件哈希值。在服务端和客户端分别计算MD5或SHA256,一旦不一致就要怀疑传输模式问题。
require 'net/ftp'
require 'digest'
ftp = Net::FTP.new
ftp.connect('192.168.0.1', 21)
ftp.login('user', 'password')
ftp.getbinaryfile('data.bin', 'data_local.bin')
ftp.close
local_hash = Digest::SHA256.file('data_local.bin').hexdigest
puts local_hash
# 与服务端计算的哈希值比对,一致才说明传输无损除了哈希校验,还有几个实践建议值得采纳。第一,封装一个统一的下载方法,内部固定调用getbinaryfile,把模式选择这件事从业务代码中彻底拿掉,团队里任何人调用都不需要关心模式问题。第二,对文件类型做白名单判断,如果是.txt、.csv这类纯文本且确实需要本地化换行符的场景,再走gettextfile;其余一律二进制。第三,如果文本文件其实不需要换行符转换(比如后续要按字节解析或直接入库),也应该用二进制模式传输,之后再用gsub显式处理换行符,把转换逻辑从传输层挪到应用层,控制权更清晰。
最后补充一点,被动模式(passive mode)与传输模式是两个完全不同的概念,前者解决的是防火墙环境下数据连接建立的问题,与换行符转换无关,不要因为配置了ftp.passive = true就以为万事大吉。理解ASCII与二进制模式的本质区别,配合语义明确的方法调用和事后哈希校验,就能从根本上避免这类文件损坏问题。
Ruby Net::FTPASCII模式二进制文件传输修改时间:2026-09-10 12:48:34