一、背景

  我在之前的文章(Java实现文件上传和下载)里讲过非FTP文件的上传和下载,今天我们来讲一下FTP文件上传和下载,本文测试过程中Spring Boot 版本为2.5.2commons-net 版本为3.8.0,JDK环境为 1.8。本文是在window环境下完成的,因为本机环境的复杂性,是把本机的防火墙关闭了的(不然dos登录后操作不了,或者上传下载超时)。

二、maven依赖

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.2</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    
    <groupId>com.alian</groupId>
    <artifactId>ftp</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>ftp</name>
    <description>java实现FTP文件上传下载</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.12.0</version>
        </dependency>

        <dependency>
            <groupId>commons-net</groupId>
            <artifactId>commons-net</artifactId>
            <version>3.8.0</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.14</version>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>compile</scope>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

三、FTP工具类

  本工具类中包含四个方法,实例化方法,上传文件,下载文件,及关闭连接方法。

ApacheFtpClient.java

package com.alian.ftp.utils;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;

import java.io.*;

@Slf4j
public class ApacheFtpClient {

    private FTPClient ftpClient;

    /**
     * 实例化
     *
     * @param hostName FTP服务器地址
     * @param port     FTP服务器端口
     * @param userName FTP登录账户
     * @param password FTP登录密码
     * @throws IOException
     */
    public ApacheFtpClient(String hostName, int port, String userName, String password) throws IOException {
        ftpClient = new FTPClient();
        //设置传输命令的超时
        ftpClient.setDefaultTimeout(20000);//毫秒
        //设置两个服务连接超时时间
        ftpClient.setConnectTimeout(10000);//毫秒
        //被动模式下设置数据传输的超时时间
        ftpClient.setDataTimeout(15000);//毫秒
        //连接FTP
        ftpClient.connect(hostName, port);
        //更加账户密码登录服务
        ftpClient.login(userName, password);
        //被动模式(需要设置在连接之后,尤其linux环境)
        ftpClient.enterLocalPassiveMode();
    }

    /**
     * FTP上传文件
     *
     * @param remoteUploadDirectory   要上传的目录(FTP服务器上的目录)
     * @param localUploadFilePathName 本地上传文件的完整路径(本地路径)
     * @return
     */
    public Pair<Boolean, String> uploadFile(String remoteUploadDirectory, String localUploadFilePathName) {
        FileInputStream fis = null;
        try {
            // 如果不能进入dir下,说明此目录不存在!
            System.out.println("FTP响应码:"+ftpClient.getReplyCode());
            System.out.println("FTP响应信息:"+ftpClient.getReplyString());
            if (!ftpClient.changeWorkingDirectory(remoteUploadDirectory)) {
                log.info("没有目录:{}", remoteUploadDirectory);
                if (!ftpClient.makeDirectory(remoteUploadDirectory)) {
                    log.info("创建文件目录【{}】 失败!", remoteUploadDirectory);
                    return Pair.of(false, "创建文件目录【" + remoteUploadDirectory + "】 失败");
                }
            }
            //进入文件目录
            ftpClient.changeWorkingDirectory(remoteUploadDirectory);
            //创建文件流
            fis = new FileInputStream(new File(localUploadFilePathName));
            //设置上传目录
            ftpClient.setBufferSize(1024);
            //设置文件类型(二进制)
            ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE);
            //FTP服务器上最终的名字
            String uploadFileName = localUploadFilePathName.substring(localUploadFilePathName.lastIndexOf(File.separator) + 1);
            //文件上传
            boolean b = ftpClient.storeFile(uploadFileName, fis);
            int replyCode = ftpClient.getReplyCode();
            log.info("上传文件响应码:{}", replyCode);
            log.info("上传文件响应信息:{}", ftpClient.getReplyString());
            return Pair.of(b, b ? "上传成功" : "上传失败");
        } catch (Exception e) {
            log.error("FTP上传文件异常!:", e);
            return Pair.of(false, "上传文件异常");
        } finally {
            try {
                if (fis != null) {
                    fis.close();
                }
            } catch (IOException e) {
                log.error("关闭流发生异常!", e);
            }
        }
    }

    /**
     * FTP文件下载
     *
     * @param remoteDownloadDirectory 要下载的目录(FTP服务器目录)
     * @param localDirectory          本地下载文件路径
     * @param downloadFileName        下载的文件名
     * @return
     */
    public Pair<Boolean, String> downloadFile(String remoteDownloadDirectory, String localDirectory, String downloadFileName) {
        OutputStream out = null;
        try {
            if (StringUtils.isBlank(downloadFileName)) {
                return Pair.of(false, "要下载的文件不能为空");
            }
            //工作目录切换到下载文件的目录下
            if (!ftpClient.changeWorkingDirectory(remoteDownloadDirectory)) {
                log.info("目录不存在:{}", remoteDownloadDirectory);
                return Pair.of(false, "目录不存在");
            }
            //获取目录下所有文件
            FTPFile[] files = ftpClient.listFiles();
            if (files.length < 1) {
                return Pair.of(false, "目录为空");
            }
            boolean fileExist = false;
            boolean downloadFlag = false;
            //遍历文件列表
            for (FTPFile ftpFile : files) {
                String localFile = localDirectory + File.separator + downloadFileName;
                //是否存在要下载的文件
                if (downloadFileName.equals(ftpFile.getName())) {
                    fileExist = true;
                    out = new FileOutputStream(localFile);
                    //下载
                    downloadFlag = ftpClient.retrieveFile(downloadFileName, out);
                    int replyCode = ftpClient.getReplyCode();
                    log.info("下载文件响应码:{}", replyCode);
                    break;
                }
            }
            if (!fileExist) {
                return Pair.of(false, "FTP服务器上文件不存在");
            }
            return Pair.of(downloadFlag, downloadFlag ? "下载成功" : "下载失败");
        } catch (Exception e) {
            log.error("FTP下载文件异常!:", e);
            return Pair.of(false, "下载文件异常");
        } finally {
            try {
                if (out != null) {
                    out.flush();
                    out.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public void close() {
        try {
            if (ftpClient != null && ftpClient.isConnected()) {
                ftpClient.disconnect();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

  注意构造方法中的调用,FTP协议有两种工作方式:

	// 被动模式(需要设置在连接之后,尤其linux环境)
    ftpClient.enterLocalPassiveMode();

  如果是windows环境上传下载没啥问题,如果是linux环境,可能端口未开放就需要我们使用这个被动模式,而且是登录之后,不然你调用 listFiles() 方法可能就是返回null了。

  如果你是要设置编码的话,你需要在你连接,登录之前进行设置。不然就是无效,FTP服务器默认的编码是 ISO-8859-1 。所以你上传和下载时,不管是文件目录,或者文件名,或者文件的编码都要进行相应的统一转换,我这里就没有改服务器上编码格式了。

	//被动模式(需要设置在连接之前)
	ftpClient.setControlEncoding("UTF-8");

3.1、主动模式(PORT)

  主动模式的FTP是指服务器主动连接客户端的数据端口。

  • FTP客户端随机开启一个大于1024的端口N向服务器的21号端口发起连接,同时开放N+1号端口进行监听,然后发送PORT命令到FTP服务器,告知是主动模式
  • FTP服务器给客户端的命令端口返回一个ACK的应答码
  • FTP客户端发出数据传输的指令
  • FTP服务器发起一个从它自己的数据端口(一般是20端口)到客户端先前指定的数据端口(N+1)的连接
  • FTP客户端返回一个ACK后,进行数据传输

3.2、被动模式(PASV)

  被动模式的FTP是指服务器被动地等待客户端连接自己的数据端口。

  • FTP客户端随机开启一个大于1024的端口N向服务器的21号端口发起连接,同时会开启N+1号端口。然后向服务器发送PASV命令,告知是被动模式
  • FTP服务器收到命令后,会开放一个大于1024的端口P进行监听,然后用PORT命令通知客户端,服务端的数据端口是P
  • FTP客户端发出数据传输的指令,会通过N+1号数据端口连接服务器的端口P
  • FTP服务器给客户端的数据端口返回一个ACK的应答码,然后在两个端口之间进行数据传输

四、验证

4.1、dos下操作FTP

  如果你是window环境下操作FTP,建议关闭防火墙操作。
输入命令:

#在cmd命令提示符下输入(ftp加FTP服务器地址)
ftp 192.168.0.151
#根据提示输入用户名
Alian
#根据提示输入登录密码
Alian@1223

本文登录后的文件列表:
在这里插入图片描述

4.2、FTP文件上传

    @Test
    public void upload() throws IOException {
        ApacheFtpClient apacheFtpClient = new ApacheFtpClient("192.168.0.151", 21, "Alian", "Alian@1223");
        Pair<Boolean, String> pair = apacheFtpClient.uploadFile("apacheFTP", "C:\\myFile\\CSDN\\result.png",);
        log.info("上传返回结果:{}", pair);
        apacheFtpClient.close();
    }

运行结果:

17:16:55.830 [main] INFO com.alian.ftp.utils.ApacheFtpClient - 上传文件响应码:226
17:16:55.835 [main] INFO com.alian.ftp.service.TestApacheFtpService - 上传返回结果:(true,上传成功)

登录FTP服务器上查看文件如下:
在这里插入图片描述

4.3、FTP文件下载

    @Test
    public void download() throws IOException {
        ApacheFtpClient apacheFtpClient = new ApacheFtpClient("192.168.0.151", 21, "Alian", "Alian@1223");
        Pair<Boolean, String> pair = apacheFtpClient.downloadFile("apacheFTP", "C:\\myFile\\download", "result.png");
        log.info("下载返回结果:{}", pair);
        apacheFtpClient.close();
    }

运行结果:

17:20:37.126 [main] INFO com.alian.ftp.utils.ApacheFtpClient - 下载文件响应码:226
17:20:37.130 [main] INFO com.alian.ftp.service.TestApacheFtpService - 下载返回结果:(true,下载成功)

进入到我们本地下载的目录,结果如下:
在这里插入图片描述

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐