首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 程式設計入門

Java 實作 FTP 伺服器核心協議與多執行緒架構完整解析

     編輯:Java 程式設計入門

FTP (File Transfer Protocol) 以命令/回應模式運作在 TCP 21 埠 (控制連線),資料傳輸另建 TCP 連線 (主動/被動模式)。本文展示一個純 Java 實作的 FTP 伺服器核心框架,涵蓋命令解析、多執行緒分派、資料連線建立、檔案上傳/下載/刪除/列表。

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) {} }
    }
}

關鍵實作細節

1. PORT 命令解析 (主動模式)

客戶端傳送 `PORT h1,h2,h3,h4,p1,p2`,伺服器解析為 IP `h1.h2.h3.h4` 和 Port `p1*256+p2`,隨後**主動連線**至該位址傳輸資料。

2. 資料連線管理

  • 每個傳輸命令 (RETR/STOR/LIST) **另建一個 Socket** 連至客戶端指定的 dataPort。
  • 傳輸完成後**立即關閉**資料 Socket,控制連線保持開啟。
  • 緩衝區 8KB 平衡記憶體與效能。

3. 目錄遍歷與安全性

  • 工作目錄 `cwd` 限制在 `rootDir` 子樹下 (實際應加入 `canonicalPath` 檢查防止目錄遍歷攻擊 `../../etc/passwd`)。
  • LIST 輸出模擬 Unix `ls -l` 格式,方便圖形化 FTP 客戶端解析。

4. 回應碼規範遵循

  • 1xx: 正在處理 (資料連線開啟中)
  • 2xx: 成功完成
  • 4xx: 暫時失敗 (可重試)
  • 5xx: 永久失敗 (語法錯誤、權限不足、檔案不存在)

擴充與生產化方向

  1. 被動模式 (PASV/EPSV): 伺服器開啟資料埠並通知客戶端連入,解決 NAT/防火牆問題 — 企業級部署必備。
  2. 使用者認證體系: 整合 LDAP/DB/檔案帳號、支援 TLS/SSL (FTPS) 加密控制與資料連線。
  3. 虛擬檔案系統: 抽象 `FileSystemView` 介面,支援資料庫 Blob、雲端儲存 (S3)、壓縮檔內部瀏覽。
  4. 非阻塞 I/O (NIO): 使用 `Selector` + `ServerSocketChannel` 單執行緒處理萬級併發連線。
  5. 配置管理: 外部化根目錄、埠號、最大連線、頻寬限制、日誌等級。

此實作雖為教學級,但完整呈現了 FTP 協議狀態機、雙通道架構、多執行緒分派、資料流複製的核心骨架,是構建生產級 FTP 服務的紮實基礎。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved