[踩雷紀錄] Java 開發上遇到的 FTP 主動式、被動式
最近一個案子中在 web server 有用到 FTP 的功能,而在開發中卻發現在本地測試時能夠正常運作的 FTP 功能,部屬到線上平台之後卻通通失靈了,查了一下才知道 FTP 有分為兩種不同的模式,而在特定的情況下一些模式是無法作用的。
FTP 模式
首先先理解一下 FTP 連線機制上其實有兩種模式:
- 主動式 (POST/Active)
- 被動式 (PASV/Passive)
分別運用在不同的情境之下,在 FTP 中定義兩個 port 通常是 20、21 來進行溝通,其中 21 被視為 Command Port,20 被視為 Data Port,其功用正如其名
主動式
最開始的 FTP 設計都是由主動式來進行溝通,溝通的方式如下:
- 由 client 端(通常是 1024)發出連線請求與 FTP server 的 Command Port 建立連線,並發出命令
- 同時 client 會開啟一個 Port (通常為 1025)等待接聽由 FTP server Data Port 傳過來的資料
- server 端回應命令並由 Data Port 傳送資料到 client 端
- client 端接收到資料後回覆 server 端
整體流程大致如下圖:
那主動式的問題主要會出現在第三步的時候,由於現行的網路架構下,client 端多數是被保護的,可能位在 router 之後或是有防火牆的設計防止外來的連線,也因此才有了被動式的方法誕生。
被動式
為了解決在現代網路架構下主動式的連線問題所誕生的被動模式,溝通過程如下:
- 由 client 端(通常是 1024)發出連線請求與 FTP server 的 Command Port 建立連線,並發出命令
- 同時,server 端開啟一個大於 1023 的 port 作為 Data Port 等待連線,並在第一步時,將這個 port 傳送給 client
- client 對 server Data Port 主動發起連線進行資料傳輸
被動式來說的話變成 server 這邊需要額外多開幾個 port 來進行連線
實際問題解析
其實蠻好理解的,既然沒辦法由 server 來連線那就轉由 client 來進行,然而就是這樣的機制造成本地測試與線上使用時預期上的不同
在本地測試時 FTP 以及 web server 皆在同一個 host 之下,而 Java 的 Apache FTP API 預設是使用主動模式進行連線,而部屬到線上的 K8s 環境之後,FTP 以及 web server 被拆開成了兩個不同的微服務部屬在不同的 pod 之內,兩個服務之間的 port 是不能隨意連線的
其實以 K8s 的角度來說的話,也可以在 web server 這邊的微服務開啟 1024 以及 1025 port 應該也可以發起主動連線,不過這邊為了避免往後還需要特別去確認要使用哪個模式,這邊我選擇寫一個判斷來自動切換模式
Java 解決方案
public class FTPUtils{
public static FTPClient getFTPClient(
String host, int port,
String username, String password
){
FTPClient ftpClient = null;
try {
ftpClient = new FTPClient();
ftpClient.connect(host, port);
ftpClient.login(username, password);
ftpClient.setRemoteVerificationEnabled(false);
ftpClient.enterLocalActiveMode();
ftpClient.listFiles("/");
boolean result = FTPReply.isPositiveCompletion(ftpClient.getReplyCode());
if (!result) {
ftpClient.enterLocalPassiveMode();
ftpClient.listFiles("/");
result = FTPReply.isPositiveCompletion(ftpClient.getReplyCode());
if (!result) {
throw new Exception(ftpClient.getReplyCode());
}
}
} catch (Exception e) {
e.printStackTrace();
}
return ftpClient;
}
}
其實這個解決方案的靈感來自於著名的 FTP 軟體 FileZilla,FileZilla 在每次連線的時候第一件事就是取得根目錄的清單,於是我也效仿這個做法來確認連線模式是否有作用