这里只讨论一下代码中需要注意的部分。
4种文件类型
一、ASCII
也就是文本为字符形式,以换行符为例,在Unix下是/n,Windows下是/r/n,Mac下是/r,ASCII模式下,文件会被处理成当前系统兼容的数据格式,数据内容挥发生一定的变化。
二、EBCDIC
这类文件也是字符形式,只不过字符来自IBM的EBCDIC字符集;
三、二进制文件(Binary)
文件传输之后,不会发生任何变化,用于传输图片、压缩包等文件;
四、本地数据
大部分的计算机,都是8位1个字节,本地数据的特点就是,一个字节长度不是由8个比特组成,某些特殊操作系统就有这种特性,接收方根据逻辑字节大小进行和本机的存储特点进行转换。
传输模式(主动模式和被动模式)
FTP的工作方式分为,主动模式和被动模式,主动和被动是针对服务端说的,
主动模式,服务端通过20端口,主动访问客户端的端口,然后开始传输数据;
被动模式,服务端开放一个端口,把端口告诉客户端,由客户端发起连接,被动地接受客户端的访问。
在主动模式情况下,因为是服务端主动访问客户端,能否成功建立数据连接,取决于客户端的配置。
这个过程有非常大的概率,被客户端的防火墙拦截;如果客户端没有公网IP,根本无法建立连接。
以下内容来自于百度:
21端口用于认证,20端口用于传输数据;
FTP客户端随机开启一个大于1024的端口N,向服务端的21号端口发起连接,然后开放N+1号端口进行监听,并向服务器发出PORT 命令,将开放的N+1端口告诉服务端。
服务端接收到命令后,会用其本地的数据端口(20端口),连接客户端指定的端口N+1,进行数据传输。
FTP客户端随机开启一个大于1024的端口N,向服务端的21号端口发起连接,然后开放N+1号端口进行监听,向服务器发送PASV命令,通知服务器自己处于被动模式。
服务器收到命令后,开放一个大于1024的端口P进行监听,然后用PORT命令通知客户端,自己的数据端口是P。
客户端收到命令后,会通过N+1号端口连接服务器的端口P,然后在两个端口之间进行数据传输。
源码参考
Maven依赖
<dependency> <groupId>commons-net</groupId> <artifactId>commons-net</artifactId> <version>3.6</version> </dependency> <!--apache连接池--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> <version>2.8.0</version> </dependency>
FTP常见的参数配置
import org.apache.commons.net.ftp.FTP; import org.apache.commons.pool2.impl.GenericObjectPoolConfig; /** * FTP请求参数配置。 * 虽然在平时的使用,ftp与http非常像,但是在代码中,有着非常大的区别, * 设计上,与JDBC更加类似,每个连接都带有域名、账号、密码等参数。 * 一个ftp连接池,只管理1个服务器的连接,如果同时管理多个ftp服务器,需要配置多个{@link FtpProperties}。 * * @author Mr.css * @date 2021-02-20 11:02 */ public class FtpProperties { /** * ftp地址 */ private String host; /** * 端口号,默认21 */ private Integer port = 21; /** * 登录用户 */ private String username; /** * 登录密码 */ private String password; /** * 是否使用被动模式。 * 主动模式下,默认情况下有非常大的概率会被客户端的防火墙拦截,并且会因为没有公网IP导致无法连接。 */ private boolean passiveMode = true; /** * 连接超时时间(秒) */ private Integer connectTimeout; /** * 连接池容量 */ private int maxTotal = GenericObjectPoolConfig.DEFAULT_MAX_TOTAL; /** * 字符编码,主要解决中文文件名乱码问题 */ private String encoding; /** * 缓存大小 * * @see new BufferedInputStream(inputStream, __bufferSize) */ private Integer bufferSize = 4096; /** * ASCII模式下,文件会被处理成当前系统兼容的数据格式,数据内容挥发生一定的变化, * BINARY模式,可以保证数据不会发生变化 */ private Integer transferFileType = FTP.ASCII_FILE_TYPE;}
连接池
import cn.seaboot.common.exception.ServiceException; import org.apache.commons.net.ftp.FTPClient; import org.apache.commons.pool2.BasePooledObjectFactory; import org.apache.commons.pool2.impl.GenericObjectPool; import org.apache.commons.pool2.impl.GenericObjectPoolConfig; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * FTP连接池 * * @author Mr.css * @date 2021-03-12 9:30 */ public class FtpClientPool implements AutoCloseable { private Logger logger = LoggerFactory.getLogger(FtpService.class); private GenericObjectPool<FTPClient> ftpClientPool; /** * constructor * * @param factory 连接池工厂 * @param config 连接池配置 */ public FtpClientPool(final BasePooledObjectFactory<FTPClient> factory, FtpProperties config) { GenericObjectPoolConfig<FTPClient> objectPoolConfig = new GenericObjectPoolConfig<>(); objectPoolConfig.setMaxTotal(config.getMaxTotal()); this.ftpClientPool = new GenericObjectPool<>(factory, objectPoolConfig); } /** * 对外借出一个FtpClient,方法与{@link #returnFtpClient}对应,用完的FtpClient需要归还到连接池中, * * @return - * @throws ServiceException - {@link FtpService#create()} */ public FTPClient borrowFtpClient() { try { return ftpClientPool.borrowObject(); } catch (Exception e) { throw new ServiceException(e); } } /** * 归还一个FtpClient,方法与{@link #borrowFtpClient()}对应,用完的FtpClient需要归还到连接池中 * * @param ftpClient - */ public void returnFtpClient(FTPClient ftpClient) { ftpClientPool.returnObject(ftpClient); } /** * 销毁全部链接 */ @Override public void close() { ftpClientPool.close(); } }
FTPFactory
是否使用连接池,需要考虑实际的业务场景,如果只是将文件下载到本地,可以不使用连接池。
package cn.seaboot.admin.net.manager.ftp; 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 org.apache.commons.pool2.impl.GenericObjectPool; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; /** * 本身是一个FTPFactory,提供了FTPClient的创建、有效性验证等功能,。 * 1个FtpService对应于1个FtpProperties,如果一个项目中维护了很多的ftp服务,则需要创建很多的FtpService。 * * @author Mr.css * @date 2021-02-20 11:04 */ public class FtpService extends BasePooledObjectFactory<FTPClient> implements AutoCloseable { private Logger logger = LoggerFactory.getLogger(FtpService.class); /** * FTP参数 */ private FtpProperties ftpProperties; /** * FTP连接池 */ private FtpClientPool ftpClientPool; public FtpService(FtpProperties config) { this.ftpProperties = config; this.ftpClientPool = new FtpClientPool(this, config); } /** * return ftpClientPool * * @return GenericObjectPool */ public FtpClientPool getFtpClientPool() { return ftpClientPool; } /** * return FtpProperties * * @return FtpProperties */ public FtpProperties getFtpProperties() { return ftpProperties; } /** * 允许将配置存储在数据库或者其它位置,发生变动时,重置FTP连接池配置 * * @param config - */ public void resetFtpProperties(FtpProperties config) { this.close(); this.ftpProperties = config; this.ftpClientPool = new FtpClientPool(this, config); } /** * 验证ftpClient的有效性 * * @param ftpClient - * @return true/false */ public boolean validateFtpClient(FTPClient ftpClient) { try { if (ftpClient.isConnected()) { //Try connect to ftp server, make sure that connect is succeed. ftpClient.printWorkingDirectory(); return ftpClient.sendNoOp(); } } catch (IOException e) { logger.error("Ftp client is failure:", e); } return false; } /** * 对外借出一个FtpClient,方法与{@link #returnFtpClient}对应,用完的FtpClient需要归还到连接池中, * * @return - * @throws Exception - {@link GenericObjectPool#borrowObject()} */ public FTPClient borrowFtpClient() { return ftpClientPool.borrowFtpClient(); } /** * 归还一个FtpClient,方法与{@link #borrowFtpClient()}对应,用完的FtpClient需要归还到连接池中 * * @param ftpClient - */ public void returnFtpClient(FTPClient ftpClient) { ftpClientPool.returnFtpClient(ftpClient); } /** * 创建FtpClient对象,与{@link #destroyFtpClient(FTPClient)}对应,用完需要自己销毁,不受连接池管理 */ @Override public FTPClient create() throws IOException { FTPClient ftpClient = new FTPClient(); ftpClient.setConnectTimeout(ftpProperties.getConnectTimeout()); ftpClient.connect(ftpProperties.getHost(), ftpProperties.getPort()); ftpClient.setBufferSize(ftpProperties.getBufferSize()); ftpClient.setFileType(ftpProperties.getTransferFileType()); //确定是否连接成功 logger.debug("Receive a ftp connection: " + ftpClient); int replyCode = ftpClient.getReplyCode(); if (!FTPReply.isPositiveCompletion(replyCode)) { ftpClient.disconnect(); logger.warn("FTPServer refused connection, replyCode:{}", replyCode); return null; } //登录 if (ftpProperties.getPassword() != null) { if (!ftpClient.login(ftpProperties.getUsername(), ftpProperties.getPassword())) { logger.error("FTPServer login failed, username is {}; password: {}", ftpProperties.getUsername(), ftpProperties.getPassword()); } } //确定字符编码,主要解决中文文件名乱码问题 String encoding = ftpProperties.getEncoding(); if (encoding == null) { if (FTPReply.isPositiveCompletion(ftpClient.sendCommand("OPTS UTF8", "ON"))) { encoding = "UTF-8"; } else { encoding = "GBK"; } } ftpClient.setControlEncoding(encoding); //确定传输模式 if (ftpProperties.isPassiveMode()) { ftpClient.enterLocalPassiveMode(); } return ftpClient; } /** * 尝试退出登录之后,销毁一个连接 * * @param ftpClient - */ public void destroyFtpClient(FTPClient ftpClient) { logger.debug("Destroy a connection :" + ftpClient); try { if (ftpClient.isConnected()) { ftpClient.logout(); } } catch (IOException io) { logger.error("Ftp client logout failed: ", io); } finally { try { ftpClient.disconnect(); } catch (IOException io) { logger.error("Close ftp client failed: ", io); } } } /** * 用PooledObject封装对象放入池中 */ @Override public PooledObject<FTPClient> wrap(FTPClient ftpClient) { return new DefaultPooledObject<>(ftpClient); } /** * 销毁FtpClient对象 */ @Override public void destroyObject(PooledObject<FTPClient> ftpPooled) { if (ftpPooled != null) { this.destroyFtpClient(ftpPooled.getObject()); } } /** * 验证FtpClient有效性 */ @Override public boolean validateObject(PooledObject<FTPClient> ftpPooled) { if (ftpPooled != null) { return this.validateFtpClient(ftpPooled.getObject()); } return false; } @Override public void close() { ftpClientPool.close(); } }