今天項目中要下載快錢的對賬單,快錢對賬單文件的FTP服務器是Unix系統,connectServer方法中已連接成功,reply code:220。
但是問題是download方法中的ftpClient.listFiles(remote)不能找到具體某一文件,如果使用ftpClient.listFiles()而不具體指定某一遠程文件時可以列舉出所有的文件,包括remote這個需要下載的文件。且代碼中的ftpClient.retrieveFile(remote, out);會出現長時間的等待,upNewStatus為false,最終會拋出:FTP response 421 received. Server closed connection.
public static boolean connectServer(String host, int port, String user, String password, String defaultPath) throws SocketException, IOException { ftpClient = new FTPClient(); // org.apache.commons.net.MalformedServerReplyException: Could not parse response code. // Server Reply: SSH-2.0-OpenSSH_7.2 // ftpClient.addProtocolCommandListener(new PrintCommandListener(new PrintWriter(System.out))); // 設置以二進制方式傳輸 ftpClient.setDataTimeout(5000); ftpClient.setConnectTimeout(connectTimeout); ftpClient.setControlEncoding("UTF-8"); ftpClient.connect(host, port); log.info("Connected to " + host + "."); log.info("FTP server reply code:" + ftpClient.getReplyCode()); if (FTPReply.isPositiveCompletion(ftpClient.getReplyCode())) { if (ftpClient.login(user, password)) { // Path is the sub-path of the FTP path if (defaultPath != null && defaultPath.length() != 0) { ftpClient.changeWorkingDirectory(defaultPath); } return true; } } disconnect(); return false; } public static File download(String remote, String local) throws IOException { log.info("remote={},local={}", remote, local); File downloadFile = null; // 檢查遠程文件是否存在 FTPFile[] files = ftpClient.listFiles(remote); if (files.length == 0) { log.info("遠程文件不存在: {}, {}", remote, files); return downloadFile; } File f = new File(local); OutputStream out = null; if (f.exists()) { f.delete(); } try { //ftpClient.enterLocalPassiveMode(); out = new FileOutputStream(f); boolean upNewStatus = ftpClient.retrieveFile(remote, out); out.flush(); if (upNewStatus) { downloadFile = f; } } catch (Exception e) { log.error(e.getMessage(), e); } finally { if (out != null) { out.close(); } } return downloadFile; }
FTP需要在自己測試服務器開通21端口,這個已經叫運維開通了。且需要快錢把我們這邊測試服務器ip加入快錢的白名單,否則是鏈接不到那邊的FTP服務器的。
開始一直以為是快錢那邊提供的用戶是否需要什么文件連接權限,定位了很久的問題,最后搜索問題才發現對FTP連接模式和原理不清楚導致。
首先FTP分2中模式:主動模式(port)和被動模式(pasv).FTP標准命令TCP端口號為21,Port方式數據端口為20
不管哪種模式,都必須通過21這個端口建立起到FTP的管道連接,通過這個通道發送命令。
port模式:1.通過tcp的21端口建立起通道
2.客戶端在此通道發起PORT命令,並產生一個隨機非特殊的端口號N(1023<N<65536)給到FTP服務器。
3.此時客戶端監聽N+1端口(N+1>=1025,不一定是N端口+1),同時通過21的通道發送命令通知FTP服務器客戶點通過此端口接受數據傳輸。
4.FTP服務器接收到上一步的響應后通過自己的數據源端口20,去鏈接遠程的客戶端的N+1端口(此時是FTP服務端主動發起的一個端口鏈接)
5.如果此時客戶端的防火牆策略是不能隨意外部鏈接內部服務器的端口,則會造成上一步出現數據端鏈接失敗!
6.如果沒有上一步的情況,FTP客戶端則會接收到服務端響應並返回響應信息,則建立起了數據鏈接通道。
pasv模式:1.通過tcp的21端口建立起通道
2.但與主動方式的FTP不同,客戶端不會提交PORT命令並允許服務器來回連它的數據端口,而是提交 PASV命令.會產生兩個隨機非特殊的端口N(1023<N<65536) 和N+1給到FTP服務器。其中N端口跟主動模式一樣,會把N給到服務端的遠程的21端口。相當於FTP服務端被動接受數據端口號而不是之前port模式的主動發起連接
3.FTP服務端會則會打開N+1的端口號
4.客戶端發起N+1端口的鏈接,並建立數據鏈接。
port模式:
pasv模式:
上面的圖都省略了建立tcp的21端口這個步驟。
正是因為之前采用主動模式,但是測試服務器防火牆阻止了快錢發起的數據端口的連接。也就是port模式的第5步出現問題。
因而FTPClient.listFiles(remote)或者FTPClient.retrieveFile(remote)方法時獲取不了數據,就停止在那里,什么反應都沒有,出現假死狀態。
解決辦法:在調用這兩個方法之前,調用FTPClient.enterLocalPassiveMode();
這個方法的意思就是每次數據連接之前,ftp client告訴ftp server:數據連接的端口號已經告訴你了,你只需被動接受數據連接的請求就行
參考:http://www.cnblogs.com/xiaohh/p/4789813.html
http://blog.csdn.net/u010154760/article/details/45458219