常见的文件传输协议—— FTP

这里只讨论一下代码中需要注意的部分。

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();
    }
}
做什么都好,不要什么都不做
原文地址:https://www.cnblogs.com/chenss15060100790/p/14673734.html