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

從零構建 Java FTP 服務器:核心協議實現與多線程架構完整指南

     編輯:Java 相關知識

FTP (File Transfer Protocol) 作為經典文件傳輸協議,雖然安全性不如 SFTP,但在內網文件分發、舊系統集成、嵌入式設備等場景仍不可替代。本文從協議核心命令入手,實現一個功能完整的 Java FTP 服務器雛形。

FTP 協議核心命令集

類別命令功能響應碼示例
訪問控制USER <username>提交用戶名331 Password required
PASS <password>提交密碼230 Login successful
QUIT斷開連接221 Goodbye
目錄操作CWD <path>切換工作目錄250 Directory changed
CDUP返回父目錄250 Directory changed
PWD顯示當前目錄257 "/home/user"
傳輸參數TYPE [A|I]設置傳輸模式 (ASCII/Binary)200 Type set to I
PORT h1,h2,h3,h4,p1,p2主動模式數據端口200 PORT command successful
PASV被動模式 (服務器開放端口)227 Entering Passive Mode (h1,h2,h3,h4,p1,p2)
文件傳輸RETR <file>下載文件150 Opening data connection / 226 Transfer complete
STOR <file>上傳文件150 Opening data connection / 226 Transfer complete
DELE <file>刪除文件250 File deleted
LIST [path]列表目錄/文件150 Opening data connection / 226 Transfer complete

架構設計:主線程監聽 + 會話線程 + 數據連接

ServerSocket (21端口) 接受連接
    │
    ├─ SessionThread (每客戶端一線程)
    │     ├─ 解析控制命令 (USER, PASS, CWD, TYPE, PORT, PASV, QUIT...)
    │     ├─ 維護會話狀態 (用戶、工作目錄、傳輸模式、數據連接模式)
    │     └─ 需數據傳輸時 (RETR, STOR, LIST) → 建立/接受數據 Socket
    │
    └─ DataConnection (臨時)
          ├─ 主動模式: 客戶端發 PORT → 服務器連接客戶端 IP:Port
          └─ 被動模式: 服務器開放隨機端口 → 客戶端連接服務器

核心代碼實現 (關鍵片段)

1. 主服務器啟動類

public class SimpleFtpServer {
    private static final int CONTROL_PORT = 21;
    private static String rootDirectory = "C:/ftp_root";  // 可通過參數配置

    public static void main(String[] args) {
        if (args.length > 0) rootDirectory = args[0];
        
        try (ServerSocket serverSocket = new ServerSocket(CONTROL_PORT)) {
            System.out.println("FTP Server started on port " + CONTROL_PORT + ", root: " + rootDirectory);
            while (true) {
                Socket clientSocket = serverSocket.accept();
                new SessionThread(clientSocket).start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

2. 會話線程核心邏輯

class SessionThread extends Thread {
    private Socket controlSocket;
    private BufferedReader in;
    private PrintWriter out;
    private String currentDir = "/";
    private boolean authenticated = false;
    private String username;
    private int dataPort;  // PORT 模式客戶端端口
    private String dataHost;  // PORT 模式客戶端 IP
    private ServerSocket passiveServerSocket;  // PASV 模式服務端 Socket

    public SessionThread(Socket socket) { this.controlSocket = socket; }

    @Override
    public void run() {
        try {
            in = new BufferedReader(new InputStreamReader(controlSocket.getInputStream()));
            out = new PrintWriter(controlSocket.getOutputStream(), true);
            sendResponse(220, "Welcome to Simple Java FTP Server");
            
            String line;
            while ((line = in.readLine()) != null) {
                System.out.println("CMD: " + line);
                processCommand(line.trim());
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            closeQuietly();
        }
    }

    private void processCommand(String cmd) {
        if (cmd.startsWith("USER ")) {
            username = cmd.substring(5).trim();
            sendResponse(331, "Password required for " + username);
        } else if (cmd.startsWith("PASS ")) {
            // 簡單演示:接受任意密碼,生產環境需對接認證系統
            authenticated = true;
            currentDir = rootDirectory;
            sendResponse(230, "User " + username + " logged in");
        } else if (!authenticated) {
            sendResponse(530, "Please login with USER and PASS");
        } else if (cmd.startsWith("CWD ")) {
            String path = cmd.substring(4).trim();
            File newDir = resolvePath(path);
            if (newDir.exists() && newDir.isDirectory()) {
                currentDir = newDir.getAbsolutePath();
                sendResponse(250, "Directory changed to " + currentDir);
            } else sendResponse(550, "Directory not found");
        } else if (cmd.equals("CDUP")) {
            File parent = new File(currentDir).getParentFile();
            if (parent != null) {
                currentDir = parent.getAbsolutePath();
                sendResponse(250, "Directory changed to " + currentDir);
            } else sendResponse(550, "Already at root");
        } else if (cmd.startsWith("TYPE ")) {
            // 簡單支持: A=ASCII, I=Binary (圖片/壓縮包必須用 Binary)
            sendResponse(200, "Type set to " + cmd.substring(5).trim());
        } else if (cmd.startsWith("PORT ")) {
            // 解析 PORT h1,h2,h3,h4,p1,p2
            String[] parts = cmd.substring(5).split(",");
            if (parts.length == 6) {
                dataHost = String.join(".", parts[0], parts[1], parts[2], parts[3]);
                dataPort = (Integer.parseInt(parts[4]) << 8) + Integer.parseInt(parts[5]);
                sendResponse(200, "PORT command successful");
            } else sendResponse(501, "Invalid PORT format");
        } else if (cmd.equals("PASV")) {
            // 被動模式:在隨機端口監聽
            try {
                passiveServerSocket = new ServerSocket(0);  // 系統分配端口
                int port = passiveServerSocket.getLocalPort();
                int p1 = port >> 8, p2 = port & 0xFF;
                String localIp = InetAddress.getLocalHost().getHostAddress().replace('.', ',');
                sendResponse(227, "Entering Passive Mode (" + localIp + "," + p1 + "," + p2 + ")");
            } catch (IOException e) {
                sendResponse(425, "Can't open passive connection");
            }
        } else if (cmd.startsWith("RETR ")) {
            handleDataTransfer(cmd.substring(5).trim(), true);  // 下載
        } else if (cmd.startsWith("STOR ")) {
            handleDataTransfer(cmd.substring(5).trim(), false); // 上傳
        } else if (cmd.equals("LIST")) {
            handleList();
        } else if (cmd.startsWith("DELE ")) {
            File file = resolvePath(cmd.substring(5).trim());
            if (file.delete()) sendResponse(250, "File deleted");
            else sendResponse(550, "Delete failed");
        } else if (cmd.equals("QUIT")) {
            sendResponse(221, "Goodbye");
            closeQuietly();
        } else {
            sendResponse(502, "Command not implemented");
        }
    }

    private void handleDataTransfer(String fileName, boolean isDownload) {
        Socket dataSocket = null;
        try {
            // 1. 建立數據連接
            if (dataHost != null && dataPort > 0) {
                // 主動模式
                dataSocket = new Socket(dataHost, dataPort);
                dataHost = null; dataPort = 0;  // 用完重置
            } else if (passiveServerSocket != null) {
                // 被動模式
                dataSocket = passiveServerSocket.accept();
                passiveServerSocket.close();
                passiveServerSocket = null;
            } else {
                sendResponse(425, "No data connection established (use PORT or PASV)");
                return;
            }

            sendResponse(150, "Opening " + (isDownload ? "download" : "upload") + " data connection");
            
            File file = resolvePath(fileName);
            byte[] buffer = new byte[8192];
            int len;

            if (isDownload) {
                // RETR: 文件 → 數據 Socket 輸出流
                try (FileInputStream fis = new FileInputStream(file);
                     OutputStream dos = dataSocket.getOutputStream()) {
                    while ((len = fis.read(buffer)) != -1) dos.write(buffer, 0, len);
                }
            } else {
                // STOR: 數據 Socket 輸入流 → 文件
                try (FileOutputStream fos = new FileOutputStream(file);
                     InputStream dis = dataSocket.getInputStream()) {
                    while ((len = dis.read(buffer)) != -1) fos.write(buffer, 0, len);
                }
            }
            sendResponse(226, "Transfer complete");
        } catch (IOException e) {
            sendResponse(426, "Transfer aborted: " + e.getMessage());
        } finally {
            closeQuietly(dataSocket);
        }
    }

    private void handleList() {
        // 類似 handleDataTransfer,但發送目錄列表文本
        // 格式: "drwxr-xr-x 1 user group 0 Jan 1 00:00 dirname"
        // 或簡化: "d dirname" / "- filename"
    }

    private File resolvePath(String relativePath) {
        // 安全處理:防止目錄遍歷攻擊 (../../etc/passwd)
        File base = new File(rootDirectory).getAbsoluteFile();
        File target = new File(base, relativePath).getAbsoluteFile();
        if (!target.getPath().startsWith(base.getPath())) {
            return base;  // 越權訪問限制在根目錄
        }
        return target;
    }

    private void sendResponse(int code, String message) {
        out.println(code + " " + message);
        out.flush();
    }

    private void closeQuietly() {
        try { if (in != null) in.close(); } catch (IOException ignored) {}
        try { if (out != null) out.close(); } catch (Exception ignored) {}
        try { if (controlSocket != null) controlSocket.close(); } catch (IOException ignored) {}
        try { if (passiveServerSocket != null) passiveServerSocket.close(); } catch (IOException ignored) {}
    }
}

關鍵安全與健壯性補強

問題解決方案
明文傳輸密碼/數據生產環境必須遷移至 FTPS (SSL/TLS) 或 SFTP (SSH)
目錄遍歷攻擊resolvePath 規範化路徑並校驗前綴
主動模式防火牆阻擋優先實現/推薦被動模式 (PASV),並配置被動端口範圍
大文件傳輸中斷支持 REST (斷點續傳) 命令
併發連接耗盡線程池 + 連接數限制 + 空閒超時踢出
匿名/弱口令強制強密碼策略、賬號鎖定、IP 白名單

生產級替代方案推薦

  • Apache Mina FtpServer:成熟、可嵌入、支持 FTPS、虛擬文件系統、Spring 集成
  • CrushFTP / FileZilla Server / vsftpd:開箱即用的獨立服務器
  • Spring Integration FTP/FTPS:企業應用集成場景

理解 FTP 協議細節與多線程數據連接模型,是排查傳輸故障、實現定制化文件服務的基礎。

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