Apache Commons FTPClient Hanging - PullRequest
       24

Apache Commons FTPClient Hanging

17 голосов
/ 14 марта 2012

Мы используем следующий FTP-код Apache Commons Net для подключения к 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 и могу войти в приложение веб-менеджера)
  • Внутриприложение менеджера, я вижу, что моя WAR все еще работает / запущена
  • catalina.out, и в журнале моего приложения нет никаких признаков каких-либо исключений

Итак, JVMВсе еще работает.Tomcat все еще работает, и моя развернутая WAR все еще работает, но она просто зависает.Иногда работает в течение 2 часов, а затем зависает;в других случаях он работает в течение нескольких дней, а затем зависает.Но когда он зависает, он делает это между строкой, которая читает About to check loadables/ for files... (которую я вижу в журналах), и строкой, которая читает File fetch completed. (которую я не вижу).

Этоговорит мне, что зависание происходит во время фактического опроса / извлечения файлов, что указывает мне то же направление, что и на этот вопрос , который мне удалось найти, и который связан с взаимоблокировкой FTPClient.Это заставляет меня задуматься, если это те же проблемы (, если они есть, я с удовольствием удалю этот вопрос! ).Однако я не думаю, что верят они одинаковы (я не вижу одинаковых исключений в своих журналах).

Сотрудник упомянул, что это может быть "Пассив"против "Активного" FTP вещь.Не зная разницы, меня немного смущают поля FTPClient ACTIVE_REMOTE_DATA_CONNECTION_MODE, PASSIVE_REMOTE_DATA_CONNECTION_MODE и т. Д., И я не знал, что ТАК считает это потенциальной проблемой.

Так как я 'м, поймав Throwable s в качестве последнего средства здесь, я бы ожидал увидеть что-то в журналах, если что-то идет не так.Ergo, я чувствую, что это определенная проблема зависания.

Есть идеи?К сожалению, я не знаю достаточно о внутренностях FTP, чтобы поставить точный диагноз.Может ли это быть что-то на стороне сервера?Связанный с FTP-сервером?

Ответы [ 4 ]

28 голосов
/ 14 марта 2012

Это может быть несколько вещей, но предложение вашего друга будет полезным.

Попробуйте ftpClient.enterLocalPassiveMode();, чтобы посмотреть, поможет ли это.

Я бы также предложил поместить разъединение в блок finally , чтобы оно никогда не оставляло соединение там.

20 голосов
/ 08 мая 2013

Вчера я не спал, но думаю, что решил проблему.

Вы можете увеличить размер буфера с помощью 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();
    }
}

Я надеюсь, что этокод может быть полезен для кого-то!

3 голосов
/ 21 августа 2015

Я должен был включить следующее после входа в систему, чтобы вызвать s.listFiles и передать без 'зависания' и, в конечном итоге, сбоя:

s.login(username, password);
s.execPBSZ(0);
s.execPROT("P");
1 голос
/ 30 ноября 2016

У меня была такая же проблема при попытке выполнить файлы списков с компьютера Linux на сервер IIS. Код прекрасно работал с моей рабочей станции разработчика, но зависал при работе на сервере, в частности, из-за брандмауэра, смешивающего смесь.

Необходимо сделать все это по порядку и потребовать от вас расширения FTPSClient 3.5

  1. connect (неявный = true, SSLContext = TLS)
  2. проверка isPositiveCompletion
  3. аутентификация (конечно)
  4. execPBSZ (0)
  5. execPROT ( "P")
  6. установить логическое значение для указания Пропускного пассивного IP (пользовательский класс FTPSClient)
  7. установить IP-адрес для сохранения соединения (пользовательский класс FTPSClient)
  8. setUseEPSVwithIPv4 (false)
  9. enterLocalPassiveMode () или enterRemotePassiveMode ()
  10. initiateListParsing () или любая команда списка a.) В этот момент будет выполнено openDataConnection , обязательно сохраните используемый здесь порт б.) Команда PASV выполнена c.) _parsePassiveModeReply выполняется, здесь вы откроете сокет с IP-адресом, который вы использовали для подключения, и сохраненным портом.
  11. отключить (всегда)

Дополнительная информация: Моя проблема связана с брандмауэром между компьютером Linux и сервером IIS.
Корень моей проблемы заключается в том, что в пассивном режиме 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);
    }

}
...