web-dev-qa-db-fra.com

Apache Commons FTPClient Hanging

Nous utilisons le code FTP Apache Commons Net suivant pour se connecter à un serveur FTP, interroger certains répertoires pour les fichiers et, si des fichiers sont trouvés, pour les récupérer sur la machine locale:

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());
}

Nous avons prévu de l'exécuter toutes les minutes, à la minute. Lorsqu'il est déployé sur Tomcat (7.0.19), ce code se charge parfaitement bien et commence à fonctionner sans accroc. À chaque fois cependant, à un moment ou à un autre, cela semble juste se bloquer. J'entends par là:

  • Aucun vidage de tas n'existe
  • Tomcat est toujours en cours d'exécution (je peux voir son pid et me connecter à l'application de gestion Web)
  • Dans l'application gestionnaire, je peux voir que ma GUERRE est toujours en cours/démarrée
  • catalina.out et mon journal spécifique à l'application ne montre aucun signe de levée d'exceptions

La JVM fonctionne donc toujours. Tomcat est toujours en cours d'exécution, et mon WAR déployé est toujours en cours d'exécution, mais il est juste suspendu. Parfois, il fonctionne pendant 2 heures, puis se bloque; d'autres fois, il s'exécute pendant des jours, puis se bloque. Mais quand il se bloque, il le fait entre la ligne qui lit About to check loadables/ for files... (que je vois dans les journaux) et la ligne qui lit File fetch completed. (que je ne vois pas).

Cela me dit que le blocage se produit lors de l'interrogation/récupération des fichiers, ce qui me pointe dans la même direction que cette question que j'ai pu trouver qui se préoccupe du blocage FTPClient. Cela me fait me demander s'il s'agit des mêmes problèmes ( s'ils le sont, je supprimerai volontiers cette question! ). Cependant, je ne pense pas que croyez ce sont les mêmes (je ne vois pas les mêmes exceptions dans mes journaux).

Un collègue a mentionné qu'il pourrait s'agir d'un problème FTP "passif" ou "actif". Ne connaissant pas vraiment la différence, je suis un peu confus par les champs FTPClient ACTIVE_REMOTE_DATA_CONNECTION_MODE, PASSIVE_REMOTE_DATA_CONNECTION_MODE, etc. et ne savait pas ce que SO considérait cela comme un problème potentiel.

Puisque j'attrape Throwables en dernier recours ici, je m'attendais à voir quelque chose dans les journaux si quelque chose ne va pas. Ergo, j'ai l'impression que c'est vraiment un problème de blocage.

Des idées? Malheureusement, je ne connais pas suffisamment les internes FTP ici pour faire un diagnostic ferme. Serait-ce quelque chose côté serveur? Lié au serveur FTP?

20
IAmYourFaja

Cela pourrait être un certain nombre de choses, mais la suggestion de votre ami en vaudrait la peine.

Essayez ftpClient.enterLocalPassiveMode(); pour voir si cela aide.

Je suggérerais également de mettre la déconnexion dans le bloc finally afin qu'il ne laisse jamais de connexion là-bas.

28
tjg184

Hier, je n'ai pas dormi mais je pense avoir résolu le problème.

Vous pouvez augmenter la taille du tampon avec 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();
    }
}

J'espère que ce code pourra être utile à quelqu'un!

20
molavec

J'ai dû inclure les éléments suivants après la connexion afin d'appeler s.listFiles et de le transférer sans qu'il ne `` se bloque '' et, éventuellement, échoue:

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

J'ai eu ce même problème lorsque j'essayais d'effectuer un listfile depuis une machine Linux vers un serveur IIS. Le code fonctionnait très bien depuis mon poste de travail de développeur, mais se bloquait lors de l'exécution sur le serveur spécifiquement en raison d'un pare-feu gommant le mélange.

Doit faire ces choses dans l'ordre et vous obligera à étendre FTPSClient 3.5

  1. connect (implicite = vrai, SSLContext = TLS)
  2. vérifier isPositiveCompletion
  3. authentifier (bien sûr)
  4. execPBSZ (0)
  5. execPROT ("P")
  6. set boolean pour indiquer Skip Passive IP (classe FTPSClient personnalisée)
  7. définir l'adresse IP de connexion de sauvegarde (classe FTPSClient personnalisée)
  8. setUseEPSVwithIPv4 (faux)
  9. enterLocalPassiveMode () ou enterRemotePassiveMode ()
  10. initialListParsing () ou toute commande de liste a.) À ce stade, openDataConnection sera exécuté, assurez-vous d'enregistrer le port utilisé ici b.) La commande PASV est exécutée c.) La _parsePassiveModeRhness est exécutée , ici vous ouvrirez le socket avec l'adresse IP que vous avez utilisée pour la connexion et le port enregistré.
  11. déconnecter (toujours)

Plus d'informations: Mon problème est spécifique à un pare-feu entre la machine Linux et le serveur IIS.
La racine de mon problème est qu'en mode passif, l'adresse IP utilisée pour ouvrir le socket lors d'une connexion de données est différente de celle utilisée pour établir la connexion initiale. Donc, en raison de deux problèmes (voir ci-dessous) avec Apache commons-net 3.5, il était incroyablement difficile à comprendre. Ma solution: étendre FTPSClient pour pouvoir remplacer les méthodes _parsePassiveModeRhness & openDataConnection. Mon parsePassiveModeRhness est vraiment juste en train d'enregistrer le port de la réponse puisque la réponse indique quel port est utilisé. Ma méthode openDataConnection utilise le port enregistré et l'IP d'origine utilisée lors de la connexion.

Problèmes avec Apache FTPCLient 3.5

  1. La connexion de données n'expire pas (se bloque) donc il n'est pas évident quel est le problème.
  2. La classe FTPSClient n'ignore pas les adresses IP passives. Définir passiveNatWorkaround sur true ne fonctionne pas comme je m'y attendais ou peut-être qu'il ne saute pas du tout l'IP.

Choses à faire attention:

  • Lorsque vous passez par un pare-feu, vous devez avoir accès à la plage de ports définie par IIS (voir la configuration de Microsoft IIS pare-feu)).
  • Vous devez également vous assurer que vous disposez des certificats appropriés dans votre magasin de clés ou du certificat spécifié lors de l'exécution.
  • Ajoutez ce qui suit à votre classe, c'est donc très utile de savoir quelles commandes FTP sont en cours d'exécution.

       ftpClient.addProtocolCommandListener(new PrintCommandListener(new PrintWriter(System.out), true));
    
  • Vérifiez les journaux du serveur FTP car il vous dira ce qui est en cours et les raisons possibles des problèmes. Vous devriez toujours voir un canal de données ouvert avant d'exécuter une liste. Comparez les résultats de votre application à ceux d'une commande curl réussie.
  • Répondez aux codes car ils indiqueront où un problème se produit.
  • Utilisez la commande curl pour vérifier que vous disposez d'une connectivité. Ce qui suit est un bon début et si tout va bien, le contenu sera répertorié dans le répertoire racine.

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

FTPSClient étendu (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.
        // 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);
    }

}
1
Tm Zengerle