首页 > 代码库 > 【大话QT之五】Windows与Linux下文件操作监控的实现

【大话QT之五】Windows与Linux下文件操作监控的实现

一、需求分析:

        随着渲染业务的不断进行,数据传输渐渐成为影响业务时间最大的因素。究其原因就是因为数据传输耗费较长的时间。于是,依托于渲染业务的网盘开发逐渐成为迫切需要解决的需求。该网盘的实现和当前市场上网盘实现有一些的不同,主要在客户端与服务器端的操作需要双向进行,即:用户在客户端的操作需要及时同步到服务器端;在服务器端作业渲染生成的文件要及时同步到客户端。即:用户不在需要单独的下载数据,而是在作业运行的同时,渲染就过就会自动同步到客户端,大大缩短了等待时间。当然,无论是在客户端还是在服务端都面临着一个问题,即:实现对文件操作的监控,这里的文件操作包括:文件(夹)创建、文件(夹)删除、文件(夹)重命名、文件(夹)移动等操作。除此之外还要能够同步客户端文件的修改操作,即:当用户退出网盘后,修改了原有同步目录中的文件,当用户再次启动网盘时通过一次扫描与md5值的比较能缺确定出哪些文件发生了改动,并将改动后的操作及时同步到服务端。这里,先将Windows(客户端实现需要)与Linux(服务端实现需要)下文件监控的实现方法简要概述。

二、文件监控实现方法分析

      1> Windows下文件监控的实现

            Windows下实现文件监控的原理是利用SHChangeNotifyRegister把指定的窗口添加到系统的消息监视链中,从而注册窗口就可以接收到来自文件系统或Shell的通知了。在继续向下说明之前,需要解释一下Windows外壳名字空间(Shell Name Space)的概念。

            外壳名字空间是Windows下的标准文件系统,它大大扩展了Dos文件系统,形成了以”桌面“为根的单一文件系统树,原有的C、D盘等目录树变成了“我的电脑”这一外壳名字控件子树的下一级子树,而像“控制面板”、“回收站”、“网上邻居”等应用程序以及“打印机”等设备也被虚拟成了外壳名字空间中的节点。为了区别于Dos中“目录”的概念,Windows引入了“文件夹”的概念。“文件夹”一般是指外壳名字空间树中的非叶几点,既可以是DOS下的目录,也可以是“控制面板”、“回收站”这类虚拟的目录。

            新的“路径”PIDL:外壳对象标识符列表。PIDL是一个元素类型为ITEMIDLIST结构的数组,数组中元素的个数是未知的,但紧接着数组的末尾的必是一个双字节的零。每个数组元素代表了外壳名字空间树中的一层(即一个文件夹或文件),数组中的前一元素代表的是后一元素的父文件夹。由此可见,PIDL实际上就是指向一块由若干个顺序排列的ITEMIDLIST结构组成、并在最后有一个双字节零的空间的指针。所以PIDL的类型就被Windows定义为ITEMIDLIST结构的指针。

            DOS中的路径是一个字符串,但PIDL是一种二进制结构,所以我们不能直接从PIDL中获知它所代表的到底是哪个文件夹或文件,而必须调用相应的函数把它转换为代表路径的字符串。如果某绝对PIDL是文件系统的一部分,则调用SHGetPathFromIDList函数即可;但如不是,就无法获得路径字符串了,因为DOS中根本就不存在这种路径。

       总体来说:实现Windows下文件监控的基本流程如下:

              

       上图是在windows下利用C++实现文件监控的一种方式(根据需要灵活改动),这里简要叙述一下:1) 利用DialogBoxParam创建一个模态对话框,进入窗口过程函数,在该窗口函数中根据各种消息来完成我们的操作。这里模态对话框与非模态对话框的区别之一是因为它有一套自己的消息泵机制,不需要我们再手动写消息的接收了(非模态对话框要自己接收消息)。拦截用户消息,根据各个不同的阶段可以加入我们自己的操作,比如初始化等。2) 获取指定路径的PIDL:即目标路径的外壳对象标识符,有了它才能继续后续的处理,这里的获取有两种方式,一种是利用IFileDialog打开对话框让用户选择(IFileDialog *pfd),从而通过GetResult(IShellItem *psi ; pfs->GetResult(&psi))获取IShellItem对象;然后利用QueryInterface(IShellItem2 *_psiDrop ; psi->QueryInterface(&_psiDrop))获取IShellItem2对象;最后利用它就可以获得PIDL了(利用SHGetIDListFromObject)。3) 最后利用SHChangeNotifyRegister完成最终目标窗体的挂载,从而将一个目录加入到系统的消息链中,从而可以获取文件系统或Shell中关于文件操作的相关信息。最后将信息解析出来就可以了。

       还有一种方式即直接提供目标文件夹的绝对路径,由该路径获取到PIDL,从而将窗体挂载到系统消息链中,注意:如果是在QT中实现的话,可以很轻松的获取到QWidget的窗口句柄。关键代码如下:

void houqd::RegisterWindow()
{
	char absoluteFolderPath[] = "C:\\openssl";


	//! 由文件夹的绝对路径获取PIDL:外壳对象标识符列表,即在windows 外壳名字空间 "Shell Name Space"中的表示方法。
	LPITEMIDLIST myFolderPIDL = ParsePidlFromPath(absoluteFolderPath);

	HRESULT res ;
	IShellItem *psi = NULL;

	//! 创建一个IShellItem(interface)对象,IShellItem interface提供了查找一个关于Shell Item相关信息的方法。
	//! IShell Item接口都继承自IUnknown interface
	res = SHCreateShellItem(NULL, NULL, myFolderPIDL, &psi);
	
	IShellItem2 *ppsi ;

	//! 检索一个对象上支持的接口的指针
	psi->QueryInterface(&ppsi);

	
	//! =======================================注册文件监控=============================================
	PIDLIST_ABSOLUTE pidlWatch;
	HRESULT hr = SHGetIDListFromObject(ppsi, &pidlWatch);

	if(SUCCEEDED(hr))
	{
		SHChangeNotifyEntry const entries[] = { pidlWatch, true };

		int const nSources = SHCNRF_ShellLevel | SHCNRF_InterruptLevel | SHCNRF_NewDelivery;

		//! 注册窗口主函数
		_ulRegister = SHChangeNotifyRegister(_hdlg, nSources, SHCNE_ALLEVENTS, c_notifyMessage, ARRAYSIZE(entries), entries);
		hr = _ulRegister != 0 ? S_OK : E_FAIL;
	}

	//ShowWindow(SW_HIDE);
	//! ====================================================================================
}
ParsePidFromPath 的具体实现如下:

LPITEMIDLIST houqd::ParsePidlFromPath(LPCSTR lpszPath)
{
	//存放以Unicode内码表示的路径字符串的缓冲区
	 OLECHAR szOleChar[MAX_PATH];

	 //“桌面“的IshellFolder接口指针
	 LPSHELLFOLDER lpsfDeskTop;

	 //返回的PIDL
	 LPITEMIDLIST lpifq;

	 ULONG ulEaten, ulAttribs;
	 HRESULT hres;

	 //得到“桌面”的IshellFolderr 接口指针
	 SHGetDesktopFolder(&lpsfDeskTop);

	 //将Ansi字符集的路径字符串转换成Unicode字符串,存入szOleChar
	 MultiByteToWideChar(CP_ACP,MB_PRECOMPOSED, lpszPath, -1, szOleChar, sizeof(szOleChar));

	 //将szOleChar,中的路径径字符串翻译成相应的PIDL,存入lpifq
	 hres = lpsfDeskTop->ParseDisplayName(NULL, NULL, szOleChar, &ulEaten, &lpifq, &ulAttribs);

	 hres = lpsfDeskTop->Release();

	 //如果翻译失败,则返回NULL

	 if(FAILED(hres))
	  return NULL;

	 return lpifq;
}

      2> Linux下文件监控的实现

        Linux下主要是通过inotify实现文件监控。它是一个内核用户通知用户空间程序文件系统变化的机制。在用户状,inotify通过三个系统调用和在返回的文件描述符上的文件I/O操作来使用.

        1) 使用inotify的第一步是创建inotify的实例:int fd = inotify_init() ; 每一个inotify实例对应一个独立的排序的队列。

        2) int wd = inotify_add_watch(fd , file_dir_path , mask);添加一个目录的监控。

        3) 删除一个监控:inotify_rm_watch(fd , wd);

       关键代码如下:

if (m_InotifyFd != LHFSMC_FD_UNCREATED_STATE)
        close(m_InotifyFd);

    //! inotify_init()
    if ((m_InotifyFd = inotify_init()) < 0)
    {
        qDebug() << "[error] LHFileSystemMonitor::Start: inotify_init failure.";
        return 0;
    }

    //! 为m_CreatedDirList中所有保存的目录创建监控
    if(!CreateWatcherForEachDir(m_CreatedDirList))
    {
        qDebug() << "[error] LHFileSystemMonitor::Start: CreateNotifierForEachDir failure.";
        return 0;
    }
CreateWatcherForEachDir的实现:

int LHFileSystemMonitor::CreateWatcherForEachDir(QStringList &dirLocationList)
{
#ifndef WIN32
    for (QStringList::const_iterator iter = dirLocationList.begin();
         iter != dirLocationList.end();
         ++iter)
    {
        int watchDescriptor;

        if ((watchDescriptor = CreateWatcher(iter->toStdString().c_str())) > 0)
            m_MonitoredObjectList.push_back(LHFSMonitorData(*iter, watchDescriptor));
        else
            qDebug() << "[error] LHFileSystemMonitor::CreateWatcherForEachDir: CreateWatcher for"
                     << *iter << "failure";
    }
#endif
    return 1;
}
CreateWatcher的实现:

int LHFileSystemMonitor::CreateWatcher(const char *fileLocation) const
{
    if (fileLocation == NULL)
        return 0;

    QDir dir(fileLocation);

    if (!dir.exists())
    {
        qDebug() << "[error] LHFileSystemMonitor::CreateNotifier:" << fileLocation << "is not exist!";
        return 0;
    }

    return ((m_InotifyFd != LHFSMC_FD_UNCREATED_STATE) ? inotify_add_watch(m_InotifyFd, fileLocation, LHFSMC_MONITOR_EVENT) : -1);
}
总结:

        以上就是Windows以及Linux下文件监控系统实现的相关思路及代码,仅仅作为一个引入,利用这种方式均可以实现对应的功能。

【大话QT之五】Windows与Linux下文件操作监控的实现