ftp

环境:centos8.1

安装

1
yum -y install vsftpd

配置

1
2
3
4
5
# 进入root用户
sudo -i

# 进入ftp配置文件
vim /etc/vsftpd/vsftpd.conf

匿名登录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 匿名登录开启
anonymous_enable=YES

local_enable=YES

write_enable=YES

local_umask=022
# 匿名上传
anon_upload_enable=YES
# 匿名操作文件夹
anon_mkdir_write_enable=YES
# 允许修改或删除文件
anon_other_write_enable=YES

dirmessage_enable=YES

xferlog_enable=YES

connect_from_port_20=YES

xferlog_std_format=YES

listen=NO
# 匿名权限,新增文件权限umask
anon_umask=022
# 匿名FTP的根路径
# 默认根目录/var/ftp/pub,此处修改到另一个目录/user/wlw
anon_root=/user/wlw
local_root=/user/wlw
listen_ipv6=YES

pam_service_name=vsftpd
userlist_enable=YES

启动ftp服务

1
2
3
4
systemctl start vsftpd.service
systemctl status vsftpd.service

或者命令:service vsftpd.service start

防火墙配置

1
2
3
4
5
firewall-cmd --zone=public --add-port=21/tcp --permanent

firewall-cmd --zone=public --add-port=1025-65535/tcp --permanent

systemctl restart firewalld

selinux配置

基本

  1. selinux是内核级加强型火墙
  2. 开启或关闭selinux,需要重启电脑

selinux的状态:

状态 解释
Enforcing 警告并拒绝
Permissive 警告并允许
Disabled 关闭

查看selinux状态

1
getenforce

更改selinux的状态

1
2
setenforce 1		##更改selinux的状态为Enforcing
setenforce 0 ##更改selinux的状态为Permissive

FTP相关

vsftpd 默认被 SELinux 拦截,会碰到如下问题:

  1. 226 Transfer done (but failed to open directory).(传输完成,但是打开路径失败)
  2. 550 Failed to change directory(更改路径失败)
  3. 553 Could not create file.
  4. 或者干脆在发送了LIST命令以后,服务器没响应,超时断开。

方法1:降低SELinux安全级别,把enforcing降低到permissive 或disabled

1
2
3
4
5
# 打开文件
vim /etc/sysconfig/selinux

# 手动修改selinux状态为Disabled。并重启。
SELINUX=disabled

方法2:只修改SELinux的FTP相关内容

1
2
3
4
5
6
7
8
9
10
# 查看SELinux中有关FTP的设置状态
getsebool -a | grep ftp

# 加-P是保存选项,每次重启时不必重新执行这个命令了
setsebool -P ftpd_disable_trans 1
#
setsebool -P ftp_home_dir 1

# 重启FTP服务
service vsftpd restart

路径问题

无论匿名用户还是实名用户都要注意可访问目录的权限问题。

匿名FTP的根路径,权限:

1
2
drwxr-xr-x    3 root root   17 6月   6 05:01 user
drwxr-xr-x 3 root root 19 6月 6 05:01 wlw

配置的最佳实践是将【var/ftp/pub】(/user/wlw/share)目录所有者改为ftp,文件操作仅在share目录进行。

1
2
3
4
5
6
7
chown ftp:ftp -R /var/ftp/pub
chown ftp:ftp -R /user/wlw/share

drwxrwxrwx 2 ftp ftp 155 6月 9 20:49 share

# 或,如果使用var/ftp/pub目录
drwxrwxrwx. 2 ftp ftp 43 6月 11 17:22 pub

总言之,

在/etc/vsft pd/vsftpd.conf查看根路径。

默认是/var/ftp/pub【此时将路径写成pub】。

1
2
3
4
根目录是/user/wlw
当要访问/user/wlw/share目录
就要将路径写成share目录
而不是/user/wlw/share

即在真正调用ftp路径时候,只需要将share目录作为路径(应该忽略ftp根路径),不需要配/user/wlw/share整个路径。

windows客户端工具

FTP配置后,先使用客户端工具尝试访问成功之后,再使用程序测试。

浏览器

资源管理器

cmd命令

cmd命令的匿名用户名是ftp,密码为空

Java程序

Java中与FTP服务器交互的客户端程序有两种:

  1. 【推进】使用第三方依赖包commons-net的FTPClient
  2. 使用JDK自带sun.net.ftp.FtpClient(JDK自带程序存在一些问题)

目前,commons-net的FTPClient使用更为普遍。

安装依赖

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/commons-net/commons-net -->
<dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>
<version>3.6</version>
</dependency>

常用API

初始化
1
FTPClient ftp = new FTPClient()
连接/断连
1
2
3
ftp.connect(host, port);
boolean ftp.isConnected()
ftp.disconnect();
登录/注销
1
2
ftp.login(username, password);
ftp.logout();
FTP状态码
1
2
3
int replyCode = ftp.getReplyCode();

boolean isLoginSuccess = FTPReply.isPositiveCompletion(replyCode)

replyCode含义

代码 描述
FTP服务器: 220 链接成功
FTP客户端: USER useway 输入用户名
FTP服务器: 331 Please specify the password. 请输入密码
FTP客户端: PASS !@#$%abce 输入密码
FTP服务器: 230 Login successful. 登录成功
FTP客户端: CWD /home/useway 切换目录
FTP服务器: 250 Directory successfully changed. 目录切换成功
FTP客户端: EPSV ALL 为EPSV被动链接方式
FTP服务器: 200 EPSV ALL ok. OK
FTP客户端: EPSV 链接
FTP服务器: 229 Entering Extended Passive Mode(62501) 被动链接端口为62501
FTP客户端: LIST 执行LIST显示文件列表
FTP服务器: 150 Here comes the directory listing. 列表从62501端口被发送
FTP服务器: 226 Directory send OK. 发送完成
FTP客户端: QUIT 退出FTP
FTP服务器: 221 Goodbye. 再见
属性设置
1
2
3
4
5
6
7
8
ftp.setConnectTimeout(60000);
ftp.enterLocalPassiveMode();
ftp.setFileType(FTP.BINARY_FILE_TYPE);
ftp.setControlEncoding("UTF-8");

// 设置linux环境
FTPClientConfig conf = new FTPClientConfig(FTPClientConfig.SYST_UNIX);
ftp.configure(conf);
目录/文件操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 切换到目录(是否成功)
boolean hasPath = ftp.changeWorkingDirectory(path);

// 新建目录
ftp.makeDirectory("want")

// 根据文件名,找到文件流
InputStream basicFile = ftp.retrieveFileStream(originFilename);
boolean isExisted = !(basicFile == null || ftp.getReplyCode() == FTPReply.FILE_UNAVAILABLE);

// 找到目录下所有文件
FTPFile[] ftpFiles = ftp.listFiles();

// 上传文件流
boolean isStoreFileSuccess = ftp.storeFile(uploadFilename, inputStream);

案例

配置文件

1
2
3
4
5
6
7
8
9
10
11
performance.ftp.ip=192.168.145.128
performance.ftp.port=21

# 注意路径,不应包含ftp的根路径
performance.ftp.path=share

# 匿名登录的用户名密码
performance.ftp.username=anonymous
performance.ftp.password=null

performance.ftp.max-file-size=41943040
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* ftp服务器地址
*/
@Value("${performance.ftp.ip}")
private String ftpHost;

@Value("${performance.ftp.port:21}")
private int ftpPort;

@Value("${performance.ftp.path}")
private String ftpPath;

@Value("${performance.ftp.username}")
private String ftpUsername;

@Value("${performance.ftp.password}")
private String ftpPassword;

// 调用FTP上报文件流
List<InputStream> csvFileStreamList = CsvUtil.createCsvFileStreamList();

try {
commonFtpFileService.uploadFileList(csvFileStreamList, prefixFileName, ftpPath, ftpHost, ftpPort, ftpUsername, ftpPassword);
} catch (final Exception e) {
LOGGER.error("上传文件失败");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252

import org.apache.commons.net.ftp.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;


@Service
public class CommonFtpFileServiceImpl implements ICommonFtpFileService {
private final static Logger LOGGER = LoggerFactory.getLogger(CommonFtpFileServiceImpl.class);

private FTPClient ftp = new FTPClient();

private final static String fileNameReset = "_R";
private final static String fileNameSplit = "_P";
private final static String fileNameEnd = "_END";
private final static String fileNameExtension = ".csv";

/**
* 批量上传文件到FTP服务器
*
* @param inputStreamList 文件流
* @param prefixFileName 文件名
* @param path 文件存放目录路径
* @param host 服务器地址
* @param port 端口
* @param username 用户名
* @param password 密码
*/
@Override
public void uploadFileList(List<InputStream> inputStreamList, String prefixFileName, String path, String host, int port, String username, String password) {
if (!login(host, port, username, password)) {
return;
}
if (!hasPath(path)) {
logout();
return;
}
int count = inputStreamList.size();
for (int index = 0; index < count; index++) {
String noStr = createNo(index);
String fileName = prefixFileName + fileNameSplit + noStr;

if (index == count - 1) {
fileName = fileName + fileNameEnd;
}

fileName = fileName + fileNameExtension;

upload(inputStreamList.get(index), fileName);
}
logout();
}

/**
* 登录FTP服务器
*
* @param host 服务器地址
* @param port 端口
* @param username 用户名
* @param password 密码
* @return 成功返回true,否则返回false
*/
public Boolean login(String host, int port, String username, String password) {
try {
// 连接
ftp.connect(host, port);
// 登录
ftp.login(username, password);
// 登录返回值
int replyCode = ftp.getReplyCode();
if (FTPReply.isPositiveCompletion(replyCode)) {
settingFtp();
return true;
}
ftp.disconnect();
} catch (IOException e) {
LOGGER.error("连接FTP失败,请检查配置信息!", e);
}
return false;
}

/**
* 上传文件到FTP服务器
*
* @param inputStream 文件流
* @param filename 文件名
* @param path 文件存放目录路径
* @param host 服务器地址
* @param port 端口
* @param username 用户名
* @param password 密码
* @return 成功返回true,否则返回false
*/
public Boolean uploadFile(InputStream inputStream, String filename, String path, String host, int port, String username, String password) {
if (!login(host, port, username, password)) {
return false;
}
if (!hasPath(path)) {
logout();
return false;
}
boolean isUploadSuccess = upload(inputStream, filename);
logout();
return isUploadSuccess;
}

/**
* 生成序号
*
* @param no 初始序号
* @return 格式化序号
*/
private String createNo(int no) {
int nextNo = no + 1;
return nextNo < 10 ? "0" + nextNo : String.valueOf(nextNo);
}

/**
* 上传单个文件的逻辑
*
* @param inputStream 文件流
* @param filename 文件名
* @return 返回true表示上传成功
*/
private Boolean upload(InputStream inputStream, String filename) {
String uploadFilename = filename;
try {
uploadFilename = formatFileName(filename);
// 上传文件流
boolean isStoreFileSuccess = ftp.storeFile(uploadFilename, inputStream);
inputStream.close();
if (isStoreFileSuccess) {
LOGGER.info("FTP文件上传成功!文件名:" + uploadFilename);
return true;
}
} catch (IOException e) {
LOGGER.error("FTP文件上传失败!文件名:" + uploadFilename);
}
return false;
}

/**
* 设置FTP属性等
*/
private void settingFtp() {
try {
// 连接超时毫秒
ftp.setConnectTimeout(60000);
// 启动FTP服务器被动模式(被动接受上传)
ftp.enterLocalPassiveMode();
// 文件类型二进制流(默认ASCII)
ftp.setFileType(FTP.BINARY_FILE_TYPE);
// 编码
ftp.setControlEncoding("UTF-8");

// 设置linux环境
FTPClientConfig conf = new FTPClientConfig(FTPClientConfig.SYST_UNIX);
ftp.configure(conf);
} catch (IOException e) {
LOGGER.error("设置FTP属性失败");
}
}

/**
* 退出FTP
*/
private void logout() {
try {
// 退出FTP
ftp.logout();
} catch (Exception e) {
LOGGER.error("关闭FTP服务器连接失败!", e);
} finally {
if (ftp.isConnected()) {
try {
ftp.disconnect();
} catch (IOException ioe) {
LOGGER.error("关闭连接失败!", ioe);
}
}
}
}

/**
* FTP路径是否存在
*
* @param path 路径
* @return true则存在
*/
private Boolean hasPath(String path) {
try {
// 切换到目录下
boolean hasPath = ftp.changeWorkingDirectory(path);
if (hasPath) {
return true;
}
} catch (IOException e) {
LOGGER.error("访问FTP目录路径失败", e);
}
return false;
}

/**
* 判断路径下是否已存该文件名,如已存在,则启用文件名重传标识
*
* @param originFilename 原始文件名
* @return 带重传标志的文件名
*/
private String formatFileName(String originFilename) {
String fileName = originFilename;

String[] fileNameWithExtension = originFilename.split("\\.");
String originName = fileNameWithExtension[0];
try {
// 检索某个文件名是否存在
InputStream basicFile = ftp.retrieveFileStream(originFilename);
boolean isExisted = !(basicFile == null || ftp.getReplyCode() == FTPReply.FILE_UNAVAILABLE);
if (isExisted) {
// 调用有返回流方法时,需要调用completePendingCommad
ftp.completePendingCommand();
List<Integer> reUploadNoList = new ArrayList<>();
// 列举目录下文件名
FTPFile[] ftpFiles = ftp.listFiles();

for (FTPFile file : ftpFiles) {
String ftpName = file.getName().split("\\.")[0];
String[] ftpNameRList = ftpName.split(fileNameReset);
String ftpNameBasic = ftpNameRList[0];

if (originName.equals(ftpNameBasic)) {
reUploadNoList.add(ftpNameRList.length > 1 ? Integer.parseInt(ftpNameRList[1]) : 0);
}
}

Integer no = Collections.max(reUploadNoList);
String nextNoStr = createNo(no);

fileName = originName + fileNameReset + nextNoStr + fileNameExtension;
}
} catch (IOException e) {
LOGGER.error("获取FTP目录下文件列表失败", e);
}
return fileName;
}
}

参考

Java FTPClient实现文件上传下载

https://blog.csdn.net/qq_36248731/article/details/91038014

JAVA FTPClient FTP简单操作

https://www.jianshu.com/p/453829127487

-------------Keep It Simple Stupid-------------
0%