FTP (File Transfer Protocol) 以命令/回應模式運作在 TCP 21 埠 (控制連線),資料傳輸另建 TCP 連線 (主動/被動模式)。本文展示一個純 Java 實作的 FTP 伺服器核心框架,涵蓋命令解析、多執行緒分派、資料連線建立、檔案上傳/下載/刪除/列表。
| 分類 | 命令 | 功能 | 典型回應碼 |
|---|---|---|---|
| 存取控制 | USER <name> | 提交使用者名稱 | 331 (需密碼) |
| PASS <pass> | 提交密碼 | 230 (登入成功) | |
| 目錄操作 | CWD <path> | 變更工作目錄 | 250 |
| CDUP | 返回上層目錄 | 250 | |
| LIST | 列出目錄內容 | 150 (資料連線開啟) / 226 (完成) | |
| 傳輸參數 | PORT h1,h2,h3,h4,p1,p2 | 指定主動模式資料埠 (IP+Port) | 200 |
| TYPE A/I | 設定 ASCII/二進制模式 | 200 | |
| 檔案傳輸 | RETR <file> | 下載檔案 (伺服器→客戶端) | 150 / 226 |
| STOR <file> | 上傳檔案 (客戶端→伺服器) | 150 / 226 | |
| DELE <file> | 刪除檔案 | 250 | |
| 連線控制 | QUIT | 登出關閉控制連線 | 221 |
public class FtpServer extends Thread {
private static String rootDir = "C:\\"; // 工作根目錄
private Socket clientSocket;
private int clientId;
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, root: " + rootDir);
int id = 1;
while (true) {
Socket client = server.accept();
new FtpServer(client, id++).start(); // 每客戶一執行緒
}
} catch (IOException e) { e.printStackTrace(); }
}
public FtpServer(Socket socket, int id) {
this.clientSocket = socket; this.clientId = id;
}
public void run() {
try (BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) {
out.println("220 Java FTP Server ready.");
String line;
String user = null, cwd = rootDir;
int dataPort = -1; String dataHost = null;
while ((line = in.readLine()) != null) {
line = line.trim();
if (line.startsWith("USER ")) {
user = line.substring(5).trim();
out.println("331 Password required for " + user);
} else if (line.startsWith("PASS ")) {
// 實際應驗證 user/pass,此處簡單通過
out.println("230 User " + user + " logged in.");
} else if (line.startsWith("CWD ")) {
String path = line.substring(4).trim();
File newDir = new File(cwd, path);
if (newDir.isDirectory()) { cwd = newDir.getAbsolutePath(); out.println("250 CWD successful."); }
else out.println("550 Directory not found.");
} else if (line.startsWith("CDUP")) {
File parent = new File(cwd).getParentFile();
if (parent != null) { cwd = parent.getAbsolutePath(); out.println("250 CDUP successful."); }
else out.println("550 Root reached.");
} else if (line.startsWith("PORT ")) {
// 解析 h1,h2,h3,h4,p1,p2 → IP + Port
String[] parts = line.substring(5).split(",");
dataHost = String.join(".", parts[0], parts[1], parts[2], parts[3]);
dataPort = Integer.parseInt(parts[4]) * 256 + Integer.parseInt(parts[5]);
out.println("200 PORT command successful.");
} else if (line.startsWith("TYPE ")) {
out.println("200 Type set to " + line.substring(5).trim());
} else if (line.startsWith("RETR ")) {
String fileName = line.substring(5).trim();
File file = new File(cwd, fileName);
if (file.exists() && file.isFile()) {
out.println("150 Opening BINARY data connection for " + fileName);
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."); }
} else out.println("550 File not found.");
} else if (line.startsWith("STOR ")) {
String fileName = line.substring(5).trim();
File file = new File(cwd, fileName);
out.println("150 Opening BINARY data connection for " + fileName);
try (Socket dataSocket = new Socket(dataHost, dataPort);
FileOutputStream fos = new FileOutputStream(file);
InputStream dis = dataSocket.getInputStream()) {
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."); }
} else if (line.startsWith("DELE ")) {
String fileName = line.substring(5).trim();
File file = new File(cwd, fileName);
if (file.delete()) out.println("250 File deleted.");
else out.println("550 Delete failed.");
} else if (line.startsWith("LIST")) {
out.println("150 Opening ASCII data connection for file list.");
try (Socket dataSocket = new Socket(dataHost, dataPort);
PrintWriter dos = new PrintWriter(dataSocket.getOutputStream(), true)) {
File dir = new File(cwd);
File[] files = dir.listFiles();
if (files != null) {
for (File f : files) {
String type = f.isDirectory() ? "d" : "-";
dos.println(type + "rw-r--r-- 1 owner group " + f.length() + " " + new Date(f.lastModified()) + " " + f.getName());
}
}
out.println("226 Directory listing complete.");
} catch (Exception e) { out.println("426 Transfer aborted."); }
} else if (line.startsWith("QUIT")) {
out.println("221 Goodbye.");
break;
} else {
out.println("500 Unknown command: " + line);
}
}
} catch (IOException e) { e.printStackTrace(); }
finally { try { clientSocket.close(); } catch (IOException ignored) {} }
}
}
客戶端傳送 `PORT h1,h2,h3,h4,p1,p2`,伺服器解析為 IP `h1.h2.h3.h4` 和 Port `p1*256+p2`,隨後**主動連線**至該位址傳輸資料。
此實作雖為教學級,但完整呈現了 FTP 協議狀態機、雙通道架構、多執行緒分派、資料流複製的核心骨架,是構建生產級 FTP 服務的紮實基礎。