Apache Commons FTPClient挂起

我们使用以下Apache Commons Net FTP代码连接到FTP服务器,轮询某些目录以查找文件,如果找到文件,则将它们检索到本地计算机:

try { logger.trace("Attempting to connect to server..."); // Connect to server FTPClient ftpClient = new FTPClient(); ftpClient.setConnectTimeout(20000); ftpClient.connect("my-server-host-name"); ftpClient.login("myUser", "myPswd"); ftpClient.changeWorkingDirectory("/loadables/"); // Check for failed connection if(!FTPReply.isPositiveCompletion(ftpClient.getReplyCode())) { ftpClient.disconnect(); throw new FTPConnectionClosedException("Unable to connect to FTP server."); } // Log success msg logger.trace("...connection was successful."); // Change to the loadables/ directory where we poll for files ftpClient.changeWorkingDirectory("/loadables/"); // Indicate we're about to poll logger.trace("About to check loadables/ for files..."); // Poll for files. FTPFile[] filesList = oFTP.listFiles(); for(FTPFile tmpFile : filesList) { if(tmpFile.isDirectory()) continue; FileOutputStream fileOut = new FileOutputStream(new File("tmp")); ftpClient.retrieveFile(tmpFile.getName(), fileOut); // ... Doing a bunch of things with output stream // to copy the contents of the file down to the local // machine. Ommitted for brevity but I assure you this // works (except when the WAR decides to hang). // // This was used because FTPClient doesn't appear to GET // whole copies of the files, only FTPFiles which seem like // file metadata... } // Indicate file fetch completed. logger.trace("File fetch completed."); // Disconnect and finish. if(ftpClient.isConnected()) ftpClient.disconnect(); logger.trace("Poll completed."); } catch(Throwable t) { logger.trace("Error: " + t.getMessage()); } 

我们计划每分钟运行一次。 当部署到Tomcat(7.0.19)时,此代码加载完全正常并开始无故障地工作。 每一次,在某些时候,它似乎只是挂起 。 我的意思是:

  • 不存在堆转储
  • Tomcat仍然在运行(我可以看到它的pid并可以登录到web管理器应用程序)
  • 在经理应用程序内部,我可以看到我的WAR仍在运行/启动
  • catalina.out和我的特定于应用程序的日志显示没有任何exception被抛出的迹象

所以JVM仍然在运行。 Tomcat仍在运行,我部署的WAR仍在运行,但它只是挂起。 有时它运行2个小时然后挂起; 其他时候它运行了几天然后挂起。 但是当它挂起时,它会在读取About to check loadables/ for files... (我在日志中看到)和读取File fetch completed.的行之间执行此操作File fetch completed. (我没看到)。

这告诉我在文件的实际轮询/获取期间发生了挂起,这种指向与我能够找到哪个与FTPClient死锁有关的问题的方向相同。 这让我想知道这些是否是同样的问题( 如果是,我会很乐意删除这个问题! )。 但是我不认为相信它们是相同的(我在日志中没有看到相同的例外)。

一位同事提到它可能是一个“被动”与“主动”FTP的事情。 不是真的知道区别,我对FTPClient字段ACTIVE_REMOTE_DATA_CONNECTION_MODEPASSIVE_REMOTE_DATA_CONNECTION_MODE等有点困惑,并且不知道SO认为这是一个潜在的问题。

由于我在这里抓住Throwable作为最后的手段,如果出现问题,我本来希望在日志中看到一些东西。 因此,我觉得这是一个明确的问题。

有任何想法吗? 不幸的是,我对这里的FTP内部知识不够了解,无法做出明确的诊断。 这可能是服务器端的东西吗? 与FTP服务器有关?

这可能是一些事情,但你朋友的建议是值得的。

试试ftpClient.enterLocalPassiveMode(); 看它是否有帮助。

我还建议将断开连接在finally块中,以便它永远不会在那里留下连接。

昨天,我没睡觉,但我想我解决了这个问题。

您可以使用FTPClient.setBufferSize()增加缓冲区大小;

  /** * Download encrypted and configuration files. * * @throws SocketException * @throws IOException */ public void downloadDataFiles(String destDir) throws SocketException, IOException { String filename; this.ftpClient.connect(ftpServer); this.ftpClient.login(ftpUser, ftpPass); /* CHECK NEXT 4 Methods (included the commented) * they were very useful for me! * and icreases the buffer apparently solve the problem!! */ // ftpClient.addProtocolCommandListener(new PrintCommandListener(new PrintWriter(System.out), true)); log.debug("Buffer Size:" + ftpClient.getBufferSize()); this.ftpClient.setBufferSize(1024 * 1024); log.debug("Buffer Size:" + ftpClient.getBufferSize()); /* * get Files to download */ this.ftpClient.enterLocalPassiveMode(); this.ftpClient.setAutodetectUTF8(true); //this.ftpClient.setFileType(FTP.BINARY_FILE_TYPE); this.ftpClient.enterLocalPassiveMode(); FTPFile[] ftpFiles = ftpClient .listFiles(DefaultValuesGenerator.LINPAC_ENC_DIRPATH); /* * Download files */ for (FTPFile ftpFile : ftpFiles) { // Check if FTPFile is a regular file if (ftpFile.getType() == FTPFile.FILE_TYPE) { try{ filename = ftpFile.getName(); // Download file from FTP server and save fos = new FileOutputStream(destDir + filename); //I don't know what useful are these methods in this step // I just put it for try this.ftpClient.enterLocalPassiveMode(); this.ftpClient.setFileType(FTP.BINARY_FILE_TYPE); this.ftpClient.setAutodetectUTF8(true); this.ftpClient.enterLocalPassiveMode(); ftpClient.retrieveFile( DefaultValuesGenerator.LINPAC_ENC_DIRPATH + filename, fos ); }finally{ fos.flush(); fos.close(); } } } if (fos != null) { fos.close(); } } 

我希望这段代码对某人有用!

我必须在登录后包含以下内容才能调用s.listFiles并在没有“挂起”的情况下进行传输并最终失败:

 s.login(username, password); s.execPBSZ(0); s.execPROT("P"); 

尝试从Linux计算机执行列表文件到IIS服务器时,我遇到了同样的问题。 代码在我的开发人员工作站上工作得很好,但是在服务器上运行时会因为防火墙混淆而混乱。

必须按顺序执行这些操作并要求您扩展FTPSClient 3.5

  1. connect(implicit = true,SSLContext = TLS)
  2. 检查isPositiveCompletion
  3. validation(当然)
  4. execPBSZ(0)
  5. execPROT( “P”)
  6. 设置布尔值以指示跳过被动IP(自定义FTPSClient类)
  7. 设置保存连接IP地址(自定义FTPSClient类)
  8. setUseEPSVwithIPv4(假)
  9. enterLocalPassiveMode()或enterRemotePassiveMode()
  10. initiateListParsing()或任何列表命令a。)此时openDataConnection将被执行,请务必保存此处使用的端口b。)执行PASV命令c。)执行_parsePassiveModeReply,这里你将打开套接字使用您用于连接的IP地址和保存的端口。
  11. 断开(总是)

更多信息:我的问题特定于Linux机器和IIS服务器之间的防火墙。
我的问题的根源是在被动模式下,用于在进行数据连接时打开套接字的IP地址与用于执行初始连接的IP地址不同。 因此,由于APACHE commons-net 3.5的两个问题(见下文),很难弄明白。 我的解决方案:扩展FTPSClient,以便我可以覆盖方法_parsePassiveModeReply和openDataConnection。 我的parsePassiveModeReply实际上只是从回复中保存端口,因为回复指示正在使用的端口。 我的openDataConnection方法使用保存的端口和连接期间使用的原始IP。

APACHE FTPCLient 3.5的问题

  1. 数据连接不会超时(挂起),因此问题不明显。
  2. FTPSClient类不会跳过被动IP地址。 将passiveNatWorkaround设置为true不会像我预期的那样工作,或者它根本不会跳过IP。

需要注意的事项:

  • 通过防火墙时,您必须能够访问IIS定义的端口范围(请参阅配置Microsoft IIS防火墙)。
  • 您还应确保在密钥库或运行时指定的证书中具有任何适当的证书。
  • 将以下内容添加到您的类中,这对于了解正在执行的FTP命令非常有帮助。

      ftpClient.addProtocolCommandListener(new PrintCommandListener(new PrintWriter(System.out), true)); 
  • 检查FTP服务器日志,因为它会告诉您正在执行的操作以及可能出现问题的原因。 在执行列表之前,应始终看到打开的数据通道。 将应用程序的结果与成功的curl命令执行的结果进行比较。
  • 回复代码,因为它们将指示问题发生的位置。
  • 使用curl命令validation您是否具有连接性。以下是一个良好的开端,如果一切正常,将列出根目录中的内容。

     curl -3 ftps://[user id]:[password][ftp server ip]:990/ -1 -v --disable-epsv --ftp-skip-pasv-ip --ftp-ssl --insecure 

FTPSClient扩展(SAMPLE CODE)

 import java.io.IOException; import java.net.Inet6Address; import java.net.InetSocketAddress; import java.net.Socket; import javax.net.ssl.SSLContext; import org.apache.commons.net.MalformedServerReplyException; import org.apache.commons.net.ftp.FTPReply; import org.apache.commons.net.ftp.FTPSClient; /** * TODO Document Me! */ public class PassiveFTPSClient extends FTPSClient { private String passiveSkipToHost; private int passiveSkipToPort; private boolean skipPassiveIP; /** Pattern for PASV mode responses. Groups: (n,n,n,n),(n),(n) */ private static final java.util.regex.Pattern PARMS_PAT; static { PARMS_PAT = java.util.regex.Pattern.compile( "(\\d{1,3},\\d{1,3},\\d{1,3},\\d{1,3}),(\\d{1,3}),(\\d{1,3})"); } /** * @param b * @param sslContext */ public PassiveFTPSClient(boolean b, SSLContext sslContext) { super(b, sslContext); } protected void _parsePassiveModeReply(String reply) throws MalformedServerReplyException { if (isSkipPassiveIP()) { System.out.println( "================> _parsePassiveModeReply" + getPassiveSkipToHost()); java.util.regex.Matcher m = PARMS_PAT.matcher(reply); if (!m.find()) { throw new MalformedServerReplyException( "Could not parse passive host information.\nServer Reply: " + reply); } try { int oct1 = Integer.parseInt(m.group(2)); int oct2 = Integer.parseInt(m.group(3)); passiveSkipToPort = (oct1 << 8) | oct2; } catch (NumberFormatException e) { throw new MalformedServerReplyException( "Could not parse passive port information.\nServer Reply: " + reply); } //do nothing } else { super._parsePassiveModeReply(reply); } } protected Socket _openDataConnection_(String command, String arg) throws IOException { System.out.println( "================> _openDataConnection_" + getPassiveSkipToHost()); System.out.println( "================> _openDataConnection_ isSkipPassiveIP: " + isSkipPassiveIP()); if (!isSkipPassiveIP()) { return super._openDataConnection_(command, arg); } System.out.println( "================> getDataConnectionMode: " + getDataConnectionMode()); if (getDataConnectionMode() != ACTIVE_LOCAL_DATA_CONNECTION_MODE && getDataConnectionMode() != PASSIVE_LOCAL_DATA_CONNECTION_MODE) { return null; } final boolean isInet6Address = getRemoteAddress() instanceof Inet6Address; Socket socket; if (getDataConnectionMode() == ACTIVE_LOCAL_DATA_CONNECTION_MODE) { return super._openDataConnection_(command, arg); } else { // We must be in PASSIVE_LOCAL_DATA_CONNECTION_MODE // Try EPSV command first on IPv6 - and IPv4 if enabled. // When using IPv4 with NAT it has the advantage // to work with more rare configurations. // Eg if FTP server has a static PASV address (external network) // and the client is coming from another internal network. // In that case the data connection after PASV command would fail, // while EPSV would make the client succeed by taking just the port. boolean attemptEPSV = isUseEPSVwithIPv4() || isInet6Address; if (attemptEPSV && epsv() == FTPReply.ENTERING_EPSV_MODE) { System.out.println( "================> _parseExtendedPassiveModeReply a: "); _parseExtendedPassiveModeReply(_replyLines.get(0)); } else { if (isInet6Address) { return null; // Must use EPSV for IPV6 } // If EPSV failed on IPV4, revert to PASV if (pasv() != FTPReply.ENTERING_PASSIVE_MODE) { return null; } System.out.println( "================> _parseExtendedPassiveModeReply b: "); _parsePassiveModeReply(_replyLines.get(0)); } // hardcode fore testing //__passiveHost = "10.180.255.181"; socket = _socketFactory_.createSocket(); if (getReceiveDataSocketBufferSize() > 0) { socket.setReceiveBufferSize(getReceiveDataSocketBufferSize()); } if (getSendDataSocketBufferSize() > 0) { socket.setSendBufferSize(getSendDataSocketBufferSize() ); } if (getPassiveLocalIPAddress() != null) { System.out.println( "================> socket.bind: " + getPassiveSkipToHost()); socket.bind(new InetSocketAddress(getPassiveSkipToHost(), 0)); } // For now, let's just use the data timeout value for waiting for // the data connection. It may be desirable to let this be a // separately configurable value. In any case, we really want // to allow preventing the accept from blocking indefinitely. // if (__dataTimeout >= 0) { // socket.setSoTimeout(__dataTimeout); // } System.out.println( "================> socket connect: " + getPassiveSkipToHost() + ":" + passiveSkipToPort); socket.connect(new InetSocketAddress(getPassiveSkipToHost(), passiveSkipToPort), connectTimeout); if ((getRestartOffset() > 0) && !restart(getRestartOffset())) { socket.close(); return null; } if (!FTPReply.isPositivePreliminary(sendCommand(command, arg))) { socket.close(); return null; } } if (isRemoteVerificationEnabled() && !verifyRemote(socket)) { socket.close(); throw new IOException( "Host attempting data connection " + socket.getInetAddress().getHostAddress() + " is not same as server " + getRemoteAddress().getHostAddress()); } return socket; } /** * Enable or disable passive mode NAT workaround. * If enabled, a site-local PASV mode reply address will be replaced with the * remote host address to which the PASV mode request was sent * (unless that is also a site local address). * This gets around the problem that some NAT boxes may change the * reply. * * The default is true, ie site-local replies are replaced. * @param enabled true to enable replacing internal IP's in passive * mode. */ public void setSkipPassiveIP(boolean enabled) { super.setPassiveNatWorkaround(enabled); this.skipPassiveIP = enabled; System.out.println( "================> skipPassiveIP: " + skipPassiveIP); } /** * Return the skipPassiveIP. * @return the skipPassiveIP */ public boolean isSkipPassiveIP() { return skipPassiveIP; } /** * Return the passiveSkipToHost. * @return the passiveSkipToHost */ public String getPassiveSkipToHost() { return passiveSkipToHost; } /** * Set the passiveSkipToHost. * @param passiveSkipToHost the passiveSkipToHost to set */ public void setPassiveSkipToHost(String passiveSkipToHost) { this.passiveSkipToHost = passiveSkipToHost; System.out.println( "================> setPassiveSkipToHost: " + passiveSkipToHost); } }