基于ftp协议的文件变化主动监听

2023-11-18

前言

文件传输协议(File Transfer Protocol,FTP)是用于在网络上进行文件传输的一套标准协议,它工作在 OSI 模型的第七层, TCP 模型的第四层, 即应用层, 使用 TCP 传输而不是 UDP, 客户在和服务器建立连接前要经过一个“三次握手”的过程, 保证客户与服务器之间的连接是可靠的, 而且是面向连接, 为数据传输提供可靠保证。以上是百度百科的官方解释,通俗点讲就是ftp是通过一种协议让你能够远程访问其他计算机上的文件数据,虽然ftp能够访问到远程服务器的文件,甚至可以下载远程服务器的文件,但是ftp没有被动监听功能,无法监听到远程服务器的文件变化,这时侯就需要我们在客户端去做主动监听。

实现思路

其实主动监听比较简单,主动监听的大致思路就是轮询,没错就是轮询很通俗易懂的一种思路。通过ftp协议连接到远程服务器,之后通过循环不断的执行查看文件列表操作,通过第一次查找的缓存目录去比对后续的查找结果;如果出现后一次文件名称在缓存列表中不存在则证明该文件是新增,如果出现后一次文件名称在缓存列表中存在且文件大小不同,则证明文件被更改,反之则证明该文件无变化;如果出现缓存列表中文件存在但是后一次文件列表中不存在的文件名称,则名称该文件是删除;如上变化除无变化状态以外其余都需要更改本地缓存,以保证每次对比的准确性;想必看完如上设计大家也会发现一些弊端,比如我一个文本文件原文件内容是abc,但是我后来更改成了abd,该种情况根据如上设计方式则无法监听到文件的变化,如果要实现较为精准的变化监听则最好将远程文件缓存到本地之后利用文件md5值的方式来判断,由于本文所述的监听方式主要是类似于系统日志文件这种不经常变化文件名称和篡改文件已有内容的场景,所以利用md5值比对的方式本文不会介绍及实现。

代码实现思路

代码设计中采用事件的方式,利用监听的形式来进行文件夹内容的监听,设计类有如下ListenerFileChangeThreadRunnable(文件变化监听线程类)、ListenerChangeRunnable(文件变化线程接口)、FileChangeType(文件变化类型枚举)、FileChangeEvent(文件变化事件接口)、FileChangeData(文件变化类)、FTPServiceImpl(FTP服务类)。
具体思路为利用commons-net软件包中提供的ftp相关的类进行ftp通信操作,通过监听时建立ListenerChangeRunnable线程来论询远程服务文件列表,当监听到实现思路中的变化时利用FileChangeEvent接口发送文件变化事件,并通过FileChangeType枚举类标记文件变化类型。

具体代码实现

依赖引入

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

FTPService接口

public interface FTPService {

    /**
     * ftp登陆
     * @return boolean 是否登陆成功
     * */
    boolean login();

    /**
     * ftp登出
     * @return boolean 是否登出成功
     * */
    boolean loginOut();

    /**
     * 获取文件列表
     * @return FTPFile[] 文件列表
     * */
    FTPFile[] listFile();

    /**
     * 监听文件夹的改变
     * @param fileChangeEvent 文件改变事件
     * */
    void addListenerFileChange(FileChangeEvent fileChangeEvent);
}

FTPServiceImpl类

@Service
public class FTPServiceImpl implements FTPService {

    @Autowired
    private FTPConfig ftpConfig;

    private String SPLIT = ":";

    private ThreadLocal<FTPClient> currentFTPClient;

    private ThreadLocal<ListenerChangeRunnable> currentListener;

    public FTPServiceImpl() {
        this.currentFTPClient = new ThreadLocal<>();
        this.currentListener = new ThreadLocal<>();
    }

    @Override
    public boolean login() {
        FTPClient ftpClient = new FTPClient();
        try {
            ftpClient.connect(ftpConfig.getFtpIp(), ftpConfig.getFtpPort());
            ftpClient.login(ftpConfig.getUsername(), ftpConfig.getPassword());
            ftpClient.setControlEncoding("gb2312");

            ftpClient.changeWorkingDirectory(new String(ftpConfig.getWorkspace().getBytes("GBK"), "iso-8859-1"));
            this.currentFTPClient.set(ftpClient);
            return Boolean.TRUE;
        } catch (Exception e) {
            return Boolean.FALSE;
        }
    }

    @Override
    public boolean loginOut() {
        try {
            currentFTPClient.get().logout();
            currentFTPClient.get().disconnect();
            return Boolean.TRUE;
        } catch (Exception e) {
            return Boolean.FALSE;
        }
    }

    @Override
    public FTPFile[] listFile() {
        FTPClient ftpClient = this.currentFTPClient.get();
        try {
            return ftpClient.listFiles();
        } catch (Exception e) {
            return null;
        }
    }

    @Override
    public void addListenerFileChange(FileChangeEvent fileChangeEvent) {
        FTPClient ftpClient = this.currentFTPClient.get();
        ListenerFileChangeThreadRunnable listenerFileChangeThread = new ListenerFileChangeThreadRunnable(ftpClient, fileChangeEvent);
        this.currentListener.set(listenerFileChangeThread);
        new Thread(listenerFileChangeThread).start();
    }
}

FileChangeEvent接口

public interface FileChangeEvent {

    /**
     * 文件发生改变时触发此方法
     * @param fileChangeData 文件发生了改变
     * */
    @Function
    void change(FileChangeData fileChangeData);
}

FileChangeData实体类

@Data
public class FileChangeData {

    /**
     * 文件信息
     * */
    private FTPFile ftpFile;

    /**
     * 文件改变类型
     * */
    private FileChangeType eventType;

    /**
     * 文件名称
     * */
    private String fileName;

    /**
     * 文件大小
     * */
    private Long fileSize;

    /**
     * FTPClient
     * */
    private FTPClient ftpClient;

    /**
     * 获取文件输入流
     * @return InputStream
     * */
    public InputStream getInputStream() {
        //如果是删除事件则不能够获取流
        if (Objects.equals(eventType, FileChangeType.FILE_DELETED)) {
            return null;
        }

        try {
            return ftpClient.retrieveFileStream(this.fileName);
        } catch (IOException e) {
            return null;
        }
    }
}

FileChangeType枚举

public enum FileChangeType {
    FILE_UPDATE(0, "文件更新"),
    FILE_ADD(1, "文件添加"),
    FILE_DELETED(2, "文件删除");

    @Getter
    private Integer type;

    @Getter
    private String desc;

    FileChangeType(Integer type, String desc) {
        this.type = type;
        this.desc = desc;
    }
}

ListenerChangeRunnable枚举

public interface ListenerChangeRunnable extends Runnable {

    /**
     * 停止监听文件
     * @return boolean 是否停止成功
     * */
    boolean stopListener();
}

ListenerFileChangeThreadRunnable实现类

@Slf4j
public class ListenerFileChangeThreadRunnable implements ListenerChangeRunnable {

    private final FTPClient ftpClient;

    private volatile boolean stop;

    private final Map<String, Long> fileMemory;

    private final FileChangeEvent fileChangeEvent;

    public ListenerFileChangeThreadRunnable(FTPClient ftpClient, FileChangeEvent fileChangeEvent) {
        this.ftpClient = ftpClient;
        this.fileChangeEvent = fileChangeEvent;
        this.fileMemory = new HashMap<>();
    }

    @Override
    public void run() {
        while (!stop) {
            try {
                FTPFile[] ftpFiles = ftpClient.listFiles();

                //判断文件被删除
                if (fileMemory.size() > 0) {
                    Set<String> fileNames = new HashSet<>();
                    for (FTPFile ftpFile : ftpFiles) {
                        if (ftpFile.isDirectory()) {
                            log.info("文件夹不做删除判断");
                            continue;
                        }
                        fileNames.add(ftpFile.getName());
                    }
                    Set<Map.Entry<String, Long>> entries = fileMemory.entrySet();
                    for (Map.Entry<String, Long> map : entries) {
                        if (!fileNames.contains(map.getKey())) {
//                            log.info("文件{}被删除了", map.getKey());
                            FileChangeData fileChangeData = new FileChangeData();
                            fileChangeData.setEventType(FileChangeType.FILE_DELETED);
                            fileChangeData.setFileName(map.getKey());
                            fileChangeData.setFileSize(map.getValue());
                            fileMemory.remove(map.getKey());
                            fileChangeEvent.change(fileChangeData);
                        }
                    }
                }
                //判断文件是否有更改或新增
                for (FTPFile ftpFile: ftpFiles) {
                    //判断是否为文件夹
                    if (ftpFile.isDirectory()) {
//                        log.info("{}为文件不进行监听操作", ftpFile.getName());
                        continue;
                    }
                    FileChangeData fileChangeData = new FileChangeData();
                    fileChangeData.setFileName(ftpFile.getName());
                    fileChangeData.setFileSize(ftpFile.getSize());
                    fileChangeData.setFtpFile(ftpFile);
                    //文件是否存在于缓存文件列表中
                    if (fileMemory.containsKey(ftpFile.getName())) {
//                        log.info("文件{}在内存中已经存在,进行大小判断", ftpFile.getName());
                        if (!Objects.equals(fileMemory.get(ftpFile.getName()), ftpFile.getSize())) {
//                            log.info("文件{}在内存中已经存在且大小不一致,进行更新缓存操作", ftpFile.getName());
                            fileMemory.put(ftpFile.getName(), ftpFile.getSize());
                            fileChangeData.setEventType(FileChangeType.FILE_UPDATE);
                            fileChangeEvent.change(fileChangeData);
                        }
                        continue;
                    }
//                    log.info("文件{}在内存中不存在进行缓存操作", ftpFile.getName());
                    fileMemory.put(ftpFile.getName(), ftpFile.getSize());
                    fileChangeData.setEventType(FileChangeType.FILE_ADD);
                    fileChangeEvent.change(fileChangeData);
                }
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

    @Override
    public boolean stopListener() {
        this.stop = Boolean.TRUE;
        this.fileMemory.clear();
        return this.stop;
    }
}

FTPConfig配置类

@Data
@Configuration
public class FTPConfig {

    @Value("${ftp.ip:127.0.0.1}")
    private String ftpIp;

    @Value("${ftp.port:21}")
    private Integer ftpPort;

    @Value("${ftp.username:root}")
    private String username;

    @Value("${ftp.password:root}")
    private String password;

    @Value("${ftp.workspace:root}")
    private String workspace;
}

使用举例

@SpringBootTest
class SendEmailApplicationTests {
   @Autowired
   private FTPService ftpService;
   @Test
   void ftpTest() {
        ftpService.login();
        FTPFile[] ftpFiles = ftpService.listFile();
        for (FTPFile file : ftpFiles) {
            System.out.println(String.format("filename:%s,filesize:%s", file.getName(), file.getSize()));
        }
        ftpService.addListenerFileChange(ftpFile -> {
            System.out.println(String.format("文件%s被改变了,文件改变类型%s", ftpFile.getFileName(), ftpFile.getEventType().getDesc()));
        });
    } 
}

结语

如上只是面对ftp文件变化监听问题的一种实现思路及具体实现代码,但并非最优解,如关于本篇内容存在疑问欢迎评论提问或者私聊,同时如果大家对于该种场景需求较多作者可以将该部分内容封装为starter供大家学习使用。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

基于ftp协议的文件变化主动监听 的相关文章

随机推荐

  • OpenCV进阶--图像变换(三)

    继上文 五 图像透视变换 首先透视变换是按照物体成像投影规律进行变换 即将物体重新投影到新的成像平面 在透视变换中 透视前的图像和透视后的图像之间的变换关系可以用一个3 3的变换矩阵表示 该矩阵可以通过两幅图像中4个对应点的坐标求取 因此透
  • 【Linux基础】stat函数

    stat函数 函数描述 获取文件属性 函数原型 include
  • iOS开发之数据存取(一)——SQLite

    概览 在iOS开发中数据存储的方式可以归纳为两类 一类是存储为文件 另一类是存储到数据库 例如前面IOS开发系列 Objective C之Foundation框架的文章中提到归档 plist文件存储 包括偏好设置其本质都是存储为文件 只是说
  • 计算机答卷方式 闭卷满分为90分,关于《计算机应用技术专业综合理论》考试说明.doc...

    计算机应用技术专业综合理论 考试大纲 一 考试性质 广东省高等职业院校对口自主招生考试是以中等职业技术学校对口专业应届毕业生和中等职业相关专业毕业 有两年以上实践经验的社会人员为对象的选拔性考试 二 考试内容 计算机应用技术专业综合理论 的
  • IDEA jdk 下载安装及项目中的配置

    进入 Oracle官网 的 Java 界面 Oracle官网地址 https www oracle com java 1 JDK下载 1 1 在网站页面滚动鼠标下拉定位到Java 选择Oracle JDK 1 2 选择Java archiv
  • Xcode警告、错误解决方法总结

    从sdk3 2 5升级到sdk 7 1中间废弃了很多的方法 还有一些逻辑关系更加严谨了 1 警告 xoxoxoxo is deprecated 解决办法 查看xoxoxoxo的这个方法的文档 替换掉这个方法即可 2 警告 Declarati
  • excel函数去重_Excel去除重复值方法汇总

    经常有人问到excel中去除重复值的问题 这个问题本来也一直有很多人讨论 现将做法汇总出来共大家参考 什么是重复值 顾名思义 重复值是指一组数据中有重复记录 去除这些重复的记录就叫去除重复值 具体有两种情况 源数据如图 去重后 结果一 所有
  • 蓝桥杯青少组python:第十三届省赛第一场

    选择题 1 下列二进制中最大数是 A 110 B 1010 C 1100 D 1001 2 以下方法 不是对文件读操作的是 A readline B readlines C readtext D read 3 以下对turtle库中函数描述
  • read_csv 与 to_csv方法

    1 read csv方法 1 1 返回数据类型 DataFrame 二维标记数据结构 列可以是不同的数据类型 是最常用的pandas对象 如同Series对象一样接受多种输入 lists dicts Series DataFrame Ser
  • JavaScript this关键字的理解

    JavaScript this 关键字的理解 仅供个人学习做笔记使用 大佬轻喷 1 全局环境直接输出this指向全局对象 console log this 2 全局函数输出this指向window 全局函数其实是window 全局对象 的方
  • 按键板的原理和实现--基于GPIO的按键板

    按键板的原理和实现 基于GPIO的按键板 上篇介绍简单的ADC实现 需要IC提供一个额外的ADC 但出于IC成本的考虑 无法提供这个的ADC时 但提供了多个额外的GPIO General Purpose Input Output 双向的 可
  • Windows平台下MingGW的网络socket编程模型

    Windows平台下MingGW的网络socket编程模型 1 TCP服务器 include
  • NSSCTF web题记录

    目录 web GXYCTF 2019 BabyUpload htaccess利用 NISACTF 2022 babyserialize pop链 NISACTF 2022 popchains pop链 NSSRound 4 SWPU 1zw
  • Linux删除文件每一行的首尾空格等操作——sed简明教程

    cat tmp txt 123456789 123456789 123456789 删除每行第一个字符 sed s tmp txt 23456789 23456789 23456789 删除每行前两个字符 并保存到tmp2 txt sed
  • Java秒杀系统方案优化 2 --第2章 实现用户登录以及分布式session功能

    第2章 实现用户登录以及分布式session功能 1 明文密码两次md5入库 分别使用签名如1a2b3c4d 分别用签名和密码使用MD5加密两次后 一次是最原始密码加密 一次是加密后再使用MD5和签名加密 才存入数据库 每个用户对应都有一个
  • Dynamics 365 DevOps CI/CD之Solution

    CI CD到了Soution就没太多可说的了 按部就班配置就行 我选择的工具是Power DevOps Tool 1 首先下载工具 然后设置连接字符串去连环境 连接字符串还是用ClientSecret的形式 2 当然导出前还是要发布下自定义
  • Ue4蓝图访问外部接口

    首先我们搭建一个服务 node express 注意电脑必须安装node mkdir test server cd test server npm init npm install express body parser chalk pat
  • 数组复制的三种方法(超详细)

    数组复制的三种方法 Arrays类 copyOf 方法 copyOfRangs 方法 System类 arraycopy Arrays类 copyOf 方法 copyOf 方法适用于从下标0开始 复制指定长度的元素到目标数组 源码如下 pu
  • jQuery实现下拉菜单[代码+详细讲解+效果图]

    文章目录 前言 一 案例功能描述 二 html代码讲解 三 css代码讲解 四 jQuery实现功能及代码讲解 五 效果图 总结 前言 下拉菜单也是每个网页基本都会用到的案例 所以知识点还是要我们掌握的 这个案例比较简单 主要还是我们的jQ
  • 基于ftp协议的文件变化主动监听

    基于ftp协议的文件变化主动监听 前言 实现思路 代码实现思路 具体代码实现 依赖引入 FTPService接口 FTPServiceImpl类 FileChangeEvent接口 FileChangeData实体类 FileChangeT