- fswatch 原理与应用简介
- fswatch 安装
- fswatch 实践应用
- 具体应用场景与细节补充

1. 简介
有些知识,你知道了不算厉害,但你要是不知道,就容易出乱。
很多时候,程序需要及时获取磁盘上某个文件对象(文件夹、文件)的变动信息,这时候 "绝大多数操作系统支持主动推送此类信息" 这个知识点,就很重要。
如果不知道这点,大概就只能让程序定时去检查、并维护前后两套数据,自行对比以发现是否有哪个文件对象发生了哪些变化。
fswatch 本是多数 linux 下的一个工具程序------但现已经支持跨平台。它提供动态库和配套的 C、C++头文件 (合称为 libfswatch),借助它,我们也可以在自己的程序中,直接获得监控指定文件对象变动信息。
更多关于文件变动信息监控以及 libfswatch 库的基本原理、功能以及应用场景示例,请看视频 A。
- 视频 A : FSWatch 简介
001-监控你的文件-1原理简介-C++开源库108杰
2. 安装 fswatch
Linux 下,可以使用各自发行版自带的包管理器安装 fswatch。以 Ubuntu 为例:
shell
sudo apt install fswatch
Windows下,因为我们使用 msys2 来安装 fswatch。进入对应环境(我们的课程使用的是 ucrt64)的终端,输入以下指令即可:
shell
pacman -S mingw-w64-ucrt-x86_64-fswatch
msys2 本身的安装可看课程 《VSCODE 多语言开发保姆手册》)的第一节课 《MSys2 + GCC 安装与应用》。
如果你手快并且长得帅的话,最快十秒钟具体操作过程以及效果演示,见视频 B 。
002-监控你的文件-2十秒安装-C++开源库108杰
3. 上机实践
我们使用 Windows + Msys2 + VSCode + GCC + CMake 的开发环境,相关准备工作同样可观看 《VSCODE 多语言开发保姆手册》)。
- 视频 C: 上机实践
003-监控你的文件-3使用示范-C++开源库108杰
3.1 CMakeList.txt
cmake
cmake_minimum_required(VERSION 3.15.0)
project(HelloFSWatch VERSION 0.1.0 LANGUAGES C CXX)
add_executable(HelloFSWatch main.cpp)
target_link_libraries(${PROJECT_NAME} PRIVATE fswatch)
target_link_directories(${PROJECT_NAME} PRIVATE "C:/msys64/ucrt64/bin")
说明:
其中最后两行是我们添加的。
- 第一行 target_link_libraries 用于告诉编译器,当前项目需要链接到 fswatch 这个库;
- 第二行 target_link_directories 则告诉编译器,上哪里去找 fswatch 这个库;
- PRIVATE 用于指示对应的设置,仅在当前目标中生效,对我们这个小项目并无影响;
- ${PROJECT_NAME} 是 CMake 的内置变量,用于表示当前项目名称,即 HelloFSWatch (见第2行的 project 语句)
GCC 在链接某个库时,会自动为它加上 lib 前缀,以及对应的扩展名,本例为 .dll,因此,我们在 CMake 源文件中写的 fswatch,最终会组成全名 "libfswatch.dll"
3.2 main.cpp
cpp
#include <ctime>
#include <iostream>
#include <iomanip>
#include <libfswatch/c++/monitor_factory.hpp>
// 返回值:必须是 void,入参必须是 std::vector<fsw::event> const & 和 void *
void on_file_changed(std::vector<fsw::event> const & events, void *)
{
std::cout << "Files Changed:\n";
for (auto const& event : events)
{
// 输出变动的文件路径:
std::cout << event.get_path() << "\n";
// 输出变动的时间:
std::time_t t = event.get_time();
std::tm lt; // local time 本地时间结构
localtime_s(<, &t); // C11 开始支持的线程安全的时间转换函数
std::cout << std::put_time(<, "%Y-%m-%d %H:%M:%S") << "\n";
// 输出变动的标志:
std::cout << "Flags:\n";
for (auto const& flag : event.get_flags())
{
std::cout << "\t" << fsw_get_event_flag_name(flag) << "\n";
}
}
}
int main(int, char**)
{
std::vector<std::string> paths = {"d:\\tmp"};
auto *monitor = fsw::monitor_factory::create_monitor(
system_default_monitor_type,
paths,
on_file_changed
);
// 启动监控
monitor->start(); // 进入死循环
}
如代码所示,使用 fswatch 实现被动式响应(那操作系统主动回调我们的函数)的文件变动监控过程,关键三步:
Step1 : 通过工厂类的静态方法 create_monitor 创建一个监控器。需指定类型(通常就是采用代码中的默认类型)、待监控的文件对象路径(默认类型下,Windows 操作系统仅支持以文件夹为单位进行测控,因此该参数只能填写文件夹),回调函数;实际还有第四个默认参数,用于向回调函数传递额外的参数,通常并不需要,因此它有默认值 nullptr;
Step2 : 准备好你的回调函数(即代码中的 on_file_changed ),注意,该函数原型须严格符合:void (std::vectorfsw::event const& , void *) ;
Step3 : 启动监控。
3.3 on_file_changed 详解
回调函数名字无所谓。第一个入参类型需为 std::vectorfsw::event const& ,这是一个常量引用,第二个入参为 void *,即前面创建监控器,所传入的第四个参数(我们使用的了函数参数默认值)。
第一个入参 events 是复数(一个容器),表明该函数被操作系统回调用时,操作系统可能想告诉我们的情况有可能是:
- 一个文件对象的一个变动事件;
- 一个文件对象的多个变动事件;
- 多个文件对象的多个变动事件。
每一个 event 主要包含:变动文件路径的对象(如果变动对象是一个文件,此时Windows系统上报的也是文件名),变动时间(注意,不一定非常精确),变动标志等。
库为变动标志取了一些英文名字,常见的有:
- NoOp : 无变动
- PlatformSpecifc :特定平台指定
- Created
- Updated
- Removed
- OwnerModified : 对象的拥有者变化,常见于 *uix 系统;
- AttributeModified: 对象属性发生变化
- MovedFrom
- MovedTo
- ......
每个变动事件可能包含有多个变动标志。
on_file_changed 第一层循环用来遍历所有事件,输出每个对象变动路径、时间。其中用到 纯C(不要加 std::)的,C11 才支持安全的时间转换函数 localtime_s 和 C++ 输出的扩展的格式操控符:put_time() 注意操控符并不返回字符串。
有关 C++ 流与操控符(比如,如何自定义一个输入或输出流操控符),可学习 《C++"流"编程视频辅导》。
第二层循环输出当前事件的所有标志,其中用到 fsw_get_event_flag_name()以获取指定标志值对应的英文名称。
4. 补充
4.1 场景补充
许多问题,确实可以通过这个"消息队列"轻松实现,除视频中提到的给用户发送通知邮件之外,还有如下场景可考虑使用:
- 用户上传文件内容审核:某网站系统,允许用户发表带图的文章,需要对图片或文章的文字内容做安全审核;
- 线上系统自动源代码编译:程序员使用 git 等工具,将相关模块的源代码上传到服务器上并触发自动编译;
- 数据采集与比对系统:为了稳定性,很多监控系统会划分成数据采集与数据比对两个独立的子进程。
4.2 细节补充
"封包" 文件:很多时候,接收方发现出现一个新增的文件,并不能直接开始读取它,因为此时发送方可能还在往该文件中写入数据。此时有两种经典解决方法。
- 方法一:要求发送方以独占模式打开文件,对应的,接收方尝试也以独占模式打开,这样后者将失败,从而避免双方同时处理(哪怕一写一读);
- 方法二:双方约定特定文件名称(通常是扩展名)为 "封包"文件。比如,约定 .seal为封包文件的扩展名。则当发送方先生成 a.dat 文件,再生成名为 a.dat.seal 的封包文件(通常是一个零字节文件);接收方仅在发现后者之后,才开始处理 a.dat(并在处理结束后,删除封包文件)。