• springboot FTP服务器 上传&&下载示例demo


    最近项目上需要使用ftp服务器和第三方进行资源交互,于是写了个小demo记录下~

    基础知识

    FTP服务器

    FTP(File Transfer Protocol)即文件传输协议,是一种基于TCP的协议,采用客户/服务器模式。通过FTP协议,用户可以在FTP服务器中进行文件的上传或下载等操作。虽然现在通过HTTP协议下载的站点有很多,但是由于FTP协议可以很好地控制用户数量和宽带的分配,快速方便地上传、下载文件,因此FTP已成为网络中文件上传和下载的首选服务器。同时,它也是一个应用程序,用户可以通过它把自己的计算机与世界各地所有运行FTP协议的服务器相连,访问服务器上的大量程序和信息。FTP服务的功能是实现完整文件的异地传输。(来自百度百科)

    多级目录下创建文件

    一直以为ftp服务器多级目录创建和java中目录创建一样,然而并不是滴~ 可参考文档:关于ftp上传changeWorkingDirectory()方法的路径切换问题
    将文件hello.json上传至/home/upload/目录下步骤:

    1. 先使用FTPClient.changeWorkingDirectory("home"),判断home目录是否存在?若不存在则2
    2. 使用FTPClient.makeDirectory("home") 创建目录,同时使用FTPClient.changeWorkingDirectory("home")将ftp session指向home目录
    3. 使用FTPClient.changeWorkingDirectory("upload")判断upload目录是否存在?若不存在则4
    4. 使用FTPClient.makeDirectory("upload")创建目录,同时使用FTPClient.changeWorkingDirectory("upload")将ftp session指向upload目录
    5. 使用FTPClient.storeFile(fileName, is); 上传文件流is

    上传流程

    FTP传输模式

    FTP的主动模式和被动模式

    对比方面主动模式被动模式
    客户端随机开放一个端口(1024以上)-
    服务端-在本地随机开放一个端口(1024以上)

    实践出真知

    引入依赖和加入配置

    1. pom.xml 中添加依赖
    <dependency>
    	<groupId>commons-netgroupId>
    	<artifactId>commons-netartifactId>
    	<version>3.6version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    1. 在application.yml中添加ftp服务器配置(多环境则需要在application-dev/test/prod.yml中分别配置)
    ftp:
      hostname: 172.16.1.1
      port: 21
      username: lizzy
      password: lizzy
      root-path: /upload/
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    定义配置bean

    package com.lizzy.common.config;
    
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.stereotype.Component;
    
    import lombok.Data;
    
    @ConfigurationProperties(prefix = "ftp")
    @Data
    @Component
    public class FtpConfig {
    	
    	private String hostname;
    	
    	private Integer port;
    	
    	private String username;
    	
    	private String password;
    	
    	private String rootPath;
    	
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    定义FTP工具

    获取FTPClient,设置超时

    private FTPClient getFTPClient() {
    		
    	FTPClient client = new FTPClient();
    	// 设置默认超时时间30s
    	client.setDefaultTimeout(30000);
    	// 设置链接超时时间
    	client.setConnectTimeout(30000);
    	// 设置数据传输时间
    	client.setDataTimeout(30000);
    	return client;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    上传

    /**
     * 上传文件至FTP服务器 
     * @param config ftp服务器配置
     * @param path 相对目录(不包含根目录)
     * @param fileName 文件名 
     * @param is 文件流 
     */
    public static void upload(FtpConfig config, String path, String fileName, InputStream is) {
    	
    	FTPClient client = getFTPClient();
    	try {
    		// 连接服务器
    		client.connect(config.getHostname(), config.getPort());
    		// 登录
    		client.login(config.getUsername(), config.getPassword());
    		int reply = client.getReplyCode();
    		if (!FTPReply.isPositiveCompletion(reply)) {
    			
    			// 连接失败 
    			client.disconnect();
    			log.info("FTP服务器配置链接失败!请检查配置:{}", config.toString());
    			return ;
    		}
    		
    		// 设置被动模式,开通一个端口来传输数据
    		client.enterLocalPassiveMode();
    		// 切换到上传目录
    		final String filePath = config.getRootPath() + path;
    		if (!client.changeWorkingDirectory(filePath)) {
    			// 目录不存在则创建
    			String[] dirs = filePath.split("/");
    			for (String dir : dirs) {
    				
    				if (StringUtils.isEmpty(dir)) {
    					continue ;
    				}
    				if (!client.changeWorkingDirectory(dir)) {
    					client.makeDirectory(dir);
    					client.changeWorkingDirectory(dir);
    				}
    			}
    		}
    		
    		// 设置被动模式,开通一个端口来传输数据
    		client.enterLocalPassiveMode();
    		//设置上传文件的类型为二进制类型
    		client.setFileType(FTP.BINARY_FILE_TYPE);
    		client.storeFile(fileName, is);
    		log.debug("文件{}成功上传至FTP服务器!", filePath + "/" + fileName);
    		client.logout();
    		
    	} catch (IOException e) {
    		log.info("FTP服务器配置链接失败!错误:{}", e.getMessage());
    		e.printStackTrace();
    	} finally {
    		try {
    			is.close();
    			if (client.isConnected()) {
    				client.disconnect();
    			}
    		} catch (IOException e) {
    			e.printStackTrace();
    		}
    	}
    	
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66

    下载

    /**
     * 从FTP服务器下载文件流 
     * @param config FTP服务器配置
     * @param path 相对目录(不包含根目录)
     * @param fileName 文件名 
     * @return
     */
    public static InputStream download(FtpConfig config, String path, String fileName) {
    	
    	FTPClient client = getFTPClient();
    	try {
    		// 连接服务器
    		client.connect(config.getHostname(), config.getPort());
    		// 登录
    		client.login(config.getUsername(), config.getPassword());
    		int reply = client.getReplyCode();
    		if (!FTPReply.isPositiveCompletion(reply)) {
    			
    			// 连接失败 
    			client.disconnect();
    			log.info("FTP服务器配置链接失败!请检查配置:{}", config.toString());
    			return null;
    		}
    		
    		// 设置被动模式,开通一个端口来传输数据
    		client.enterLocalPassiveMode();
    		// 切换到下载目录
    		final String filePath = config.getRootPath() + path;
    		client.changeWorkingDirectory(filePath);
    		log.debug("FTP session指向下载目录:{}", filePath);
    		//设置上传文件的类型为二进制类型
    		client.setFileType(FTP.BINARY_FILE_TYPE);
    //			client.setControlEncoding("utf8");
    		
    		// 中文名会下载失败,文件名需转码 
    		InputStream is = client.retrieveFileStream(new String(fileName.getBytes("gbk"), "ISO-8859-1"));
    		if (null == is) {
    			log.debug("下载文件:{}失败!读取长度为0!", fileName);
    			return null;
    		}
    		InputStream retIs = copyStream(is);
    		is.close();
    		client.completePendingCommand();
    		log.debug("从FTP服务器下载文件({})成功!", fileName);
    		return retIs;
    		
    	} catch (IOException e) {
    		log.info("FTP服务器配置链接失败!错误:{}", e.getMessage());
    		e.printStackTrace();
    	} finally {
    		
    		try {
    			if (client.isConnected()) {
    				log.info("从FTP服务器下载文件{}流程结束,关闭链接!", fileName);
    				client.disconnect();
    			}
    		} catch (IOException e) {
    			e.printStackTrace();
    		}
    	}
    	return null;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62

    复制流方法如下:

    private InputStream copyStream(InputStream is) throws IOException {
    	
    	ByteArrayOutputStream baos = new ByteArrayOutputStream(); 
    	byte[] buffer = new byte[1024];
    	int len;
    	while ((len = is.read(buffer)) > -1 ) {
    	    baos.write(buffer, 0, len);
    	}
    	baos.flush();
    	
    	return new ByteArrayInputStream(baos.toByteArray());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    遇到的问题

    文件覆盖

    FTP服务器上同名文件是否覆盖的配置

    retrieveFileStream卡死

    出错前代码如下:我真是看了半天也不知道为嘛子就失败,于是乎开始打印各个文件的大小,终于发现了一点点端倪,两个不同的文件打印的文件大小一样!看出来了么,简直无语死了,慎用inputstream.available()方法判断流=_=||

    // 中文名会下载失败,文件名需转码 
    InputStream is = client.retrieveFileStream(new String(fileName.getBytes("gbk"), "ISO-8859-1"));
    if (null == is || is.available() < 1) {
    	log.debug("下载文件:{}失败!读取长度为0!", fileName);
    	return null;
    }
    log.debug("结束读取文件:{},大小为:{}", fileName, is.available());
    InputStream retIs = copyStream(is);
    is.close();
    client.completePendingCommand();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    判断流操作慎用inputstream.available()

    注意事项(采过的坑)

    先connect后enterLocalActiveMode

    参考文档:Ftpclient调用retrieveFileStream返回null, docker中下载失败问题

    connect方法中,将模式设置为主动模式ACTIVE_LOCAL_DATA_CONNECTION_MODE.
    enterLocalActiveMode方法则是将链接模式设置为被动模式

    若先调用enterLocalActiveMode再connect,则链接模式还是为主动模式!

    获取返回流一定要调用completePendingCommad

    参考文档:FTPClient中使用completePendingCommand方法注意事项

    编写FTP过程中遇到的问题就是,流程的框框都是固定的,不能多写也不能少写,不然就是一堆的block!

    // 上传方法,之后若调用completePendingCommand会卡死 
    public boolean storeFile(String remote, InputStream local)
    // 上传方法,之后必须调用completePendingCommand 
    public OutputStream storeFileStream(String remote)
    
    // 下载方法,之后若调用completePendingCommand会卡死 
    public boolean retrieveFile(String remote, OutputStream local)
    // 下载方法,之后必须调用completePendingCommand 
    public InputStream retrieveFileStream(String remote)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    先inputStream.close()再completePendingCommad

    参考文档:FTPClient中使用completePendingCommand方法时之踩坑

    completePendingCommand()会一直在等FTP Server返回226 Transfer complete,但是FTP Server只有在InputStream执行close方法时,才会返回。所以先要执行close方法,再执行completePendingCommand

    后记

    demo写的很匆忙,后续遇到问题了再更新~

  • 相关阅读:
    RL学习笔记(一)
    实验七 循环神经网络(3)LSTM的记忆能力实验
    PHICOMM(斐讯)N1盒子 - Armbian5.77(Debian 9)刷入EMMC
    【React-Hooks基础】入门级详解——useState / useEffect /自定义hook
    Linux系统学习
    Towards a General Purpose CNN for Long Range Dependencies in ND
    基于php的物流信息公共平台设计与实现
    【API篇】十、生成Flink水位线
    八股文-TCP的三次握手
    查找postgreSQL环境pg_hba.conf文件位置
  • 原文地址:https://blog.csdn.net/huhui806/article/details/126856494