文章目录
最近项目上需要使用ftp服务器和第三方进行资源交互,于是写了个小demo记录下~
基础知识
FTP服务器
FTP(File Transfer Protocol)即文件传输协议,是一种基于TCP的协议,采用客户/服务器模式。通过FTP协议,用户可以在FTP服务器中进行文件的上传或下载等操作。虽然现在通过HTTP协议下载的站点有很多,但是由于FTP协议可以很好地控制用户数量和宽带的分配,快速方便地上传、下载文件,因此FTP已成为网络中文件上传和下载的首选服务器。同时,它也是一个应用程序,用户可以通过它把自己的计算机与世界各地所有运行FTP协议的服务器相连,访问服务器上的大量程序和信息。FTP服务的功能是实现完整文件的异地传输。(来自百度百科)
多级目录下创建文件
一直以为ftp服务器多级目录创建和java中目录创建一样,然而并不是滴~ 可参考文档:关于ftp上传changeWorkingDirectory()方法的路径切换问题
将文件hello.json
上传至/home/upload/
目录下步骤:
- 先使用
FTPClient.changeWorkingDirectory("home")
,判断home目录是否存在?若不存在则2 - 使用
FTPClient.makeDirectory("home")
创建目录,同时使用FTPClient.changeWorkingDirectory("home")
将ftp session指向home目录 - 使用
FTPClient.changeWorkingDirectory("upload")
判断upload目录是否存在?若不存在则4 - 使用
FTPClient.makeDirectory("upload")
创建目录,同时使用FTPClient.changeWorkingDirectory("upload")
将ftp session指向upload目录 - 使用
FTPClient.storeFile(fileName, is);
上传文件流is
FTP传输模式
对比方面 | 主动模式 | 被动模式 |
---|---|---|
客户端 | 随机开放一个端口(1024以上) | - |
服务端 | - | 在本地随机开放一个端口(1024以上) |
实践出真知
引入依赖和加入配置
- pom.xml 中添加依赖
<dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>
<version>3.6</version>
</dependency>
- 在application.yml中添加ftp服务器配置(多环境则需要在application-dev/test/prod.yml中分别配置)
ftp:
hostname: 172.16.1.1
port: 21
username: lizzy
password: lizzy
root-path: /upload/
定义配置bean
package com.lizzy.common.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import lombok.Data;
@ConfigurationProperties(prefix = "ftp")
@Data
@Component
public class FtpConfig {
private String hostname;
private Integer port;
private String username;
private String password;
private String rootPath;
}
定义FTP工具类
获取FTPClient,设置超时
private FTPClient getFTPClient() {
FTPClient client = new FTPClient();
// 设置默认超时时间30s
client.setDefaultTimeout(30000);
// 设置链接超时时间
client.setConnectTimeout(30000);
// 设置数据传输时间
client.setDataTimeout(30000);
return client;
}
上传
/**
* 上传文件至FTP服务器
* @param config ftp服务器配置
* @param path 相对目录(不包含根目录)
* @param fileName 文件名
* @param is 文件流
*/
public static void upload(FtpConfig config, String path, String fileName, InputStream is) {
FTPClient client = getFTPClient();
try {
// 连接服务器
client.connect(config.getHostname(), config.getPort());
// 登录
client.login(config.getUsername(), config.getPassword());
int reply = client.getReplyCode();
if (!FTPReply.isPositiveCompletion(reply)) {
// 连接失败
client.disconnect();
log.info("FTP服务器配置链接失败!请检查配置:{}", config.toString());
return ;
}
// 设置被动模式,开通一个端口来传输数据
client.enterLocalPassiveMode();
// 切换到上传目录
final String filePath = config.getRootPath() + path;
if (!client.changeWorkingDirectory(filePath)) {
// 目录不存在则创建
String[] dirs = filePath.split("/");
for (String dir : dirs) {
if (StringUtils.isEmpty(dir)) {
continue ;
}
if (!client.changeWorkingDirectory(dir)) {
client.makeDirectory(dir);
client.changeWorkingDirectory(dir);
}
}
}
// 设置被动模式,开通一个端口来传输数据
client.enterLocalPassiveMode();
//设置上传文件的类型为二进制类型
client.setFileType(FTP.BINARY_FILE_TYPE);
client.storeFile(fileName, is);
log.debug("文件{}成功上传至FTP服务器!", filePath + "/" + fileName);
client.logout();
} catch (IOException e) {
log.info("FTP服务器配置链接失败!错误:{}", e.getMessage());
e.printStackTrace();
} finally {
try {
is.close();
if (client.isConnected()) {
client.disconnect();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
下载
/**
* 从FTP服务器下载文件流
* @param config FTP服务器配置
* @param path 相对目录(不包含根目录)
* @param fileName 文件名
* @return
*/
public static InputStream download(FtpConfig config, String path, String fileName) {
FTPClient client = getFTPClient();
try {
// 连接服务器
client.connect(config.getHostname(), config.getPort());
// 登录
client.login(config.getUsername(), config.getPassword());
int reply = client.getReplyCode();
if (!FTPReply.isPositiveCompletion(reply)) {
// 连接失败
client.disconnect();
log.info("FTP服务器配置链接失败!请检查配置:{}", config.toString());
return null;
}
// 设置被动模式,开通一个端口来传输数据
client.enterLocalPassiveMode();
// 切换到下载目录
final String filePath = config.getRootPath() + path;
client.changeWorkingDirectory(filePath);
log.debug("FTP session指向下载目录:{}", filePath);
//设置上传文件的类型为二进制类型
client.setFileType(FTP.BINARY_FILE_TYPE);
// client.setControlEncoding("utf8");
// 中文名会下载失败,文件名需转码
InputStream is = client.retrieveFileStream(new String(fileName.getBytes("gbk"), "ISO-8859-1"));
if (null == is) {
log.debug("下载文件:{}失败!读取长度为0!", fileName);
return null;
}
InputStream retIs = copyStream(is);
is.close();
client.completePendingCommand();
log.debug("从FTP服务器下载文件({})成功!", fileName);
return retIs;
} catch (IOException e) {
log.info("FTP服务器配置链接失败!错误:{}", e.getMessage());
e.printStackTrace();
} finally {
try {
if (client.isConnected()) {
log.info("从FTP服务器下载文件{}流程结束,关闭链接!", fileName);
client.disconnect();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
复制流方法如下:
private InputStream copyStream(InputStream is) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) > -1 ) {
baos.write(buffer, 0, len);
}
baos.flush();
return new ByteArrayInputStream(baos.toByteArray());
}
遇到的问题
文件覆盖
retrieveFileStream卡死
出错前代码如下:我真是看了半天也不知道为嘛子就失败,于是乎开始打印各个文件的大小,终于发现了一点点端倪,两个不同的文件打印的文件大小一样!看出来了么,简直无语死了,慎用inputstream.available()
方法判断流=_=||
// 中文名会下载失败,文件名需转码
InputStream is = client.retrieveFileStream(new String(fileName.getBytes("gbk"), "ISO-8859-1"));
if (null == is || is.available() < 1) {
log.debug("下载文件:{}失败!读取长度为0!", fileName);
return null;
}
log.debug("结束读取文件:{},大小为:{}", fileName, is.available());
InputStream retIs = copyStream(is);
is.close();
client.completePendingCommand();
判断流操作慎用inputstream.available()
注意事项(采过的坑)
先connect后enterLocalActiveMode
参考文档:Ftpclient调用retrieveFileStream返回null, docker中下载失败问题
connect方法中,将模式设置为主动模式ACTIVE_LOCAL_DATA_CONNECTION_MODE
.
enterLocalActiveMode方法则是将链接模式设置为被动模式
若先调用enterLocalActiveMode再connect,则链接模式还是为主动模式!
获取返回流一定要调用completePendingCommad
参考文档:FTPClient中使用completePendingCommand方法注意事项
编写FTP过程中遇到的问题就是,流程的框框都是固定的,不能多写也不能少写,不然就是一堆的block!
// 上传方法,之后若调用completePendingCommand会卡死
public boolean storeFile(String remote, InputStream local)
// 上传方法,之后必须调用completePendingCommand
public OutputStream storeFileStream(String remote)
// 下载方法,之后若调用completePendingCommand会卡死
public boolean retrieveFile(String remote, OutputStream local)
// 下载方法,之后必须调用completePendingCommand
public InputStream retrieveFileStream(String remote)
先inputStream.close()再completePendingCommad
参考文档:FTPClient中使用completePendingCommand方法时之踩坑
completePendingCommand()会一直在等FTP Server返回226 Transfer complete,但是FTP Server只有在InputStream执行close方法时,才会返回。所以先要执行close方法,再执行completePendingCommand
后记
demo写的很匆忙,后续遇到问题了再更新~