Linux

关注公众号 jb51net

关闭
首页 > 网站技巧 > 服务器 > Linux > Linux配置FTP文件上传下载

Linux配置FTP服务器实现文件上传下载功能

作者:知远漫谈

FTP作为最古老、最成熟的文件传输协议之一,至今仍在企业级应用中扮演重要角色,本文将带你从零开始,在 Linux 系统上搭建完整的 FTP 服务器,并通过 Java 编写客户端程序实现自动化上传与下载功能,需要的朋友可以参考下

引言

FTP(File Transfer Protocol)作为最古老、最成熟的文件传输协议之一,至今仍在企业级应用中扮演重要角色。无论是部署静态资源、备份数据库,还是在不同系统间同步文件,FTP 服务都是不可或缺的基础组件。本文将带你从零开始,在 Linux 系统上搭建完整的 FTP 服务器,并通过 Java 编写客户端程序实现自动化上传与下载功能。

为什么选择 FTP?

虽然现代云存储和 HTTP API 已经非常流行,但 FTP 依然有其不可替代的优势:

小贴士:如果你需要加密传输,建议使用 SFTP 或 FTPS,它们是在 FTP 基础上增加 SSL/TLS 加密的安全版本。

环境准备

我们将在一台标准的 Ubuntu 22.04 LTS 服务器上进行操作。如果你使用的是 CentOS、Debian 或其他发行版,命令略有不同,我会在文中注明。

最小系统要求:

安装 vsftpd —— 非常安全的 FTP 服务器

vsftpd(Very Secure FTP Daemon)是 Linux 上最受欢迎的 FTP 服务器软件之一,以其高性能和安全性著称。

sudo apt update
sudo apt install vsftpd -y

如果是 CentOS/RHEL:

sudo yum install vsftpd -y
# 或者在较新版本中:
sudo dnf install vsftpd -y

安装完成后,启动并设置开机自启:

sudo systemctl start vsftpd
sudo systemctl enable vsftpd
sudo systemctl status vsftpd

你应该看到类似如下输出:

● vsftpd.service - vsftpd FTP server
     Loaded: loaded (/lib/systemd/system/vsftpd.service; enabled; vendor preset: enabled)
     Active: active (running) since Mon 2024-06-17 10:00:00 UTC; 5s ago

配置 vsftpd —— 安全第一!

默认配置位于 /etc/vsftpd.conf,我们需要对其进行修改以满足生产环境需求。

首先备份原始配置:

sudo cp /etc/vsftpd.conf /etc/vsftpd.conf.bak

然后编辑配置文件:

sudo nano /etc/vsftpd.conf

推荐配置内容如下:

# 禁用匿名登录
anonymous_enable=NO
# 启用本地用户登录
local_enable=YES
# 允许本地用户写入
write_enable=YES
# 限制用户只能访问自己的主目录
chroot_local_user=YES
# 允许 chroot 目录可写(重要!否则无法上传)
allow_writeable_chroot=YES
# 使用被动模式(推荐用于公网访问)
pasv_enable=YES
pasv_min_port=49152
pasv_max_port=65534
# 启用日志记录
xferlog_enable=YES
xferlog_file=/var/log/vsftpd.log
xferlog_std_format=YES
# 设置连接超时时间(秒)
idle_session_timeout=600
data_connection_timeout=120
# 限制最大客户端数
max_clients=50
max_per_ip=5
# 拒绝某些用户登录(可选)
# userlist_enable=YES
# userlist_file=/etc/vsftpd.userlist
# userlist_deny=YES
# 启用 UTF-8 编码
utf8_filesystem=YES

注意:allow_writeable_chroot=YES 是关键配置。如果没有它,即使你开启了 write_enable,用户也无法在被 chroot 的目录中上传文件。

保存后重启服务:

sudo systemctl restart vsftpd

创建专用 FTP 用户

为了安全起见,不建议直接使用 root 或已有系统用户进行 FTP 操作。我们创建一个专用用户:

sudo adduser ftpuser

系统会提示你设置密码和填写一些信息(可以一路回车跳过)。接着,为该用户创建专属上传目录:

sudo mkdir -p /home/ftpuser/uploads
sudo chown ftpuser:ftpuser /home/ftpuser/uploads
sudo chmod 755 /home/ftpuser

权限说明:

  • 755 表示所有者可读写执行,组和其他人只读执行。
  • 如果希望其他用户也能上传,可设为 775,但需谨慎。

防火墙配置

Ubuntu 默认使用 ufw,CentOS 使用 firewalld。确保开放 FTP 端口:

Ubuntu:

sudo ufw allow 20/tcp
sudo ufw allow 21/tcp
sudo ufw allow 49152:65534/tcp
sudo ufw reload

CentOS:

sudo firewall-cmd --permanent --add-port=20-21/tcp
sudo firewall-cmd --permanent --add-port=49152-65534/tcp
sudo firewall-cmd --reload

测试 FTP 连接

我们可以使用命令行工具 ftp 或图形化工具 FileZilla 进行测试。

使用命令行测试:

ftp localhost

输入用户名 ftpuser 和密码,如果成功登录,你会看到:

Connected to localhost.
220 (vsFTPd 3.0.3)
Name (localhost:yourname): ftpuser
331 Please specify the password.
Password:
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
ftp>

尝试上传一个测试文件:

echo "Hello FTP Server!" > test.txt
ftp> put test.txt
ftp> ls
ftp> quit

如果一切顺利,文件应已上传至 /home/ftpuser/uploads/

外网访问注意事项

如果你希望通过公网 IP 访问 FTP 服务器,请注意:

  1. 路由器端口转发:将 21 和被动端口范围映射到内网服务器。
  2. 云服务商安全组:如 AWS、阿里云、腾讯云等,需在控制台放行相应端口。
  3. 动态 DNS(可选):如果你没有固定公网 IP,可使用 No-IP 或 DynDNS 服务绑定域名。

警告:FTP 协议本身是明文传输,包括用户名和密码。强烈建议仅在内网使用,或升级为 FTPS/SFTP。

FTP 工作模式图解

FTP 有两种工作模式:主动模式(Active)和被动模式(Passive)。理解它们对防火墙配置至关重要。

图表解读:

  • 主动模式中,服务器主动连接客户端的数据端口 —— 对客户端防火墙不友好。
  • 被动模式中,客户端连接服务器的数据端口 —— 更适合现代网络环境。
  • 我们前面配置的就是被动模式。

Java 实现 FTP 客户端 —— Apache Commons Net

现在进入重头戏 —— 用 Java 编写 FTP 客户端程序,实现自动上传、下载、列出目录等功能。

第一步:添加 Maven 依赖

在你的 pom.xml 中加入:

<dependency>
    <groupId>commons-net</groupId>
    <artifactId>commons-net</artifactId>
    <version>3.9.0</version>
</dependency>

如果你使用 Gradle:

implementation 'commons-net:commons-net:3.9.0'

pache Commons Net 是一个强大的网络协议库,支持 FTP、SMTP、POP3、Telnet 等多种协议。官方文档详见:Apache Commons Net

Java 代码实战 —— 基础上传下载

下面是一个完整的 Java 类,封装了 FTP 连接、上传、下载、断开等操作。

import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;
import org.apache.commons.net.ftp.FTPReply;
import java.io.*;
import java.nio.charset.StandardCharsets;
public class FTPUploader {
    private String server;
    private int port;
    private String username;
    private String password;
    private FTPClient ftpClient;
    public FTPUploader(String server, int port, String username, String password) {
        this.server = server;
        this.port = port;
        this.username = username;
        this.password = password;
        this.ftpClient = new FTPClient();
    }
    /**
     * 连接到 FTP 服务器
     */
    public boolean connect() {
        try {
            ftpClient.connect(server, port);
            int replyCode = ftpClient.getReplyCode();
            if (!FTPReply.isPositiveCompletion(replyCode)) {
                System.err.println("连接失败,服务器返回码:" + replyCode);
                return false;
            }
            boolean loggedIn = ftpClient.login(username, password);
            if (!loggedIn) {
                System.err.println("登录失败,用户名或密码错误");
                return false;
            }
            // 设置被动模式
            ftpClient.enterLocalPassiveMode();
            // 设置二进制传输模式(推荐用于所有文件)
            ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
            // 设置编码(避免中文乱码)
            ftpClient.setControlEncoding(StandardCharsets.UTF_8.name());
            System.out.println("✅ 成功连接到 FTP 服务器: " + server);
            return true;
        } catch (IOException e) {
            System.err.println("连接异常:" + e.getMessage());
            return false;
        }
    }
    /**
     * 上传文件
     */
    public boolean uploadFile(String localFilePath, String remoteFileName) {
        File localFile = new File(localFilePath);
        if (!localFile.exists()) {
            System.err.println("本地文件不存在:" + localFilePath);
            return false;
        }
        try (InputStream inputStream = new FileInputStream(localFile)) {
            boolean done = ftpClient.storeFile(remoteFileName, inputStream);
            if (done) {
                System.out.println("📤 文件上传成功: " + remoteFileName);
                return true;
            } else {
                System.err.println("❌ 文件上传失败: " + remoteFileName);
                return false;
            }
        } catch (IOException e) {
            System.err.println("上传过程中发生异常:" + e.getMessage());
            return false;
        }
    }
    /**
     * 下载文件
     */
    public boolean downloadFile(String remoteFileName, String localFilePath) {
        try (OutputStream outputStream = new FileOutputStream(localFilePath)) {
            boolean done = ftpClient.retrieveFile(remoteFileName, outputStream);
            if (done) {
                System.out.println("📥 文件下载成功: " + localFilePath);
                return true;
            } else {
                System.err.println("❌ 文件下载失败: " + remoteFileName);
                return false;
            }
        } catch (IOException e) {
            System.err.println("下载过程中发生异常:" + e.getMessage());
            return false;
        }
    }
    /**
     * 列出远程目录内容
     */
    public void listFiles(String remoteDir) {
        try {
            ftpClient.changeWorkingDirectory(remoteDir);
            FTPFile[] files = ftpClient.listFiles();
            System.out.println("📁 当前目录: " + remoteDir);
            System.out.println("----------------------------------------");
            for (FTPFile file : files) {
                String fileInfo = file.isDirectory() ? "[DIR] " : "[FILE]";
                fileInfo += " " + file.getName() + " (" + file.getSize() + " bytes)";
                System.out.println(fileInfo);
            }
        } catch (IOException e) {
            System.err.println("列出文件失败:" + e.getMessage());
        }
    }
    /**
     * 创建远程目录
     */
    public boolean makeDirectory(String dirPath) {
        try {
            boolean success = ftpClient.makeDirectory(dirPath);
            if (success) {
                System.out.println("✅ 目录创建成功: " + dirPath);
                return true;
            } else {
                System.err.println("❌ 目录创建失败: " + dirPath);
                return false;
            }
        } catch (IOException e) {
            System.err.println("创建目录异常:" + e.getMessage());
            return false;
        }
    }
    /**
     * 删除远程文件
     */
    public boolean deleteFile(String fileName) {
        try {
            boolean success = ftpClient.deleteFile(fileName);
            if (success) {
                System.out.println("🗑️  文件删除成功: " + fileName);
                return true;
            } else {
                System.err.println("❌ 文件删除失败: " + fileName);
                return false;
            }
        } catch (IOException e) {
            System.err.println("删除文件异常:" + e.getMessage());
            return false;
        }
    }
    /**
     * 断开连接
     */
    public void disconnect() {
        if (ftpClient.isConnected()) {
            try {
                ftpClient.logout();
                ftpClient.disconnect();
                System.out.println("🔌 已断开 FTP 连接");
            } catch (IOException e) {
                System.err.println("断开连接时发生异常:" + e.getMessage());
            }
        }
    }
}

测试 Java 客户端

编写一个简单的测试类来验证功能:

public class FTPTest {
    public static void main(String[] args) {
        // 替换为你自己的服务器信息
        String server = "your-server-ip-or-domain";
        int port = 21;
        String username = "ftpuser";
        String password = "your-password";
        FTPUploader uploader = new FTPUploader(server, port, username, password);
        // 1. 连接服务器
        if (!uploader.connect()) {
            System.exit(1);
        }
        // 2. 创建子目录
        uploader.makeDirectory("test-dir");
        // 3. 上传文件
        uploader.uploadFile("/path/to/local/file.txt", "test-dir/uploaded-file.txt");
        // 4. 列出目录内容
        uploader.listFiles("test-dir");
        // 5. 下载文件
        uploader.downloadFile("test-dir/uploaded-file.txt", "/tmp/downloaded-file.txt");
        // 6. 删除文件(可选)
        // uploader.deleteFile("test-dir/uploaded-file.txt");
        // 7. 断开连接
        uploader.disconnect();
    }
}

运行后,你将看到类似输出:

✅ 成功连接到 FTP 服务器: 192.168.1.100
✅ 目录创建成功: test-dir
📤 文件上传成功: test-dir/uploaded-file.txt
📁 当前目录: test-dir
----------------------------------------
[FILE] uploaded-file.txt (18 bytes)
📥 文件下载成功: /tmp/downloaded-file.txt
🔌 已断开 FTP 连接

高级功能 —— 断点续传与进度监控

对于大文件传输,断点续传和进度条是刚需。Apache Commons Net 支持这些功能。

断点续传上传:

public boolean uploadWithResume(String localFilePath, String remoteFileName) {
    File localFile = new File(localFilePath);
    if (!localFile.exists()) {
        System.err.println("本地文件不存在:" + localFilePath);
        return false;
    }
    try {
        // 获取远程文件大小(如果存在)
        long remoteSize = 0;
        FTPFile[] files = ftpClient.listFiles(remoteFileName);
        if (files.length > 0) {
            remoteSize = files[0].getSize();
        }
        // 如果远程文件大小 >= 本地文件,则无需上传
        if (remoteSize >= localFile.length()) {
            System.out.println("✅ 文件已完整上传,跳过:" + remoteFileName);
            return true;
        }
        // 打开输入流,从断点位置开始读取
        RandomAccessFile raf = new RandomAccessFile(localFile, "r");
        raf.seek(remoteSize); // 移动到断点位置
        // 告诉服务器从指定位置开始写入
        ftpClient.setRestartOffset(remoteSize);
        InputStream inputStream = new FileInputStream(raf.getFD());
        boolean done = ftpClient.storeFile(remoteFileName, inputStream);
        raf.close();
        inputStream.close();
        if (done) {
            System.out.println("📤 断点续传完成: " + remoteFileName);
            return true;
        } else {
            System.err.println("❌ 断点续传失败: " + remoteFileName);
            return false;
        }
    } catch (IOException e) {
        System.err.println("断点续传异常:" + e.getMessage());
        return false;
    }
}

带进度条的上传(使用观察者模式):

import java.util.function.Consumer;
public class ProgressMonitorInputStream extends InputStream {
    private final InputStream inputStream;
    private final long totalBytes;
    private long bytesRead = 0;
    private final Consumer<Long> progressCallback;
    public ProgressMonitorInputStream(InputStream inputStream, long totalBytes, Consumer<Long> progressCallback) {
        this.inputStream = inputStream;
        this.totalBytes = totalBytes;
        this.progressCallback = progressCallback;
    }
    @Override
    public int read() throws IOException {
        int b = inputStream.read();
        if (b != -1) {
            bytesRead++;
            progressCallback.accept(bytesRead);
        }
        return b;
    }
    @Override
    public int read(byte[] b, int off, int len) throws IOException {
        int result = inputStream.read(b, off, len);
        if (result != -1) {
            bytesRead += result;
            progressCallback.accept(bytesRead);
        }
        return result;
    }
    @Override
    public void close() throws IOException {
        inputStream.close();
    }
}

然后在上传方法中使用:

public boolean uploadWithProgress(String localFilePath, String remoteFileName) {
    File localFile = new File(localFilePath);
    if (!localFile.exists()) {
        System.err.println("本地文件不存在:" + localFilePath);
        return false;
    }
    try (FileInputStream fis = new FileInputStream(localFile)) {
        ProgressMonitorInputStream pmis = new ProgressMonitorInputStream(
            fis,
            localFile.length(),
            current -> {
                double percent = (double) current / localFile.length() * 100;
                System.out.printf("\r📤 上传进度: %.2f%% (%d/%d bytes)", percent, current, localFile.length());
            }
        );
        boolean done = ftpClient.storeFile(remoteFileName, pmis);
        System.out.println(); // 换行
        if (done) {
            System.out.println("✅ 上传完成: " + remoteFileName);
            return true;
        } else {
            System.err.println("❌ 上传失败: " + remoteFileName);
            return false;
        }
    } catch (IOException e) {
        System.err.println("上传异常:" + e.getMessage());
        return false;
    }
}

调用示例:

uploader.uploadWithProgress("/large/video.mp4", "videos/video.mp4");

输出效果:

📤 上传进度: 47.32% (496210944/1048576000 bytes)

异常处理与重试机制

网络不稳定时,FTP 操作可能失败。我们可以加入重试逻辑:

public boolean uploadWithRetry(String localFilePath, String remoteFileName, int maxRetries) {
    for (int attempt = 1; attempt <= maxRetries; attempt++) {
        System.out.println("🔄 尝试第 " + attempt + " 次上传...");
        if (uploadFile(localFilePath, remoteFileName)) {
            return true;
        }
        if (attempt < maxRetries) {
            System.out.println("⏳ " + (5 * attempt) + " 秒后重试...");
            try {
                Thread.sleep(5000 * attempt); // 指数退避
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            }
        }
    }
    System.err.println("❌ 达到最大重试次数,放弃上传");
    return false;
}

性能优化建议

  1. 连接池:频繁创建/销毁连接开销大,建议使用连接池(如 Apache Commons Pool)。
  2. 批量操作:尽量减少交互次数,比如一次列出多个文件而不是逐个查询。
  3. 压缩传输:对文本文件启用压缩(需服务器支持)。
  4. 并发上传:使用多线程同时上传多个文件(注意服务器并发限制)。

安全加固建议

虽然我们已经做了基础安全配置,但仍可进一步加固:

1. 使用 FTPS(FTP over SSL)

修改 /etc/vsftpd.conf

ssl_enable=YES
rsa_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem
rsa_private_key_file=/etc/ssl/private/ssl-cert-snakeoil.key
allow_anon_ssl=NO
force_local_data_ssl=YES
force_local_logins_ssl=YES
ssl_tlsv1=YES
ssl_sslv2=NO
ssl_sslv3=NO

Java 客户端需改用 FTPSClient

import org.apache.commons.net.ftp.FTPSClient;
// ...
this.ftpClient = new FTPSClient(true); // true 表示显式 SSL
((FTPSClient) ftpClient).execPBSZ(0);
((FTPSClient) ftpClient).execPROT("P");

2. 限制用户权限

创建 /etc/vsftpd.userlist 并添加允许登录的用户:

echo "ftpuser" | sudo tee -a /etc/vsftpd.userlist

然后在配置中启用:

userlist_enable=YES
userlist_file=/etc/vsftpd.userlist
userlist_deny=NO  # 只允许列表中的用户登录

3. 启用日志审计

确保日志路径存在并定期轮转:

sudo touch /var/log/vsftpd.log
sudo chmod 644 /var/log/vsftpd.log

配置 logrotate(/etc/logrotate.d/vsftpd):

/var/log/vsftpd.log {
    weekly
    missingok
    rotate 12
    compress
    delaycompress
    notifempty
    create 644 root root
}

自动化脚本示例

你可以编写 Shell 脚本定时备份网站文件到 FTP:

#!/bin/bash
# backup-to-ftp.sh
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/backup"
WEB_ROOT="/var/www/html"
FTP_SERVER="your.ftp.server"
FTP_USER="ftpuser"
FTP_PASS="password"
# 创建备份
tar -czf "$BACKUP_DIR/website_$DATE.tar.gz" -C /var/www html
# 上传到 FTP
ftp -n <<EOF
open $FTP_SERVER
user $FTP_USER $FTP_PASS
binary
put $BACKUP_DIR/website_$DATE.tar.gz
bye
EOF
# 清理本地7天前的备份
find $BACKUP_DIR -name "website_*.tar.gz" -mtime +7 -delete
echo "✅ 备份完成: website_$DATE.tar.gz"

添加到 crontab:

crontab -e
# 每天凌晨2点执行
0 2 * * * /path/to/backup-to-ftp.sh

🆘 常见问题排查

问题1:530 Login incorrect

问题2:550 Permission denied

问题3:连接超时或卡住

问题4:中文文件名乱码

总结

通过本文,你已经掌握了:

✅ 在 Linux 上安装配置 vsftpd
✅ 创建安全的 FTP 用户和目录结构
✅ 配置防火墙和被动模式
✅ 使用 Java 编写功能完整的 FTP 客户端
✅ 实现断点续传、进度监控、自动重试
✅ 安全加固与性能优化技巧

FTP 虽然“古老”,但在自动化部署、文件同步、备份归档等场景中依然高效可靠。结合 Java 的强大生态,你可以轻松构建企业级文件传输解决方案。

最后提醒

生产环境中请务必使用 FTPSSFTP 替代明文 FTP,保护你的数据安全!

以上就是Linux配置FTP服务器实现文件上传下载功能的详细内容,更多关于Linux配置FTP文件上传下载的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文