如何让Ruby Net::SFTP非阻塞,实现远程文件流式下载?
解决Net::SFTP流式传输大文件,让浏览器立即响应下载
我太懂你这个痛点了——1GB的文件要等全下载完才让浏览器动,用户肯定以为卡了!其实你之前用的download/download!方法都是全量下载到本地/内存后再输出,自然会有延迟。Net::SFTP本身支持流式读取,我们换个方式直接逐块读取远程文件,边读边给浏览器发数据就行。
核心思路
通过Net::SFTP的open方法获取远程文件的句柄,然后循环读取固定大小的文件块,每读一块就立即输出到HTTP响应流并强制刷新,让浏览器实时接收数据,不用等整个文件下载完成。
具体实现代码(以Rails为例)
def stream_large_sftp_file # 先设置HTTP响应头,告诉浏览器这是下载文件,同时禁用缓存避免延迟 response.headers['Content-Type'] = 'application/zip' response.headers['Content-Disposition'] = 'attachment; filename="test.zip"' response.headers['Cache-Control'] = 'no-cache, no-store' response.headers['X-Accel-Buffering'] = 'no' # 禁用Nginx等反向代理的缓存,确保数据实时传输 # 建立SFTP连接(建议把连接逻辑抽成工具类,避免重复代码) sftp = Net::SFTP.start('你的远程主机IP', '用户名', password: '你的密码') remote_file_path = '/远程服务器上的路径/test.zip' file_total_size = sftp.lstat(remote_file_path).size # 打开远程文件句柄,开始流式读取 sftp.open(remote_file_path, 'r') do |remote_file| block_size = 8192 # 每次读8KB,可根据服务器性能调整(比如16KB/32KB) bytes_read = 0 while bytes_read < file_total_size # 读取一块数据 chunk = remote_file.read(block_size) break unless chunk # 处理文件读取结束的情况 # 输出到响应流并强制刷新,确保数据立刻发往浏览器 response.stream.write(chunk) response.stream.flush bytes_read += chunk.size end end ensure # 确保资源被正确释放,避免内存泄漏 sftp.close if sftp response.stream.close end
关键细节说明
X-Accel-Buffering: no:如果你的应用前面有Nginx等反向代理,一定要加这个头,不然代理会缓存整个文件后再发给浏览器,还是会有延迟。- 块大小调整:块太小会增加IO次数,块太大可能占用过多内存,8KB-32KB是比较均衡的选择。
- 异常处理:可以在代码里添加
rescue块捕获Net::SFTP::Exception等异常,给用户返回友好的错误提示(比如“下载失败,请稍后重试”)。 - Web服务器支持:确保你的Ruby Web服务器(比如Puma、Unicorn)支持流式响应,大部分主流服务器默认都支持,但如果是老旧版本可能需要调整配置。
Sinatra环境下的适配示例
如果用的是Sinatra,逻辑类似,只是响应流的写法略有不同:
get '/download-test-zip' do content_type 'application/zip' headers['Content-Disposition'] = 'attachment; filename="test.zip"' headers['Cache-Control'] = 'no-cache, no-store' headers['X-Accel-Buffering'] = 'no' sftp = Net::SFTP.start('远程主机', '用户名', password: '密码') remote_path = '/path/to/test.zip' file_size = sftp.lstat(remote_path).size stream do |out| sftp.open(remote_path, 'r') do |file| block_size = 8192 bytes_read = 0 while bytes_read < file_size chunk = file.read(block_size) break unless chunk out.write(chunk) out.flush bytes_read += chunk.size end end ensure sftp.close if sftp end end
这样修改后,浏览器应该会立即弹出下载提示,并且能看到实时的下载进度,不用再等几分钟啦!
内容的提问来源于stack exchange,提问作者gdonald
相关产品推荐
相关产品推荐

