FTP 協議(RFC 959)規定路徑/檔名編碼為 **ISO-8859-1 (Latin-1)**。中文屬於多位元組字符,直接傳輸會被截斷或錯誤解碼,顯示為 `??` 或亂碼。
// 關鍵:UTF-8 位元組 -> ISO-8859-1 字符串
String chineseName = "中文目錄/檔名.txt";
String ftpName = new String(chineseName.getBytes("UTF-8"), "ISO-8859-1");
// 或 GBK(視伺服器而定)
// String ftpName = new String(chineseName.getBytes("GBK"), "ISO-8859-1");
原理:將中文按 UTF-8 編碼為位元組陣列,再按 ISO-8859-1 **強制解碼**為字符串。傳輸時位元組不變,伺服器按 ISO-8859-1 讀取,再由客戶端反向轉換還原。
public class FtpUploader {
private final String host;
private final int port;
private final String username;
private final String password;
private final String encoding = "UTF-8"; // 或 GBK
public FtpUploader(String host, int port, String username, String password) {
this.host = host;
this.port = port;
this.username = username;
this.password = password;
}
public boolean upload(File localFile, String remotePath, String remoteFileName) {
FTPClient ftp = new FTPClient();
try {
// 連接與登入
ftp.connect(host, port);
if (!ftp.login(username, password)) return false;
ftp.setFileType(FTPClient.BINARY_FILE_TYPE);
ftp.enterLocalPassiveMode(); // 被動模式穿透防火牆
// 處理遠端路徑:逐級創建目錄
if (remotePath != null && !remotePath.trim().isEmpty()) {
String[] dirs = remotePath.split("/");
for (String dir : dirs) {
if (dir.isEmpty()) continue;
String ftpDir = encode(dir);
if (!ftp.changeWorkingDirectory(ftpDir)) {
if (!ftp.makeDirectory(ftpDir)) return false;
if (!ftp.changeWorkingDirectory(ftpDir)) return false;
}
}
}
// 上傳檔案
String ftpFileName = encode(remoteFileName);
try (FileInputStream fis = new FileInputStream(localFile)) {
return ftp.storeFile(ftpFileName, fis);
}
} catch (IOException e) {
e.printStackTrace();
return false;
} finally {
try { ftp.logout(); ftp.disconnect(); } catch (IOException ignored) {}
}
}
private String encode(String name) throws UnsupportedEncodingException {
return new String(name.getBytes(encoding), "ISO-8859-1");
}
}
# 強制 UTF-8
utf8_filesystem=YES
# 或指定字符集(需編譯支援)
# character_set_server=utf8
export LANG="zh_CN.UTF-8"
export LC_ALL="zh_CN.UTF-8"
# 或 GBK 環境
# export LANG="zh_CN.GBK"
# export LC_ALL="zh_CN.GBK"
| 伺服器環境 | 客戶端 encoding 參數 | 備註 |
|---|---|---|
| Linux UTF-8 (現代) | UTF-8 | 推薦,支援所有 Unicode |
| Linux GBK (舊系統) | GBK | 需匹配伺服器 locale |
| Windows IIS FTP | GBK/UTF-8 | 視 IIS 版本與設置 |
| 嵌入式/網路設備 | GBK/Big5 | 查閱設備文檔 |
| 現象 | 原因 | 解決 |
|---|---|---|
| 目錄創建成功但進入失敗 | 編碼不一致導致路徑不匹配 | 創建與進入用同一 encode() 方法 |
| 上傳成功但檔名顯示亂碼 | 伺服器文件系統編碼與客戶端不匹配 | 調整 encoding 參數,配合伺服器 locale |
| 中文路徑下載失敗 | retrieveFile 同樣需編碼轉換 | 下載時同樣 encode(remoteName) |
| 被動模式連接超時 | 防火牆阻擋數據端口 | ftp.enterLocalPassiveMode() + 開放被動端口範圍 |
public InputStream download(String remotePath, String remoteFileName) throws IOException {
FTPClient ftp = new FTPClient();
ftp.connect(host, port);
ftp.login(username, password);
ftp.setFileType(FTPClient.BINARY_FILE_TYPE);
ftp.enterLocalPassiveMode();
// 切換目錄
if (remotePath != null) {
for (String dir : remotePath.split("/")) {
if (!dir.isEmpty()) ftp.changeWorkingDirectory(encode(dir));
}
}
return ftp.retrieveFileStream(encode(remoteFileName));
}
FTP 中文亂碼本質是**協議規定編碼 (ISO-8859-1) 與業務編碼 (UTF-8/GBK) 不匹配**。核心解法:客戶端雙向轉換 `new String(bytes(UTF-8), ISO-8859-1)`,配合伺服器正確 locale 設置。新版 Commons Net 提供部分配置簡化,但數據連接編碼仍需手動處理。