FTP(File Transfer Protocol)基於命令/回應模型,客戶端發送文字指令,伺服器回應三位數狀態碼與訊息。主要指令分類:
| 類別 | 指令 | 說明 |
|---|---|---|
| 傳輸模式 | TYPE A / I | ASCII(文字) / Binary(二進位) 模式切換 |
| MODE S / B / C | 串流/區塊/壓縮傳輸模式(較少用) | |
| 目錄操作 | CWD <path> | 變更工作目錄 |
| CDUP | 回上層目錄 | |
| PWD / LIST / NLST | 顯示當前路徑 / 詳細列表 / 精簡列表 | |
| 檔案傳輸 | RETR <file> | 下載檔案 |
| STOR <file> | 上傳檔案 | |
| APPE <file> | 斷點續傳(附加模式) | |
| 檔案管理 | DELE <file> | 刪除檔案 |
| RNFR / RNTO | 重新命名(從/至) | |
| MKD / RMD | 建立/刪除目錄 | |
| 連線控制 | USER / PASS | 使用者認證 |
| QUIT / ABOR | 離線 / 中止傳輸 | |
| 資料連線 | PORT / PASV | 主動/被動模式資料埠協商 |
FTP 採用雙通道架構:
ServerSocket(21) 監聽。accept(),為每個連線建立服務執行緒(FTPServer extends Thread)。run() 迴圈讀取指令、解析、派發處理、回應。PORT 或 PASV 協商建立資料 Socket 連線。public class FTPServer extends Thread {
private Socket clientSocket;
private static String rootDir = "C:\\"; // 預設根目錄
public static void main(String[] args) {
if (args.length > 0) rootDir = args[0];
try (ServerSocket server = new ServerSocket(21)) {
System.out.println("FTP Server started on port 21");
int clientId = 1;
while (true) {
Socket client = server.accept();
new FTPServer(client, clientId++).start();
}
} catch (IOException e) { e.printStackTrace(); }
}
// 建構子、run() 等省略...
}
public void run() {
try (BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true)) {
out.println("220 Welcome to Java FTP Server");
String line;
while ((line = in.readLine()) != null && !done) {
String cmd = line.split(" ")[0].toUpperCase();
switch (cmd) {
case "USER": handleUser(line, out); break;
case "PASS": handlePass(line, out); break;
case "CWD": handleCwd(line, out); break;
case "CDUP": handleCdup(out); break;
case "PORT": handlePort(line, out); break;
case "TYPE": handleType(out); break;
case "RETR": handleRetr(line, out); break;
case "STOR": handleStor(line, out); break;
case "LIST": handleList(out); break;
case "DELE": handleDele(line, out); break;
case "QUIT": handleQuit(out); break;
default: out.println("500 Unknown command");
}
}
} catch (IOException e) { /* log */ }
}
客戶端傳送:PORT h1,h2,h3,h4,p1,p2(IP 4 組 + 埠號高低位元組)。
private void handlePort(String line, PrintWriter out) {
// 格式:PORT 192,168,1,100,4,50 -> IP:192.168.1.100 Port:4*256+50=1074
String[] parts = line.substring(5).split(",");
String ip = String.join(".", parts[0], parts[1], parts[2], parts[3]);
int port = Integer.parseInt(parts[4]) * 256 + Integer.parseInt(parts[5]);
this.dataHost = ip;
this.dataPort = port;
out.println("200 PORT command successful");
}
private void handleRetr(String line, PrintWriter out) throws IOException {
String filename = line.substring(5).trim();
File file = new File(currentDir, filename);
if (!file.exists()) { out.println("550 File not found"); return; }
out.println("150 Opening BINARY data connection");
try (Socket dataSocket = new Socket(dataHost, dataPort);
FileInputStream fis = new FileInputStream(file);
OutputStream dos = dataSocket.getOutputStream()) {
byte[] buf = new byte[8192];
int len;
while ((len = fis.read(buf)) > 0) dos.write(buf, 0, len);
out.println("226 Transfer complete");
} catch (Exception e) { out.println("426 Transfer aborted"); }
}
private void handleStor(String line, PrintWriter out) throws IOException {
String filename = line.substring(5).trim();
File file = new File(currentDir, filename);
out.println("150 Opening BINARY data connection");
try (Socket dataSocket = new Socket(dataHost, dataPort);
InputStream dis = dataSocket.getInputStream();
FileOutputStream fos = new FileOutputStream(file)) {
byte[] buf = new byte[8192];
int len;
while ((len = dis.read(buf)) > 0) fos.write(buf, 0, len);
out.println("226 Transfer complete");
} catch (Exception e) { out.println("426 Transfer aborted"); }
}
private void handleList(PrintWriter out) throws IOException {
out.println("150 Opening ASCII data connection");
try (Socket dataSocket = new Socket(dataHost, dataPort);
PrintWriter dos = new PrintWriter(dataSocket.getOutputStream(), true)) {
File dir = new File(currentDir);
File[] files = dir.listFiles();
if (files != null) {
for (File f : files) {
String type = f.isDirectory() ? "d" : "-";
// 簡易權限表示,實際應依 RFC 3659 格式化
dos.printf("%s rw-r--r-- 1 owner group %12d %s %s%n",
type, f.length(),
new SimpleDateFormat("MMM dd HH:mm").format(new Date(f.lastModified())),
f.getName());
}
}
out.println("226 Transfer complete");
} catch (Exception e) { out.println("426 Transfer aborted"); }
}
| 議題 | 說明 |
|---|---|
| 被動模式 (PASV/EPSV) | 伺服器開啟隨機埠等待客戶端連入,需防火牆開放埠範圍,現代客戶端預設使用 |
| SSL/TLS 加密 (FTPS) | 控制/資料通道加密,需整合 SSLSocketFactory,支援 AUTH TLS 指令 |
| 虛擬使用者/權限控管 | 整合資料庫或 LDAP,實作檔案級存取控制(ACL) |
| 大檔案支援 | 使用 RandomAccessFile 實作斷點續傳(REST 指令) |
| 閒置超時與連線限制 | 防止資源耗盡,需實作連線池與心跳機制 |
| NIO/非阻塞架構 | 高併發場景下可遷移至 Selector 或 Netty/MINA 框架 |
本文以精簡核心代碼示範 Java 實作 FTP 伺服器的雙通道多執行緒架構、指令解析派發、PORT/PASV 協商與檔案傳輸邏輯。實際部署仍需補強安全性(FTPS)、效能(NIO/連線池)、管理功能(虛擬用戶/配額/日誌審計)等企業級特性。建議生產環境直接採用成熟開源專案(Apache FtpServer、vsftpd、ProFTPD)而非自行開發。