一、背景
业务需要从ftp服务器上上传、下载、删除文件等功能,通过查阅资料及手动敲打代码,实现了操作ftp的基本功能,有需求的小伙伴可以看看具体的实现过程。
二、ftp介绍
摘自百度百科:文件传输协议(File Transfer Protocol,FTP)是用于在网络上进行文件传输的一套标准协议,FTP允许用户以文件操作的方式(如文件的增、删、改、查、传送等)与另一主机相互通信。
三、具体代码实现
1、引入以下依赖
<!--apache下,包含连接ftp服务器的工具--><dependency> <groupId>commons-net</groupId> <artifactId>commons-net</artifactId> <version>3.6</version></dependency>
2、编写一个FTP工具类
含以下四个方法:
*获取一个FtpClinet连接
*关闭FtpClinet连接
*下载文件
*上传文件
import lombok.extern.slf4j.Slf4j;import org.apache.commons.net.ftp.FTP;import org.apache.commons.net.ftp.FTPClient;import org.apache.commons.net.ftp.FTPReply;import java.io.*;import java.nio.charset.StandardCharsets;@Slf4jpublic class FtpUtil { /** * 获取一个ftp连接 * @param host ip地址 * @param port 端口 * @param username 用户名 * @param password 密码 * @return 返回ftp连接对象 * @throws Exception 连接ftp时发生的各种异常 */ public static FTPClient getFtpClient(String host, Integer port, String username, String password) throws Exception{ FTPClient ftpClient = new FTPClient(); // 连接服务器 ftpClient.connect(host, port); int reply = ftpClient.getReplyCode(); if(!FTPReply.isPositiveCompletion(reply)){ log.error("无法连接至ftp服务器, host:{}, port:{}", host, port); ftpClient.disconnect(); return null; } // 登入服务器 boolean login = ftpClient.login(username, password); if(!login){ log.error("登录失败, 用户名或密码错误"); ftpClient.logout(); ftpClient.disconnect(); return null; } // 连接并且成功登陆ftp服务器 log.info("login success ftp server, host:{}, port:{}, user:{}", host, port, username); // 设置通道字符集, 要与服务端设置一致 ftpClient.setControlEncoding("UTF-8"); // 设置文件传输编码类型, 字节传输:BINARY_FILE_TYPE, 文本传输:ASCII_FILE_TYPE, 建议使用BINARY_FILE_TYPE进行文件传输 ftpClient.setFileType(FTP.BINARY_FILE_TYPE); // 动模式: enterLocalActiveMode(),被动模式: enterLocalPassiveMode(),一般选择被动模式 ftpClient.enterLocalPassiveMode(); // 切换目录 //ftpClient.changeWorkingDirectory("xxxx"); return ftpClient; } /** * 断开ftp连接 * @param ftpClient ftp连接客户端 */ public static void disConnect(FTPClient ftpClient){ if(ftpClient == null){ return; } try { log.info("断开ftp连接, host:{}, port:{}", ftpClient.getPassiveHost(), ftpClient.getPassivePort()); ftpClient.logout(); ftpClient.disconnect(); } catch (IOException e) { e.printStackTrace(); log.error("ftp连接断开异常, 请检查"); } } /** * 文件下载 * @param ftpClient ftp连接客户端 * @param path 文件路径 * @param fileName 文件名称 */ public static void download(FTPClient ftpClient, String path, String fileName) throws Exception { if(ftpClient == null || path == null || fileName == null){ return; } // 中文目录处理存在问题, 转化为ftp能够识别中文的字符集 String remotePath; try { remotePath = new String(path.getBytes(StandardCharsets.UTF_8), FTP.DEFAULT_CONTROL_ENCODING); } catch (UnsupportedEncodingException e) { remotePath = path; } InputStream inputStream = ftpClient.retrieveFileStream(remotePath); if (inputStream == null) { log.error("{}在ftp服务器中不存在,请检查", path); return; } FileOutputStream outputStream = new FileOutputStream("D://test//" + fileName); BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream); BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream); try{ byte[] buffer = new byte[2048]; int i; while ((i = bufferedInputStream.read(buffer)) != -1) { bufferedOutputStream.write(buffer, 0, i); bufferedOutputStream.flush(); } } catch (Exception e) { log.error("文件下载异常", e); log.error("{}下载异常,请检查", path); } inputStream.close(); outputStream.close(); bufferedInputStream.close(); bufferedOutputStream.close(); // 关闭流之后必须执行,否则下一个文件导致流为空 boolean complete = ftpClient.completePendingCommand(); if(complete){ log.info("文件{}下载完成", remotePath); }else{ log.error("文件{}下载失败", remotePath); } } /** * 上传文件 * @param ftpClient ftp连接客户端 * @param sourcePath 源地址 */ public static void upload(FTPClient ftpClient, String sourcePath) throws Exception{ if(ftpClient == null || sourcePath == null){ return; } File file = new File(sourcePath); if(!file.exists() || !file.isFile()){ return; } // 中文目录处理存在问题, 转化为ftp能够识别中文的字符集 String remotePath; try { remotePath = new String(file.getName().getBytes(StandardCharsets.UTF_8), FTP.DEFAULT_CONTROL_ENCODING); } catch (UnsupportedEncodingException e) { remotePath = file.getName(); } try( InputStream inputStream = new FileInputStream(file); OutputStream outputStream = ftpClient.storeFileStream(remotePath); ){ byte[] buffer = new byte[2048]; int length; while((length = inputStream.read(buffer)) != -1){ outputStream.write(buffer, 0, length); outputStream.flush(); } }catch (Exception e){ log.error("文件上传异常", e); } // 关闭流之后必须执行,否则下一个文件导致流为空 boolean complete = ftpClient.completePendingCommand(); if(complete){ log.info("文件{}上传完成", remotePath); }else{ log.error("文件{}上传失败", remotePath); } }}
3、测试连接、上传文件、下载文件、关闭连接功能
@Slf4j@RestControllerpublic class FtpController { private static final String FTP_HOST = "your host"; private static final Integer FTP_PORT = 21; private static final String FTP_USERNAME = "your username"; private static final String FTP_PASSWORD = "your password"; @GetMapping("/test") public void test() throws Exception{ FTPClient ftpClient = FtpUtil.getFtpClient(FTP_HOST, FTP_PORT, FTP_USERNAME, FTP_PASSWORD); // 展示文件夹 FTPFile[] ftpFiles = ftpClient.listDirectories(); for(FTPFile file : ftpFiles){ System.out.println(file.getName()); } // 下载文件 FtpUtil.download(ftpClient, "test.txt", "test.txt"); //FtpUtil.download(ftpClient, "test.py", "test.py"); // 上传文件 FtpUtil.upload(ftpClient, "D://test//test.py"); FtpUtil.disConnect(ftpClient); }}
经过测试,能够正常连接上ftp服务器,并且上传下载文件,最后关闭连接。
四、遇到的问题
在使用过程中,遇到以下问题,需要记录一下,以防下次忘记:
就是在创建完一个FtpClient连接之后,关闭连接之前,第一次进行文件上传或下载是正常的,当第二次进行文件上传或下载时,从FtpClient获取的输入流或输出流是空的。需要在每次操作完文件上传或下载时,添加上以下代码片段:
// 关闭流之后必须执行,否则下一个文件导致流为空boolean complete = ftpClient.completePendingCommand();if(complete){ log.info("文件{}下载完成", remotePath);}else{ log.error("文件{}下载失败", remotePath);}
该方法有以下注释:
There are a few FTPClient methods that do not complete the entire sequence of FTP commands to complete a transaction. These commands require some action by the programmer after the reception of a positive intermediate command. After the programmer's code completes its actions, it must call this method to receive the completion reply from the server and verify the success of the entire transaction.
百度翻译大致意思应该是:有一些FTPClient方法不能完成完成事务的整个FTP命令序列。这些命令需要程序员在接收到肯定的中间命令后采取一些行动。程序员的代码完成其操作后,必须调用此方法以从服务器接收完成回复,并验证整个事务的成功。
这里不知道是不是描述的我这种情况, completePedingCommand的意思是完成挂起命令。可能我们当前场景就符合。刚兴趣的小伙伴可以深入去探讨一下。
五、参考连接
https://blog.csdn.net/AndyWei147/article/details/90745518
https://blog.csdn.net/weixin_39981289/article/details/128614774