Python ftplib二进制传输模式上传文件无法执行结束
问题根因
- 挂起问题和你之前下载遇到的故障完全同源:隐式FTPS模式下,原生ftplib不会自动给数据传输通道的套接字包装SSL层。控制通道发送
STOR指令收到150响应后,客户端会尝试建立明文数据连接,但隐式FTPS服务端要求所有连接(含数据传输连接)必须走SSL加密,双方握手不匹配直接卡死,不会抛出常规可捕获的异常,所以进程会一直挂住没有响应。 - 你之前实现的下载逻辑只适配了
retrbinary的传输场景,没有针对storbinary上传做兼容,也没有给上传流程加僵死连接检测、超时断开、重连逻辑,哪怕是小文件也会卡在数据连接建立阶段,和文件大小、传输速率无关。 - 原生
ftplib.FTP_TLS本身存在已知bug:调用prot_p()开启数据通道加密后,被动模式下建立数据连接时的SSL包装逻辑在隐式FTPS端口(默认990)下不会自动触发,这个bug是隐式FTPS传输挂起的核心诱因。
稳定上传实现方案
你不需要重构之前已经跑通的隐式FTPS下载逻辑,只需要在原有PyFTPclient类中补充上传方法,复用已有的SSL自动包装、速率监控、重连逻辑即可,核心调整点包括:
- 所有数据连接强制走SSL包装,复用已有的
ImplicitFTP_TLS子类 - 上传前给套接字配置TCP保活参数,避免中间网络设备掐断空闲连接
- 复用下载逻辑的定时监控机制,检测到传输速率为0时主动断开僵死连接触发重连
- 支持断点续传,每次重连时先查询服务端已存文件大小,从断点位置续传
- 配置最大重试次数,避免无限重连导致进程永久挂死
- 传输结束后显式校验服务端返回的
226 Transfer complete响应,确认传输真实完成
可直接复用的完整代码如下,你之前写好的类和方法无需修改,直接补充UploadFile方法即可:
import os import time import logging import socket import threading import ssl from ftplib import FTP_TLS class ImplicitFTP_TLS(FTP_TLS): """FTP_TLS subclass that automatically wraps sockets in SSL to support implicit FTPS.""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._sock = None @property def sock(self): """Return the socket.""" return self._sock @sock.setter def sock(self, value): """When modifying the socket, ensure that it is ssl wrapped.""" if value is not None and not isinstance(value, ssl.SSLSocket): value = self.context.wrap_socket(value) self._sock = value def setInterval(interval, times = -1): def outer_wrap(function): def wrap(*args, **kwargs): stop = threading.Event() def inner_wrap(): i = 0 while i != times and not stop.isSet(): stop.wait(interval) function(*args, **kwargs) i += 1 t = threading.Timer(0, inner_wrap) t.daemon = True t.start() return stop return wrap return outer_wrap class PyFTPclient: def __init__(self, host, port = 990, login = 'anonymous', passwd = 'anonymous', monitor_interval = 30): self.host = host self.port = port self.login = login self.passwd = passwd self.monitor_interval = monitor_interval self.ptr = None self.max_attempts = 15 self.waiting = True def get_files_names(self, dir_score='/'): ftp_client = ImplicitFTP_TLS() ftp_client.connect(host=self.host, port=self.port) ftp_client.login(user=self.login, passwd=self.passwd) ftp_client.prot_p() ftp_client.cwd(dir_score) files = [] ftp_client.dir(files.append) ftp_client.quit() return files def DownloadFile(self, dst_filename, local_filename = None): res = '' if local_filename is None: local_filename = dst_filename.split('/')[-1] with open(local_filename, 'w+b') as f: self.ptr = f.tell() @setInterval(self.monitor_interval) def monitor(): if not self.waiting: i = f.tell() if self.ptr < i: logging.debug("%d - %0.1f Kb/s" % (i, (i-self.ptr)/(1024*self.monitor_interval))) self.ptr = i else: ftp.close() def connect(): ftp.connect(self.host, self.port) ftp.login(self.login, self.passwd) ftp.prot_p() ftp.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) ftp.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 75) ftp.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 60) ftp = ImplicitFTP_TLS() ftp.set_debuglevel(2) ftp.set_pasv(True) connect() ftp.voidcmd('TYPE I') dst_filesize = ftp.size(dst_filename) mon = monitor() while dst_filesize > f.tell(): try: connect() self.waiting = False res = ftp.retrbinary('RETR %s' % dst_filename, f.write) if f.tell() == 0 else \ ftp.retrbinary('RETR %s' % dst_filename, f.write, rest=f.tell()) except: self.max_attempts -= 1 if self.max_attempts == 0: mon.set() logging.exception('') raise self.waiting = True time.sleep(30) mon.set() ftp.close() if not res.startswith('226 Transfer complete'): return None return 1 # 新增的上传方法 def UploadFile(self, local_filename, dst_filename = None): res = '' if dst_filename is None: dst_filename = os.path.basename(local_filename) local_filesize = os.path.getsize(local_filename) with open(local_filename, 'rb') as f: self.ptr = 0 @setInterval(self.monitor_interval) def monitor(): if not self.waiting: i = f.tell() if self.ptr < i: logging.debug("%d - %0.1f Kb/s" % (i, (i-self.ptr)/(1024*self.monitor_interval))) self.ptr = i else: ftp.close() def connect(): ftp.connect(self.host, self.port) ftp.login(self.login, self.passwd) ftp.prot_p() ftp.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) ftp.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 75) ftp.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 60) ftp = ImplicitFTP_TLS() ftp.set_debuglevel(2) ftp.set_pasv(True) connect() ftp.voidcmd('TYPE I') mon = monitor() rest_pos = 0 # 首次连接查询服务端已存文件大小,确定续传起点 try: rest_pos = ftp.size(dst_filename) if rest_pos is None: rest_pos = 0 except: rest_pos = 0 while local_filesize > rest_pos: try: connect() self.waiting = False f.seek(rest_pos) res = ftp.storbinary(f'STOR {dst_filename}', f, rest=rest_pos) rest_pos = f.tell() except Exception as e: self.max_attempts -= 1 if self.max_attempts == 0: mon.set() logging.exception('Upload failed after max retries') raise self.waiting = True time.sleep(30) # 重连后重新查询断点位置 try: connect() rest_pos = ftp.size(dst_filename) if rest_pos is None: rest_pos = 0 except: rest_pos = 0 mon.set() ftp.close() if not res.startswith('226 Transfer complete'): return None return 1 # 调用示例 if __name__ == '__main__': obj = PyFTPclient(FTP_HOST, port=FTP_PORT, login=FTP_USER, passwd=FTP_PWD) obj.UploadFile(local_filename=path_local_file, dst_filename=path_remote_save_path)
配置注意事项
- 隐式FTPS默认端口是990,不要使用明文FTP的21端口,21端口是显式FTPS/明文FTP的默认端口
- 如果网络环境较差,可以把
monitor_interval参数调小到10-15秒,更快识别僵死连接 - 上传文件必须用
rb二进制只读模式打开,禁止用文本模式,否则会出现文件损坏、大小不匹配的问题 - 如果服务端不支持断点续传,可以删掉
rest相关逻辑,每次重连直接从文件头开始传输即可
内容的提问来源于stack exchange,提问作者vitormgou
相关产品推荐
相关产品推荐

