2012-03-14 65 views
16

Chúng tôi đang sử dụng mã Apache Commons Net FTP sau để kết nối với một máy chủ FTP, thăm dò ý kiến ​​một số thư mục cho các tập tin, và nếu file được tìm thấy, để lấy chúng để máy cục bộ:Apache Commons FTPClient Treo

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

Chúng tôi có kế hoạch để chạy từng phút, vào phút. Khi được triển khai tới Tomcat (7.0.19), mã này tải lên hoàn toàn tốt và bắt đầu làm việc mà không bị cản trở. Tuy nhiên, mỗi lần, tại một thời điểm nào đó, có vẻ như chỉ treo. Bởi rằng tôi có nghĩa là:

  • Không bãi đống tồn tại
  • Tomcat vẫn chạy (tôi có thể thấy pid của nó và có thể đăng nhập vào các ứng dụng quản lý web)
  • Bên trong ứng dụng quản lý, tôi có thể thấy WAR của tôi vẫn chạy/bắt đầu
  • catalina.out và ứng dụng cụ thể cho thấy đăng nhập của tôi không có dấu hiệu của bất kỳ trường hợp ngoại lệ được ném

Vì vậy, các JVM vẫn chạy. Tomcat vẫn đang chạy, và WAR đã triển khai của tôi vẫn đang chạy, nhưng nó chỉ bị treo. Đôi khi nó chạy trong 2 giờ và sau đó bị treo; thời gian khác nó chạy trong nhiều ngày và sau đó bị treo. Nhưng khi nó treo, nó làm như vậy giữa dòng mà đọc About to check loadables/ for files... (mà tôi thấy trong các bản ghi) và dòng mà đọc File fetch completed. (mà tôi không nhìn thấy).

Điều này cho tôi biết sự cố xảy ra trong quá trình thăm dò/tìm nạp thực tế các tệp, loại tôi theo cùng một hướng như this question mà tôi có thể tìm thấy mối quan ngại nào với sự bế tắc FTPClient. Điều này khiến tôi băn khoăn nếu đây là những vấn đề tương tự (nếu có, tôi sẽ vui vẻ xóa câu hỏi này!). Tuy nhiên, tôi không nghĩ rằng tin rằng chúng giống nhau (tôi không thấy ngoại lệ tương tự trong nhật ký của mình).

Một đồng nghiệp đề cập đến nó có thể là một "thụ động" so với "FTP" hoạt động. Không thực sự biết sự khác biệt, tôi là một chút bối rối bởi các lĩnh vực FTPClient ACTIVE_REMOTE_DATA_CONNECTION_MODE, PASSIVE_REMOTE_DATA_CONNECTION_MODE, vv và không biết những gì SO nghĩ về điều đó như là một vấn đề tiềm năng.

Vì tôi đang xem Throwable s là phương sách cuối cùng ở đây, tôi đã mong đợi sẽ thấy nội dung nào đó trong nhật ký nếu có điều gì đó không ổn. Ergo, tôi cảm thấy như đây là một vấn đề treo chắc chắn.

Bất kỳ ý tưởng nào? Thật không may tôi không biết đủ về FTP internals ở đây để thực hiện một chẩn đoán chắc chắn. Điều này có thể là một cái gì đó phía máy chủ? Liên quan đến máy chủ FTP?

Trả lời

26

Đây có thể là một số điều, nhưng đề xuất của bạn bè của bạn sẽ đáng giá.

Hãy thử ftpClient.enterLocalPassiveMode(); để xem điều đó có hữu ích hay không.

Tôi cũng khuyên bạn nên đặt ngắt kết nối trong khối finally sao cho không bao giờ để kết nối ở đó.

+0

Chỉ có điều mà có thể không có ý nghĩa với câu trả lời của tôi là lý do tại sao các công trình này đôi khi. – tjg184

+0

Chính xác! Và không chỉ * đôi khi * ... nó hoạt động khoảng 99,9% thời gian! Thats lý do tại sao tôi cảm thấy như thế này là một vấn đề phía máy chủ ... cảm ơn cho các đề xuất mặc dù tôi sẽ thử cả hai! – IAmYourFaja

+0

Trong sự tò mò, sự khác biệt thực sự giữa thụ động/hoạt động là gì? Sự hiểu biết của tôi là, dưới hoạt động, máy chủ của nó khởi tạo kết nối. Đó có phải là ý chính của nó không? – IAmYourFaja

20

Hôm qua, tôi đã không ngủ nhưng tôi nghĩ rằng tôi đã giải quyết được vấn đề.

Bạn có thể tăng kích thước bộ đệm bằng 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(); 
    } 
} 

Tôi hy vọng rằng mã này có thể hữu ích cho ai đó!

+3

Nó rất hữu ích với tôi, vì vậy cảm ơn! Dòng quan trọng là this.ftpClient.setBufferSize (1024 * 1024); – Bambax

+0

Chức năng kích thước bộ đệm có tác động đáng kinh ngạc đến hiệu suất, cảm ơn bạn molavec, chàng trai hàng đầu! – Sipty

+3

Tại sao hôm qua bạn không ngủ? – TriCore

2

tôi phải bao gồm những điều sau đây sau khi đăng nhập để kêu gọi s.listFiles và chuyển giao mà không có nó 'treo' và cuối cùng thất bại:

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

rất tốt. Vâng!! Cái này làm việc cho tôi. –

1

Tôi có vấn đề này tương tự khi cố gắng thực hiện một listfiles từ Máy Linux đến máy chủ IIS. Mã này hoạt động rất tốt từ máy trạm của nhà phát triển của tôi, nhưng sẽ treo khi chạy trên máy chủ đặc biệt do một bức tường lửa tăng cường kết hợp.

Phải làm những việc này theo thứ tự và sẽ yêu cầu bạn phải mở rộng FTPSClient 3,5

  1. kết nối (implicit = true, SSLContext = TLS)
  2. séc isPositiveCompletion
  3. xác thực (tất nhiên)
  4. execPBSZ (0)
  5. execPROT ("P")
  6. đặt boolean để chỉ Bỏ qua IP thụ động (lớp FTPS tùy chỉnh)
  7. thiết lập lưu kết nối địa chỉ IP (tùy chỉnh FTPSClient lớp)
  8. setUseEPSVwithIPv4 (false)
  9. enterLocalPassiveMode() hoặc enterRemotePassiveMode()
  10. initiateListParsing() hoặc bất kỳ danh sách lệnh a.) Tại thời điểm này các openDataConnection sẽ được thực hiện, hãy chắc chắn lưu cổng đang được sử dụng ở đây b.) Lệnh PASV được thực hiện c.) _parsePassiveModeReply được thực thi, tại đây bạn sẽ mở ổ cắm với địa chỉ IP bạn đã sử dụng để kết nối và lưu Hải cảng.
  11. ngắt kết nối (luôn luôn)

More INFO: Vấn đề của tôi là cụ thể cho một bức tường lửa giữa máy Linux và máy chủ IIS.
Gốc của vấn đề của tôi là ở chế độ thụ động, địa chỉ IP được sử dụng để mở ổ cắm khi thực hiện kết nối dữ liệu khác với địa chỉ được sử dụng để thực hiện kết nối ban đầu. Vì vậy, do hai vấn đề (xem bên dưới) với APACHE commons-net 3.5, thật khó để tìm ra. Giải pháp của tôi: Mở rộng FTPSClient để tôi có thể ghi đè lên phương thức _parsePassiveModeReply & openDataConnection. ParsePassiveModeReply của tôi thực sự chỉ lưu cổng từ thư trả lời vì câu trả lời cho biết cổng nào đang được sử dụng. Phương thức openDataConnection của tôi đang sử dụng cổng đã lưu và IP ban đầu được sử dụng trong khi kết nối.

Vấn đề với Apache FTPCLient 3,5

  1. kết nối dữ liệu không thời gian ra (bị treo) vì vậy nó không phải là rõ ràng vấn đề là gì.
  2. Lớp FTPSClient không bỏ qua địa chỉ IP thụ động. Đặt passiveNatWorkaround thành true không hoạt động như tôi mong đợi hoặc có thể nó không bỏ qua IP.

Những điều cần chú ý đến:

  • Khi đi qua một bức tường lửa, bạn phải có quyền truy cập đến phạm vi cảng xác định bởi IIS (xem cấu hình Microsoft IIS tường lửa).
  • Bạn cũng nên đảm bảo bạn có bất kỳ chứng chỉ phù hợp nào trong kho khóa hoặc cert được chỉ định khi chạy.
  • Thêm thông tin sau vào lớp học của bạn, điều này rất hữu ích khi biết những gì Lệnh FTP đang được thực thi.

    ftpClient.addProtocolCommandListener(new PrintCommandListener(new PrintWriter(System.out), true)); 
    
  • Kiểm tra nhật ký máy chủ FTP vì nó sẽ cho bạn biết những gì đang được thực hiện và có thể tại sao bạn gặp sự cố. Bạn sẽ luôn thấy kênh dữ liệu được mở trước khi thực hiện danh sách. So sánh kết quả của ứng dụng của bạn với ứng dụng của lệnh curl thành công.
  • Mã trả lời vì chúng sẽ cho biết nơi xảy ra sự cố.
  • Sử dụng lệnh curl để xác minh bạn có kết nối, sau đây là một khởi đầu tốt và nếu tất cả sẽ liệt kê các nội dung trong thư mục gốc .

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

FTPSClient mở rộng (MẪU MÃ)

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

}