windows环境下C++实现的多种目录监控和优劣解析

FileSystemWatcher使用原因:

最近在项目中需要在windows平台,实时的监控一个目录下各级子目录中被FTP下载到本地的文件的生成,所以研究了windows的各种监控目录变化信息的API。

1.FindFirstChangeNotification()函数

         功能:创建一个改变通知句柄,并设置通知条件,即发生某种修改时,该通知句柄将被促发生效。该函数仅仅负责对文件修改作出通知,并不记录和反馈修改信息。

         返回值:成功时返回指向改变通知的句柄,失败时返回INVALID_HANDLE_VALUE,可以用GetLastError()函数得到。

         示例代码:      

#define _CRT_SECURE_NO_WARNINGS

#include
#include
#include
void main()
{
	//监视C盘根目录下文件名改变的例子,其中while循环一直监控,直到程序关闭
	HANDLE hChangeHandle = FindFirstChangeNotification("C:\\", false, FILE_NOTIFY_CHANGE_FILE_NAME);
	//判断是否设置成功
	if (hChangeHandle == INVALID_HANDLE_VALUE) {
		printf("Find First Change Notification failed\n");
		system("pause");
		return;
	}
		//等待通知
		printf("These change a file name\n");
		//循环等待
		while (true)
			if (FindNextChangeNotification(hChangeHandle)) {
				printf("These change a file name\n");
				//结束监视程序调用FindCloseChangeNotification关闭句柄
				FindCloseChangeNotification(hChangeHandle);
				getchar(); //按键退出程序}
				system("pause");
			}
	
	system("pause");
}
           优点: 这种方式监控目录很简单,API很好掌握,返回值也很简单

           缺点: 太TM简单了,无法获取是哪一个文件发生了改变。

          应用途径:可以作为简单的警报模块,当一个固定的文件发生了很细微的(比较单调,或者已经知道类型的)变化的时候,可以及时的通知和反馈。

  ps:下面是转载自http://blog.csdn.net/woshinia/article/details/8162983的一段代码,略作修改以便于直接使用。

#include 
#include 
#include 

//使用FindFirstChangeNotification()实现了警报
void WatchDirectory(LPTSTR lpDir)
{
	DWORD dwWaitStatus;
	HANDLE dwChangeHandles[3];
	TCHAR lpDrive[4];//存储磁盘符
	TCHAR lpFile[_MAX_FNAME];//用于存储文件名
	TCHAR lpExt[_MAX_EXT];//用于存储对应文件的后缀

	_tsplitpath(lpDir, lpDrive, NULL, lpFile, lpExt);
	lpDrive[2] = (TCHAR)'\\';
	lpDrive[3] = (TCHAR)'\0';

	//分别监控文件名,路径名,文件内容的修改 
	dwChangeHandles[0] = FindFirstChangeNotification(
		lpDir,
		TRUE,
		FILE_NOTIFY_CHANGE_FILE_NAME); //文件名   
	if (dwChangeHandles[0] == INVALID_HANDLE_VALUE)
		ExitProcess(GetLastError());

	dwChangeHandles[1] = FindFirstChangeNotification(
		lpDrive,
		TRUE,
		FILE_NOTIFY_CHANGE_DIR_NAME); //路径名  
	if (dwChangeHandles[1] == INVALID_HANDLE_VALUE)
		ExitProcess(GetLastError());

	dwChangeHandles[2] = FindFirstChangeNotification(
		lpDir,
		TRUE,
		FILE_NOTIFY_CHANGE_LAST_WRITE); //文件内容/或者说最后保存时间  
	if (dwChangeHandles[2] == INVALID_HANDLE_VALUE)
		ExitProcess(GetLastError());

	//改变通知已经设置完成,现在只需等待这些通知被触发,然后做相应处理
	while (TRUE)
	{
		dwWaitStatus = WaitForMultipleObjects(3, dwChangeHandles, FALSE, INFINITE);//把等待的时间设置为无限
	//	time(&ChangeTime);

		switch (dwWaitStatus)
		{
			//分别监控文件名,路径名,文件内容的修改 
		case WAIT_OBJECT_0:		
			if (FindNextChangeNotification(dwChangeHandles[0]) == FALSE)
				ExitProcess(GetLastError());
			break;
		case WAIT_OBJECT_0 + 1:
			if (FindNextChangeNotification(dwChangeHandles[1]) == FALSE)
				ExitProcess(GetLastError());
			break;
		case WAIT_OBJECT_0 + 2:
			if (FindNextChangeNotification(dwChangeHandles[2]) == FALSE)
				ExitProcess(GetLastError());
			break;
		default:
			ExitProcess(GetLastError());
		}
	}
}     

2.ReadDirectoryChangesW()函数

         功能:监控文件修改,并记录文件修改的相关信息,如修改的文件名,何种类型的修改等。

         返回值:如果函数成功,返回值就是非0。对于同步调用,这意味着操作成功,对于异步调用,这意味着操作成功地排队。如果函数失败,返回值是0。如果操作目录或文件系统不支持这个操作,函数将返回ERROR_INVALID_FUNCTION,可以使用GetLastError()函数获取。

         注意,FindFirstChangeNotification 和 ReadDirectoryChangesW 是互斥的,不能同时使用。

ps:   这个API其实我在一开始的时候是最感兴趣的,因为它机会包含了我一切想要的功能,而且看起来也不是很复杂,但是在实际的操作中总会出现一些奇怪的现象——比如同时在需要monitor的path下change一些file,总会奇怪的pass掉一部分数据,而在我的项目中这是不能容忍的,为此我找了很多的资料,幸好我还是找到了一个大牛解释这个问题:

Jim Beveridge:

使用 ReadDirectoryChangesW 的最大挑战在于,在IO模式,处理信号,等待方式,以及线程模型这几个问题的整合上,存在数百种可能性。如果你不是 Win32 I/O 方面的专家,即使最简单的场景,你也很难搞定。

  • A. I/O模式:
    1. 阻塞同步(Blocking synchronous)
    2. 触发式同步(Signaled synchronous)
    3. 重叠异步(Overlapped asynchronous)
    4. 完成例程(Completion Routine) (又名 Asynchronous Procedure Call or APC)
  • B. 当调用 WaitForXxx 函数的时候:
    1. 等待目录句柄
    2. 等待 OVERLAPPED 结构体里的 Event 对象
    3. 什么都不等 (APCs)
  • C. 处理通知:
    1. 阻塞
    2. WaitForSingleObject
    3. WaitForMultipleObjects
    4. WaitForMultipleObjectsEx
    5. MsgWaitForMultipleObjectsEx
    6. IO完成端口(I/O Completion Ports)
  • D. 线程模型:
    1. 每个工作线程调用一次 ReadDirectoryChangesW.
    2. 每个工作线程调用多次 ReadDirectoryChangesW.
    3. 在主线程上调用多次 ReadDirectoryChangesW.
    4. 多个线程进行多个调用. (I/O Completion Ports)

  最后,当调用 ReadDirectoryChangesW 的时候,你可以通过 flags 选择你要监控的内容,包括文件创建,内容变更,属性变更等等。你可以多次调用,每次一个 flag,也可以在一次调用中使用多个 flag。多个 flag 总是正确的解决方案。但如果你为了调试方便,需要一个 flag 一个 flag 的调用的话,那就需要从 ReadDirectoryChangesW 返回的通知缓冲区中读取更多的数据。

  如果你的脑子正在囧的话,那么你就能够明白为什么那么多人都没法搞定这件事了。

解决方案:

  简单 - A2C3D1 - 在单独的线程中调用 ReadDirectoryChangesW,然后通过 PostMessage 发送给主线程。对于性能要求不高的 GUI 程序最合适。在 CodeProject 上的 CDirectoryChangeWatcher 就是使用的这个策略。微软的 FWATCH 例子 也是使用的这个策略。

  性能 - A4C6D4 - 性能最好的解决方案是使用I/O完成端口,但是,这个激进的多线程方案实在太过复杂,应当仅限在服务器上使用。对任何 GUI 程序来说,这个方案似乎都是不必要的。如果你不是一个多线程专家,请远离这个策略。

  平衡 - A4C5D3 - 通过完成例程(Completion Routines),在一个线程中完成所有工作。你可以发起尽可能多的 ReadDirectoryChangesW 调用,由于完成例程是自动分派的,所有不需要等待任何句柄。你可以通过回调传递对象的指针,以便跟踪原始的数据结构。

  起初我曾经认为 GUI 程序可以通过 MsgWaitForMultipleObjectsEx 将变更通知混入到窗口消息中。但由于对话框有自己的消息循环,当对话框显示的时候,通知便无法处理了。于是这个好主意被现实无情的碾碎了。

 

  -------------------------------------------------------------------------------------------------------------------------------------------------------

但是不管问题有多大,我还是介绍一下这个API以便于在单线程下奋斗的小伙子们使用:

          

BOOL WINAPI ReadDirectoryChangesW(
  __in          HANDLE hDirectory,//指向监控目录的句柄,可以用CreatFile生成
  __in_out      LPVOID lpBuffer,//存储修改信息的首地址
  __in          DWORD nBufferLength,//分配的存储修改信息的空间的长度
  __in          BOOL bWatchSubtree,//TRUE则监控子目录,FALSE则不监控
  __in          DWORD dwNotifyFilter,//通知条件
  __out         LPDWORD lpBytesReturned,//该函数返回信息的字节数,也就是存到lpBuffer中的内容的字节数
  __in          LPOVERLAPPED lpOverlapped,//一个指向OVERLAPPED结构的指针,他在同步调用时提供数据供使用,否则他就为NULL
  __in          LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine//当操作结束或被取消或者线程进入等待状态时的一个指向将被调用操作的指针
);
返回值:如果函数成功,返回值就是非0。对于同步调用,这意味着操作成功,对于异步调用,这意味着操作成功地排队。如果函数失败,返回值是0。如果操作目录或文件系统不支持这个操作,函数将返回ERROR_INVALID_FUNCTION,可以使用GetLastError()函数获取。

PS:下面是一个关于ReadDirectoryChangesW()函数的使用例子.添加主函数可以使用,效果一般,不建议直接在项目中使用。

#include 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;
//ThreadParameter结构体的定义
/*
*整合参数为一个结构体传给子线程的原因在于创建线程时
*指定的线程入口函数只接受一个LPVOID类型的指针,具体内容可以参考msdn,
*实际上向子线程传递参数还有一种方法,全局变量,
*例如后面程序中的WriteLog就是一个文件输入流对象,我就是在程序首部定义的全局变量。
*/
typedef struct ThreadParameter
{
	LPTSTR in_directory;//监控的路径
	FILE_NOTIFY_INFORMATION *in_out_notification;//存储监控函数返回信息地址
	DWORD in_MemorySize;//传递存储返回信息的内存的字节数
	DWORD *in_out_BytesReturned;//存储监控函数返回信息的字节数
	DWORD *in_out_version;//返回版本信息
	FILE_NOTIFY_INFORMATION *temp_notification;//备用的一个参数
}ThreadParameter;

//  函数: WatchChanges(LPVOID lpParameter)
//
//  目的: 监控目录的程序
//
//  注释:主函数创建线程时制定了这个函数的入口
//		 届时该子程序将自动启动执行。
//  备注:因为代码不全,看下面的代码时,主要参考红色的字体部分

DWORD WINAPI WatchChanges(LPVOID lpParameter)//返回版本信息
{
	ThreadParameter *parameter = (ThreadParameter*)lpParameter;
	LPCTSTR WatchDirectory = parameter->in_directory;//


													 //创建一个目录句柄
	HANDLE handle_directory = CreateFile(WatchDirectory,
		FILE_LIST_DIRECTORY,
		FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
		NULL,
		OPEN_EXISTING,
		FILE_FLAG_BACKUP_SEMANTICS,
		NULL);
	if (handle_directory == INVALID_HANDLE_VALUE)
	{
		DWORD ERROR_DIR = GetLastError();
		MessageBox(NULL, TEXT("打开目录错误!"), TEXT("文件监控"), 0);
	}


	BOOL watch_state;


	while (TRUE)
	{
		watch_state = ReadDirectoryChangesW(handle_directory,
			(LPVOID)parameter->in_out_notification,
			parameter->in_MemorySize,
			TRUE,
			FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_LAST_WRITE,
			(LPDWORD)parameter->in_out_BytesReturned,
			NULL,
			NULL);
		//printf("########%d########\n",watch_state);//检测结果为1,表明函数已成功执行
	///	time(&ChangeTime);//记录修改时间


		if (GetLastError() == ERROR_INVALID_FUNCTION)
		{
			MessageBox(NULL, TEXT("系统不支持!"), TEXT("文件监控"), 0);
		}
		else if (watch_state == 0)
		{
			MessageBox(NULL, TEXT("监控失败!"), TEXT("文件监控"), 0);
		}
		else if (GetLastError() == ERROR_NOTIFY_ENUM_DIR)
		{
			MessageBox(NULL, TEXT("内存溢出!"), TEXT("文件监控"), 0);
		}
		else
		{

			//将宽字符类型的FileName变量转换成string,便于写入log文件,否则写不进去正确的文件名
			string file_name;
			DWORD length = WideCharToMultiByte(0, 0, parameter->in_out_notification->FileName, -1, NULL, 0, NULL, NULL);
			PSTR ps = new CHAR[length];
			if (length >= 0)
			{
				WideCharToMultiByte(0, 0, parameter->in_out_notification->FileName, -1, ps, length, NULL, NULL);
				file_name = string(ps);
				delete[] ps;
			}

			//这里主要就是检测返回的信息,需要注意FILE_NOTIFY_INFORMATION结构体的定义,以便正确调用返回信息

			if (parameter->in_out_notification->Action == FILE_ACTION_ADDED)
			{
				cout<< "新增文件 : " << file_name << "\n" << flush;
			}
			if (parameter->in_out_notification->Action == FILE_ACTION_REMOVED)
			{
				cout << "删除文件 : " << file_name << "\n" << flush;
			}
		

			//对于下面两种情况,Action本身也是文件名(可能是old_name也可能是new_name)
			if (parameter->in_out_notification->Action == FILE_ACTION_RENAMED_OLD_NAME)
			{
				cout << "重命名\"" << file_name << "\"文件\n" << flush;
			}
			if (parameter->in_out_notification->Action == FILE_ACTION_RENAMED_NEW_NAME)
			{
				cout << "重命名\"" << file_name << "\"文件为\"" << parameter->in_out_notification->Action << "\"\n" << flush;

			}
			(*(parameter->in_out_version))++;
			memset(parameter->in_out_notification, '\0', 1024);

		}
		Sleep(500);
	}
	return 0;
}
3.FileSystemWatcher

   其实关于这个类,我最开始听说是在一个搞网络的朋友那里,他前段时间有做一个监控档案系统的修改,是多线程并发的监控到某一个目录下档案信息的改变。然后这个是使用公共语言运行时CLR.net的一个产品,我就花了一些时间在这个上面,主要是参看用MSDN手册,在里面有很详细的解释——https://msdn.microsoft.com/zh-cn/library/system.io.filesystemwatcher(v=vs.110).aspx

1.常用的几个基本属性

(1) Path :设置要监视的目录的路径。

(2) IncludeSubdirectories :设置是否级联监视指定路径中的子目录。

(3) Filter :设置筛选字符串,用于确定在目录中监视哪些类型的文件。

(4) NotifyFilter :设置文件的哪些属性的变动会触发Changed事件,同时监控多个属性变动可以按“或”组合。(默认值为 NotifyFilter.LastWrite | NotifyFilter.FileName | NotifyFilter.DirectoryName 组合)

子项: Attributes           --  文件或文件夹的属性。  
             CreationTime   --  文件或文件夹的创建时间。  
             DirectoryName --  目录名。(常用) 
             FileName          --  文件名。 (常用)
             LastAccess       --  文件或文件夹上一次打开的日期。  
             LastWrite           --  上一次向文件或文件夹写入内容的日期。  
             Security              --  文件或文件夹的安全设置。  
             Size                    --   文件或文件夹的大小。 (常用)

(5) EnableRaisingEvents :设置是否开始监控。(默认为false)

2.常用事件

(1) Changed  :当更改文件和目录时发生,可以通过NotifyFilter属性设置触发该事件的需要文件更改的属性。

(2) Created  :  当创建文件和目录时发生。 

(3) Deleted :    删除文件或目录时发生。 

(4) Renamed  :重命名文件或目录时发生。

(5) FileSystemEventArgs 对象:


PS:下面是我具体实现的代码:

using namespace System;
using namespace msclr::interop;
using namespace System::IO;
using namespace System::Security::Permissions;
using namespace System::Runtime::InteropServices;
ref class FSEventHandler
{
private:
	
	void OnChanged(Object^ source, FileSystemEventArgs^ e)
	{

		char* path = (char*)(Marshal::StringToHGlobalAnsi(e->FullPath)).ToPointer();
		std::cout<Path = MonitorPath;

		//设置缓冲区大小1M,根据需求设置
		fsWatcher->InternalBufferSize = 1048576;

		//监听文件、目录、文件大小的改变
		fsWatcher->NotifyFilter = NotifyFilters::FileName | NotifyFilters::DirectoryName | NotifyFilters::Size;

		//监听子目录
		fsWatcher->IncludeSubdirectories = true;

		//添加事件处理程序
		FSEventHandler^ handler = gcnew FSEventHandler();
		fsWatcher->Changed += gcnew FileSystemEventHandler(
			handler, &FSEventHandler::OnChanged);

		//开始监听
		fsWatcher->EnableRaisingEvents = true;
		
		system("pause");//把父进程阻塞起来,防止负责检测的子进程死掉
		return 0;

	}
};
但是,这样做同样出现了很多问题:

1.首先是在做类似拷贝这样的操作的时候,监控事件create会监控到多次

2.监控FTP的下载产物的时候会把tmp文件也录入进去

限于篇幅,在下一篇博客中写我的解决办法。

  




                               

你可能感兴趣的:(windows环境下C++实现的多种目录监控和优劣解析)