【问题标题】:Apache Commons FTPClient HangingApache Commons FTPClient 挂起
【发布时间】:2012-03-31 04:59:05
【问题描述】:

我们正在使用以下 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 和我的应用程序特定日志显示没有任何异常被抛出的迹象

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

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

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

由于我在这里将Throwables 作为最后的手段,所以如果出现问题,我本来希望在日志中看到 something。因此,我觉得这是一个明确的挂起问题。

有什么想法吗?不幸的是,我对这里的 FTP 内部知识知之甚少,无法做出明确的诊断。这可能是服务器端的东西吗?跟FTP服务器有关吗?

【问题讨论】:

    标签: java ftp ftp-client apache-commons-net


    【解决方案1】:

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

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

    我还建议将断开连接放在finally 块中,这样它就不会留下连接。

    【讨论】:

    • 我的回答可能没有意义的唯一一点是为什么有时会起作用。
    • 没错!而且不只是有时...它在大约 99.9% 的时间里都有效!这就是为什么我觉得这是一个服务器端问题...感谢您的建议,尽管我会尝试它们!
    • 出于好奇,被动/主动之间的真正区别是什么?我的理解是,在活动状态下,它是启动连接的服务器。这就是它的要点吗?
    • 简而言之,被动模式使客户端发起连接。它经常与防火墙一起使用。 slacksite.com/other/ftp.html
    • Ehhh,我试过了,它部署了,看起来它在工作了大约 20 分钟后就死了(一路上处理了大约 16 个文件)。 loadables/ 目录或其挂起时正在处理的文件类型没有明显差异。
    【解决方案2】:

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

    您可以使用 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();
        }
    }
    

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

    【讨论】:

    • 这对我很有用,非常感谢!重要的一行是 this.ftpClient.setBufferSize(1024 * 1024);
    • 缓冲区大小函数对性能产生了令人难以置信的影响,谢谢 molavec,小伙子!
    • 你昨天怎么没睡?
    • Buffersize 还解决了我遇到的 storeFile() 从未实际发送过 250MB 大文件的问题
    • 缓冲区大小如何解决问题?是真的挂了还是很慢?
    【解决方案3】:

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

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

    【讨论】:

    • 这在使用 FTPSClient 但@IAmYourFaja 正在使用 FTPClient 时很有帮助
    • WTF?我已经为我的 FTPSClient 苦苦挣扎了好几个小时,而这些神奇的线条,我不知道它们实际上做了什么,解决了我的问题。谢谢 ..
    【解决方案4】:

    我在尝试从 Linux 机器到 IIS 服务器执行列表文件时遇到了同样的问题。该代码在我的开发人员工作站上运行良好,但在服务器上运行时会挂起,特别是由于防火墙阻塞了混合。

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

    1. 连接(隐式 = true,SSLContext = TLS)
    2. 检查 isPositiveCompletion
    3. 认证(当然)
    4. 执行PBSZ(0)
    5. execPROT("P")
    6. 设置布尔值以指示跳过被动 IP(自定义 FTPSClient 类)
    7. 设置保存连接IP地址(自定义FTPSClient类)
    8. setUseEPSVwithIPv4(false)
    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 命令验证你是否有连接,如下 是一个好的开始,如果一切顺利,将列出根目录中的内容 目录。

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

    FTPSClient 扩展(示例代码)

    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.
            // E.g. 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, i.e. 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);
        }
    
    }
    

    【讨论】:

      猜你喜欢
      • 2016-01-25
      • 2012-05-18
      • 2023-04-05
      • 2013-11-21
      • 2015-01-17
      • 2012-07-19
      • 2014-03-26
      • 2011-08-18
      • 2012-05-22
      相关资源
      最近更新 更多