ftp客户端池(支持多ftp源以及连接保活)
hello 各位小伙伴,大家好,我是毕业1年,即拥有三年CRUD经验的爱抄中间代码路人丙,今天想跟大家分享一下基于工作中遇到的一个需求来实现的 ftp客户端池:
看完本章你将会收获以下技能点
- 池化思想 深入骨髓
- 设计模式 信手拈来
- ftp连接池(支持多源配置:代码免费下载,开箱即用)-- 代码目前还没有时间工程化放到gitee仓库,以下代码是笔者从内网贴出来的,也基本可用
ftp客户端池(支持多ftp源以及连接保活)
具体业务需求:就是将多个ftp服务器上的数百T(公司的数据资产)文件做一个整理且入库(整理存到表里)
前言 :笔者虽然之前在自己开源的轻量级rpc的消费者端实现过长连接池,但是也先上gitee、csdn上看了一下别人怎么去实现,不看不知道,一看吓一跳,特别是那种付费的文章,小伙伴们一定差亮眼睛,反正希望大家在看别人的代码和文章的时候,一定要取其精华,去其糟粕(总而言之,就算是开源的项目,也是会有bug和问题的,即使是别人文章中提到自己经过生产验证的,也一定要理性的抄,因为说不定你抄的代码很坑很坑),同理大家在看笔者的文章的时候,也一定要理性看待(因为笔者可能也会埋很多很多坑)
笔者也非常喜欢抄代码,常常抄一些开源项目的代码(巨人的肩膀上,效率更高),所有如果大家感兴趣,可以在评论区留言,笔者也可以整理一下哪些超过的代码给大家分享!!!
1、为什么要做ftp客户端池呢?
笔者写这个主要有以下几个原因:
- 工作中因功能需求引出的非功能需求(工作中用的到,能供产生结果)
- 锻炼自己对公共模型抽取以及公共功能的下层能力(service mesh不就是这样么,其实就是锻炼自己的设计能力)
- 看了几篇相关的文章,感觉整体文章质量不高,不想让后来的小伙伴们再被割韭菜了(笔者 不割韭菜,如果觉得对你有帮助,帮我到gitee仓库和文章点个赞和star即可,也算是这篇文章的报酬了(因为下次出去面试的时候就可以吹牛了))
其实最主要的原因还是业务场景的非功能需求需要:
大家可以想象一下,一个ftp客户端连接去操作几百t的文件,这不得搞到天荒地老去,你这单线程搞上去,你领导不得骂死你(不可描述),所以搞一个ftp客户端连接池配合多线程批处理去搞是非常必要的非功能需求
2、怎么想到要做ftp客户端池呢?
- 性能利斧:池化思想
其实最主要的来源,是笔者很早之前看了jdk8的线程池的设计,本身线程池就是基于池化思想设计的产物,除了线程池之外其实很多其他的地方我们都能看到池化思想的设计:jredis连接池、mysql连接池、disruptor环形队列对象池、netty对象池以及各种开源rpc(fegin、dobbo、joyrpc(京东的)、sofarpc(蚂蚁的))的客户端连接池,其实不难发现,他们的应用场景都有一个共性:他们都是利用池化思想在解决有限资源的利用(复用)问题
通过这些开源的应用共性,池化主要解决2个问题:
- 解决资源重复创建以及销毁带来资源浪费和性能开销
- 解决资源无限扩张的极端情况
相信到这里,小伙伴也知道池化思想典型的应用场景了吧,如果你在需求上遇到了上面2个问题,都可以采用池化的思想去设计和实现
3、具体怎么实现呢?
笔者跟大家分享一个题外话:抄代码,抄代码,还是抄代码!
大家不要对抄代码有误解哈(我们不是常常会听到一些清华北大的同学总说自己的成绩是抄出来的么!虽然笔者是一个普通人,但我认为这句绝对是真的(虽然也有一点傲娇的凡尔赛在里面,但谁叫人家的结果确实上了清华北大))
具体怎么抄呢?如果有小伙伴感兴趣,可以在评论区留言!看看有多少小伙伴是不会抄代码的!
至于为什么要抄代码呢?因为抄可以提高我们的工作效率同时在一定程度上保证我们的代码质量(这样我们就有更多的时间去了解更多的开源代码,然后继续抄,这不就不断的正向循环,直到我们进入不需要抄代码的阶段,笔者也不知道要抄到什么时候)
抄代码的本质是因为:我们入行不久,各个维度都没有形成自己的知识体系,所以我们需要不断的攀登巨人的肩膀,然后借助他们的力量去丰富自己的知识体系,其实本质上也是学习的一个过程
笔者是用Java代码实现的
代码工程截图:
3.1 maven依赖引入
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<commons-compress.version>1.18</commons-compress.version>
<hutool.version>5.8.17</hutool.version>
<commons-net-version>3.6</commons-net-version>
</properties>
<dependencies>
<dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>
<version>${commons-net-version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<!-- 对象池 <!– https://mvnrepository.com/artifact/org.apache.commons/commons-pool2 –>-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>${commons-pool}</version>
</dependency>
</dependencies>
3.2 3个核心类
代码不多,一个接口,三个类
- 配置类:ftpClient相关的配置
package com.code.copy.love.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @title: FtpProperties
* @Description: 单ftp配置
* @Author Lmm
* @Date: 2024/3/28 14:09
* @Version 1.0
*/
@Data
@ConfigurationProperties(prefix = FtpProperties.PREFIX) // 不一定需要依赖spring,这个注解可要可不要,大家根据实际需求来
public class FtpProperties {
public static final String PREFIX = "code.love.ftp";
/**
* Ip
*/
private String host;
/**
* 端口
*/
private Integer port;
/**
* 账号
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 访问前缀
*/
private String urlPrefix;
/**
* 是否被动模式
* 主动模式
* 被动模式
*/
private boolean passiveMode;
/**
* 编码格式
*/
private String encoding = "UTF-8";
/**
* 连接超时时间
*/
private int connectTimeout = 10 * 1000;
/**
* 缓存
* 8m
*/
private int bufferSize = 8 * 1024;
/**
*
*/
private boolean useEPSVwithIPv4;
/**
*
*/
private boolean remoteVerificationEnabled = true;
/**
* 连接池最大连接数
*/
private int maxClientLimit = 8;
/**
* 连接保活时间
* 单位 分钟
*/
private int controlKeepAliveTimeout = 5;
}
- ftp连接生成工厂类🖤主要去实现池化对象具体的生成
package com.code.copy.love.ftp;
import cn.hutool.core.util.StrUtil;
import com.code.copy.love.Exception.LoveCodeFtpException;
import com.code.copy.love.properties.FtpProperties;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPReply;
import org.apache.commons.pool2.BasePooledObjectFactory;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;
import java.io.IOException;
import java.time.Duration;
/**
* @title: FtpClientFactory
* @Description: ftp客户端工厂
* @Author Lmm
* @Date: 2024/3/28 14:20
* @Version 1.0
*/
@Slf4j
@Data
public class FtpClientFactory extends BasePooledObjectFactory<FTPClient> {
private final FtpProperties ftpProperties;
@Override
public void activateObject(PooledObject<FTPClient> p) throws Exception {
super.activateObject(p);
}
// 验证对象可用否
@Override
public boolean validateObject(PooledObject<FTPClient> p) {
FTPClient ftpClient = p.getObject();
boolean connect = false;
try {
connect = ftpClient.sendNoOp(); // 保证控制连接的 keepalive
} catch (IOException e) {
e.printStackTrace();
}
return connect;
}
public FtpClientFactory(FtpProperties ftpProperties) {
this.ftpProperties = ftpProperties;
}
// 生成方法
@Override
public FTPClient create(){
final FTPClient ftpClient = new FTPClient();
ftpClient.setControlEncoding(ftpProperties.getEncoding());
ftpClient.setConnectTimeout(ftpProperties.getConnectTimeout());
ftpClient.setControlKeepAliveTimeout(Duration.ofMinutes(ftpProperties.getControlKeepAliveTimeout()));// 设置控制连接的保活时间
try {
// 连接ftp服务器
ftpClient.connect(ftpProperties.getHost(), ftpProperties.getPort());
// 登录ftp服务器
ftpClient.login(ftpProperties.getUsername(), ftpProperties.getPassword());
} catch (IOException e) {
throw new LoveCodeFtpException("创建ftp连接失败", e);
}
// 是否成功登录服务器
final int replyCode = ftpClient.getReplyCode();
if (!FTPReply.isPositiveCompletion(replyCode)) {
try {
ftpClient.disconnect();
} catch (IOException e) {
throw new LoveCodeFtpException(StrUtil.format("Login failed for user [{}] ,host [{}], reply code is: [{}]",
ftpProperties.getUsername(),ftpProperties.getHost(), replyCode));
}
}
ftpClient.setBufferSize(ftpProperties.getBufferSize());
//设置模式
if (ftpProperties.isPassiveMode()) {
ftpClient.enterLocalPassiveMode();
}
if (ftpProperties.isUseEPSVwithIPv4()){
ftpClient.setUseEPSVwithIPv4(true);
}
if (!ftpProperties.isRemoteVerificationEnabled()){
ftpClient.setRemoteVerificationEnabled(false);
}
return ftpClient;
}
// 包装方法
@Override
public PooledObject<FTPClient> wrap(FTPClient ftpClient) {
return new DefaultPooledObject<>(ftpClient);
}
// 销毁方法
@Override
public void destroyObject(PooledObject<FTPClient> ftpPooled){
if (ftpPooled == null) {
return;
}
FTPClient ftpClient = ftpPooled.getObject();
try {
if (ftpClient.isConnected()) {
ftpClient.logout();
}
} catch (IOException io) {
log.error("ftp client logout failed...", io);
} finally {
try {
ftpClient.disconnect();
} catch (IOException io) {
log.error("close ftp client failed...", io);
}
}
}
}
- FtpClientPool:连接池类
package com.code.copy.love.ftp;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
/**
* @title: FtpClientPool
* @Description: ftp客户端池
* @Author Lmm
* @Date: 2024/3/28 14:42
* @Version 1.0
*/
@Slf4j
@Data
public class FtpClientPool{
/**
* ftp客户端连接池
*/
private final GenericObjectPool<FTPClient> ftpClientPool;
private final FtpClientFactory ftpClientFactory;
private final String flag;
/**
* @param ftpClientFactory ftp工厂
*/
public FtpClientPool(FtpClientFactory ftpClientFactory,String flag){
this(ftpClientFactory.getFtpProperties().getMaxClientLimit(), ftpClientFactory,flag);
}
public FtpClientPool(int poolSize, FtpClientFactory factory,String flag) {
this.ftpClientFactory = factory;
GenericObjectPoolConfig<FTPClient> poolConfig = initPoolConfig(poolSize);
this.ftpClientPool = new GenericObjectPool<>(factory,poolConfig);
this.flag = flag;
}
public FtpClientPool(FtpClientFactory factory,GenericObjectPoolConfig<FTPClient> poolConfig,String flag) {
this.flag = flag;
this.ftpClientFactory = factory;
this.ftpClientPool = new GenericObjectPool<>(factory,poolConfig);
}
private GenericObjectPoolConfig<FTPClient> initPoolConfig(int poolSize) {
GenericObjectPoolConfig<FTPClient> poolConfig = new GenericObjectPoolConfig<>();
poolConfig.setMaxTotal(poolSize);
return poolConfig;
}
/**
* 获取连接
*
* @return FTPClient
*/
public FTPClient borrowObject() throws Exception {
return ftpClientPool.borrowObject();
}
/**
*
* 归还连接
**/
public void returnObject(FTPClient client) {
if (null != client)
ftpClientPool.returnObject(client);
}
}
- 多ftp连接池接口定义
这里为什么要设计一个接口呢,大家可以想一下!
笔者在这里设计接口的原因是因为:有些业务场景的ftp服务器的配置是存在数据库里的,有的比较少直接在配置文件中配好,所以笔者定义了一个接口,大家可以根据实际的情况去实现多ftp服务器的配置加载
package com.code.copy.love.ftp;
/**
*
* 多ftp服务器池 接口定义:方便大家扩展
*/
public interface MultiFtpClientPoolFun<T> {
/**
* 获取ftp客户端池
*/
T getPool(String flag);
/**
* 初始化多数据源 ftp池
*/
void initPool(String flag,T clientPool);
}
- 多ftp服务器连接池类:负责多源ftp的连接池,笔者的场景没有用到多ftp服务器情况,但是在搜索文章的时候,看见评论有人问,所以也基于自己的想法实现了一下,写法有点像策略模式,其实这段代码还可以加一个模板设计模式,我们再加一个抽象类,把getPool()定义为抽象方法,因为不同的业务场景获取池实列的算法策略可能不一样(笔者的策略已经在代码中注释了),这样的话,扩展性会更好,就算业务需求变更对getPool的要求变了,我们代码的变动也不会太多,继承模板抽象类,重写getPool即可
package com.code.copy.love.ftp;
import cn.hutool.core.util.StrUtil;
import com.code.copy.love.Exception.LoveCodeFtpException;
import java.util.Arrays;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
/**
* @title: MutilFtpClientPool
* @Description: 多ftp服务器池
* @Author Lmm
* @Date: 2024/3/29 9:15
* @Version 1.0
*/
public class DefaultMultiFtpClientPool implements MultiFtpClientPoolFun<FtpClientPool> {
private final Map<String,FtpClientPool> multiPool = new ConcurrentHashMap<>(16);
/**
* 不同的ftp服务器客户端池 在构造的时候可以调用该方法把自己放进来
*/
public void initPool(String flag,FtpClientPool clientPool){
if (StrUtil.isBlank(flag))
throw new LoveCodeFtpException("param :flag must not be '' or null");
if (null == clientPool)
throw new LoveCodeFtpException("param :clientPool must not be null");
multiPool.putIfAbsent(flag, clientPool);
}
/**
* @param flag {@link #initPool(String, FtpClientPool)}方法传进来的flag
* 获取客户端池
* (1)如果给了flag标识,优先返回对应池
* 如果对应池不存在,则随机返回一个池
* (2)如果flag标识为null,则则随机返回一个池
*/
public FtpClientPool getPool(String flag){
if (StrUtil.isBlank(flag)){
return getRandomClientPool();
}
if (multiPool.containsKey(flag))
return multiPool.get(flag);
return getRandomClientPool();
}
/**
* 随机获取一个ftp 客户端池
*/
private FtpClientPool getRandomClientPool() {
Object[] multiPoolArray = Arrays.stream(multiPool.values().toArray()).toArray();
Random random = new Random();
int next = random.nextInt(multiPoolArray.length);
return (FtpClientPool)multiPoolArray[next];
}
}
- 测试类(笔者这里偷懒,直接搞了一个man方法测试)
package com.code.copy.love;
import com.code.copy.love.ftp.DefaultMultiFtpClientPool;
import com.code.copy.love.ftp.FtpClientFactory;
import com.code.copy.love.ftp.FtpClientPool;
import com.code.copy.love.ftp.MultiFtpClientPoolFun;
import com.code.copy.love.properties.FtpProperties;
import org.apache.commons.net.ftp.FTPClient;
/**
* @title: Main
* @Description: 测试类
* @Author Lmm
* @Date: 2024/3/29 14:15
* @Version 1.0
*/
public class Main {
public static void main(String[] args) throws Exception{
// 笔者这里直接用main测试了
// (1)准备ftp配置
FtpProperties ftpProperties = getFtpProperties();
// (2)构造FtpClientFactory对象
FtpClientFactory ftpClientFactory = new FtpClientFactory(ftpProperties);
// (3)构造池对象
String flag = ftpProperties.getHost() + ":"+ ftpProperties.getPort();
FtpClientPool ftpClientPool = new FtpClientPool(ftpClientFactory,flag);
// 如果只是单池的话 ftpClientPool可以直接用了
// FTPClient ftpClient = ftpClientPool.borrowObject(); // 池获取连接
// ftpClientPool.returnObject(ftpClient); // 用完还连接
// (4)多ftp连接池构造
MultiFtpClientPoolFun<FtpClientPool> multiFtpClientPoolFun = new DefaultMultiFtpClientPool();
multiFtpClientPoolFun.initPool(flag,ftpClientPool);
// (5) 多ftp源获取连接 这个flag可以根据自己业务需求来定义
FtpClientPool pool = multiFtpClientPoolFun.getPool(flag);
FTPClient ftpClient = pool.borrowObject();
pool.returnObject(ftpClient);
}
/**
* 设置ftp相关的配置
* todo 大家代码拉下来就可以测试
* */
private static FtpProperties getFtpProperties() {
FtpProperties ftpProperties = new FtpProperties();
// todo 配置自己的ftp相关配置
return ftpProperties;
}
}
4、埋下伏笔:笔者的疑问?-关于commons-pool2中的BasePooledObjectFactory的create()方法在GenericObjectPool类中是否是线程安全的使用?
笔者的疑问:
关于commons-pool2中的BasePooledObjectFactory的create()方法在GenericObjectPool类中是否是线程安全的使用?
为什么笔者有这个疑问呢?有2个原因:
(1)笔者没看过GenericObjectPool的源码,所以不确定BasePooledObjectFactory的create()方法是不是线程安全的使用
(2)BasePooledObjectFactory的create()方法主要作用是创建连接,这个地方是很有可能产生并发的(比如池中一个连接都没有的时候)
所以基于以上原因,这里笔者埋个伏笔,空闲的时候抽时间去研究一下commons-pool2中关于GenericObjectPool类池化设计的源码以及思想,看看有不有什么优秀的代码可以抄以及确定一下是不是线程安全的(笔者猜测应该是线程安全的)