Linux配置FTP服务器实现文件上传下载功能
引言
FTP(File Transfer Protocol)作为最古老、最成熟的文件传输协议之一,至今仍在企业级应用中扮演重要角色。无论是部署静态资源、备份数据库,还是在不同系统间同步文件,FTP 服务都是不可或缺的基础组件。本文将带你从零开始,在 Linux 系统上搭建完整的 FTP 服务器,并通过 Java 编写客户端程序实现自动化上传与下载功能。
为什么选择 FTP?
虽然现代云存储和 HTTP API 已经非常流行,但 FTP 依然有其不可替代的优势:
- 简单稳定:协议成熟,兼容性极强。
- 权限控制灵活:可针对用户、目录设置读写权限。
- 断点续传支持:大文件传输更可靠。
- 广泛支持:几乎所有操作系统和编程语言都内置或提供库支持。
- 低资源占用:轻量级服务,适合嵌入式或老旧设备。
小贴士:如果你需要加密传输,建议使用 SFTP 或 FTPS,它们是在 FTP 基础上增加 SSL/TLS 加密的安全版本。
环境准备
我们将在一台标准的 Ubuntu 22.04 LTS 服务器上进行操作。如果你使用的是 CentOS、Debian 或其他发行版,命令略有不同,我会在文中注明。
最小系统要求:
- 操作系统:Ubuntu 22.04 / CentOS 8+
- 内存:≥ 512MB
- 磁盘空间:≥ 2GB(视文件大小而定)
- 网络:开放 20, 21, 及被动模式端口范围(如 49152–65534)
安装 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 服务器,请注意:
- 路由器端口转发:将 21 和被动端口范围映射到内网服务器。
- 云服务商安全组:如 AWS、阿里云、腾讯云等,需在控制台放行相应端口。
- 动态 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;
}性能优化建议
- 连接池:频繁创建/销毁连接开销大,建议使用连接池(如 Apache Commons Pool)。
- 批量操作:尽量减少交互次数,比如一次列出多个文件而不是逐个查询。
- 压缩传输:对文本文件启用压缩(需服务器支持)。
- 并发上传:使用多线程同时上传多个文件(注意服务器并发限制)。
安全加固建议
虽然我们已经做了基础安全配置,但仍可进一步加固:
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
- 用户名或密码错误
- 用户 shell 被设为
/usr/sbin/nologin→ 改为/bin/bash - 检查
/etc/shells是否包含用户的 shell
问题2:550 Permission denied
- 目标目录无写权限 →
chmod 755或chown allow_writeable_chroot=YES未设置- SELinux 阻止(CentOS)→
setsebool -P ftp_home_dir on
问题3:连接超时或卡住
- 防火墙未开放被动模式端口
- 客户端未启用被动模式 →
enterLocalPassiveMode() - 网络中间有 NAT/代理 → 尝试主动模式(不推荐)
问题4:中文文件名乱码
- 服务器和客户端编码不一致 → 统一使用 UTF-8
- 在 Java 中设置:
ftpClient.setControlEncoding("UTF-8") - 在 vsftpd.conf 中设置:
utf8_filesystem=YES
总结
通过本文,你已经掌握了:
✅ 在 Linux 上安装配置 vsftpd
✅ 创建安全的 FTP 用户和目录结构
✅ 配置防火墙和被动模式
✅ 使用 Java 编写功能完整的 FTP 客户端
✅ 实现断点续传、进度监控、自动重试
✅ 安全加固与性能优化技巧
FTP 虽然“古老”,但在自动化部署、文件同步、备份归档等场景中依然高效可靠。结合 Java 的强大生态,你可以轻松构建企业级文件传输解决方案。
最后提醒
生产环境中请务必使用 FTPS 或 SFTP 替代明文 FTP,保护你的数据安全!
以上就是Linux配置FTP服务器实现文件上传下载功能的详细内容,更多关于Linux配置FTP文件上传下载的资料请关注脚本之家其它相关文章!
相关文章
用DNSPod和Squid打造自己的CDN (六) 编译并安装Squid
这篇文章主要介绍centos下编译并安装Squid的方法,需要的朋友可以参考下2013-04-04
Linux学习之CentOS(二十二)--进入单用户模式下修改Root用户的密码
这篇文章主要介绍了Linux学习之CentOS(二十二)--进入单用户模式下修改Root用户的密码,有需要的可以了解一下。2016-11-11


最新评论