首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
105 阅读
2
nvim番外之将配置的插件管理器更新为lazy
78 阅读
3
2018总结与2019规划
62 阅读
4
PDF标准详解(五)——图形状态
40 阅读
5
为 MariaDB 配置远程访问权限
33 阅读
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
Thinking
FIRE
菜谱
登录
Search
标签搜索
c++
c
学习笔记
windows
文本操作术
编辑器
NeoVim
Vim
win32
VimScript
emacs
linux
文本编辑器
Java
elisp
反汇编
OLEDB
数据库编程
数据结构
内核编程
Masimaro
累计撰写
314
篇文章
累计收到
31
条评论
首页
栏目
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
Thinking
FIRE
菜谱
页面
归档
友情链接
关于
搜索到
42
篇与
的结果
2017-06-11
windows 下文件的高级操作
本文主要说明在Windows下操作文件的高级方法,比如直接读写磁盘,文件的异步操作,而文件普通的读写方式在网上可以找到一大堆资料,在这也就不再进行专门的说明。判断文件是否存在在Windows中并没有专门提供判断文件是否存在的API,替代的解决方案是使用函数GetFileAttributes,传入一个路径,如果文件不存在,函数会返回INVALID_FILE_ATTRIBUTES,这个时候一般就可以认为文件不存在。更严格一点的,可以在返回INVALID_FILE_ATTRIBUTES之后调用GetLastError函数,判断返回值是否为ERROR_FILE_NOT_FOUND或者ERROR_PATH_NOT_FOUND(这个值适用于判断目录)下面是它的实例代码BOOL IsFileExist(LPCTSTR pFilePath) { DWORD dwRet = GetFileAttributes(pFilePath); if(INVALID_FILE_ATTRIBUTES == dwRet) { dwRet = GetLastError(); if (ERROR_FILE_NOT_FOUND == dwRet || ERROR_PATH_NOT_FOUND == dwRet) { return FALSE; } } return TRUE; }文件查找和目录遍历这个操作主要使用到了下面几个API函数:FindFirstFile:建立一个指定搜索条件的搜索句柄,函数原型如下:HANDLE FindFirstFile( LPCTSTR lpFileName, LPWIN32_FIND_DATA lpFindFileData ); 第一个参数是一个搜索起始位置路劲的字符串,但是这个字符串的格式为“路径+特定文件的通配符”这样它会以这个路径作为起始路径,依次查找到目录中文件名符合通配符的文件,比如"c:\."会返回c盘下的所有文件,而"c:\"直接返回错误,"c:\a*.txt"会返回c盘中以a开头的txt文件FindNextFile:搜索符合条件的下一项,在循环中调用它的话,它会依次返回符合FindFirstFile要求的文件信息和所有子目录新消息FindClose:关闭搜索句柄FindFirstFile和FindNextFile返回的文件信息结构为WIN32_FIND_DATA,它的定义如下:typedef struct _WIN32_FIND_DATA { DWORD dwFileAttributes; //文件属性 FILETIME ftCreationTime; //创建时间 FILETIME ftLastAccessTime; //最后访问时间 FILETIME ftLastWriteTime; //最后修改时间 DWORD nFileSizeHigh; DWORD nFileSizeLow; //这两个值是一个64位的文件大小的高32位和低32位 DWORD dwOID; TCHAR cFileName[MAX_PATH]; //文件名称 } WIN32_FIND_DATA; 一般在遍历的时候首先判断文件属性,如果为FILE_ATTRIBUTE_DIRECTORY(是个目录),并且文件名称不为".",".."则递归调用遍历函数遍历它的子目录,但是一定要记得进行文件路径的拼接,如果不为目录,这个时候一般就是普通文件,这个时候可以选择进行打印(遍历文件目录)或者比较文件名称与需要查找的名称是否相同(查找文件)。下面是一个全盘搜索特定文件名的实例代码:void FindFileByPath(LPCTSTR pszSearchEntry, LPCTSTR pszFileName) { WIN32_FIND_DATA fd = {0}; TCHAR szFilePath[MAX_PATH] = _T(""); StringCchCat(szFilePath, MAX_PATH, pszSearchEntry); StringCchCat(szFilePath, MAX_PATH, _T("*.*")); HANDLE hSearch = FindFirstFile(szFilePath, &fd); if (INVALID_HANDLE_VALUE == hSearch) { return; } do { if ((fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) && _tcscmp(fd.cFileName, _T(".")) != 0 && _tcscmp(fd.cFileName, _T("..")) != 0) { TCHAR szSubDir[MAX_PATH] = _T(""); StringCchCat(szSubDir, MAX_PATH, pszSearchEntry); StringCchCat(szSubDir, MAX_PATH, fd.cFileName); StringCchCat(szSubDir, MAX_PATH, _T("\\")); FindFileByPath(szSubDir, pszFileName); }else { if (_tcscmp(fd.cFileName, pszFileName) == 0) { TCHAR szFullPath[MAX_PATH] = _T(""); StringCchCat(szFullPath, MAX_PATH, pszSearchEntry); StringCchCat(szFullPath, MAX_PATH, _T("\\")); StringCchCat(szFullPath, MAX_PATH, fd.cFileName); printf("full path:%ws\n", szFullPath); return; } } ZeroMemory(&fd, sizeof(fd)); } while (FindNextFile(hSearch, &fd)); } void FindFile(LPCTSTR pFileName) { TCHAR szVolumn[MAX_PATH] = _T(""); GetLogicalDriveStrings(MAX_PATH, szVolumn); LPCTSTR pVolumnName = szVolumn; while (_tcscmp(pVolumnName, _T("")) != 0) { FindFileByPath(pVolumnName, pFileName); //偏移到下一个盘符的字符串位置 size_t nLen = 0; StringCchLength(pVolumnName, MAX_PATH, &nLen); nLen++; pVolumnName += nLen; } }由于这段代码会遍历整个磁盘,查找所有具有相同文件名称的文件,所以当某个逻辑分区的文件结构比较复杂的时候,可能执行效果比较慢。这段代码出现了两个函数,第一个函数是真正遍历文件的函数,由于FindFirst函数需要传入一个入口点,所以在需要进行全盘遍历的时候提供了另外一个函数来获取所有磁盘的逻辑分区名。获取所有逻辑分区名调用函数GetLogicalDriveStrings,这个函数会返回一个含有所有分区名称的字符串,每个分区名称之间以"\0"分割,所以在获取所有名称的时候需要自己进行字符串指针的偏移操作在遍历的时候为了要遍历所有文件及目录搜索的统配符应该匹配所有文件名称。另外FindFirst也会返回一个文件信息的结构,这个结构是当前目录中符合条件的第一个文件信息,在遍历的时候不要忘记也取一下它返回的文件信息。最后当文件为目录的时候需要判断它是否为当前目录或者当前目录的父目录,也就是是否为"."和"..",这段代码有一点不足就是不支持通配符,必须输入文件名的全称目录变更监视一般像notepad++等文本编辑器都会提供一个功能,就是在它们打开了一个文本之后,如果文本被其他程序更改,那么它们会提示用户是否需要重新载入,这个功能的实现需要对文件进行监控,windows中提供了一套API用于监控目录变更使用函数FindFirstChangeNotification创建一个监控句柄,该函数原型如下:HANDLE FindFirstChangeNotification( LPCTSTR lpPathName, BOOL bWatchSubtree, DWORD dwNotifyFilter);第一个参数是一个目录的字符串,表示将要监控哪个目录,注意这里必须穿入一个目录,不能穿文件路径第二个参数是一个bool类型,表示是否监控目录中的整个目录树第三个参数是监控的时间类型,如果要监控目录中的文件的改动,可以使用FILE_NOTIFY_CHANGE_LAST_WRITE 标记,该标记会监控文件的最后一次写入,其他类型请查阅MSDN创建监控句柄后使用Wait函数循环等待监控句柄,如果目录中发生对应的事件,wait函数返回,这个时候可以对比上次目录结构得出哪个文件被修改,做相应的处理后调用FindNextChangeNotification函数传入监控句柄,继续监控下一次变更。最后当我们不需要进行监控的时候调用FindCloseChangeNotification关闭监控句柄void WatchDirectoryChange(LPCTSTR lpDir) { HANDLE hChangNotify = FindFirstChangeNotification(lpDir, FALSE, FILE_NOTIFY_CHANGE_LAST_WRITE ); if (hChangNotify == INVALID_HANDLE_VALUE) { printf("FindFirstChangeNotification function faild!\n"); return ExitProcess(GetLastError()); } while (TRUE) { printf("wait for change notify.......\n"); if(WAIT_OBJECT_0 == WaitForSingleObject(hChangNotify, INFINITE)) { printf("some file be changed in this directory\n"); } FindNextChangeNotification(hChangNotify); } FindCloseChangeNotification(hChangNotify); }如果嫌这个方法比较麻烦的话,为了实现这个功能,Windows专门提供了一个函数ReadDirectoryChangesW,就跟他的名字一样他只能用于UNICODE平台,这个函数不存在ANSI版本,所以在ANSI版本时需要进行字符串的转化操作。函数原型如下:BOOL WINAPI ReadDirectoryChangesW( __in HANDLE hDirectory, //需要监控的目录的句柄,这个句柄可以用CreateFile打开 __out LPVOID lpBuffer, //函数返回信息的缓冲 __in DWORD nBufferLength, //缓冲区的长度 __in BOOL bWatchSubtree, //是否监控它的子目录 __in DWORD dwNotifyFilter, //监控的事件 __out_opt LPDWORD lpBytesReturned, //实际返回数据长度 __inout_opt LPOVERLAPPED lpOverlapped, //异步调用时的OVERLAPPED结构 __in_opt LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine //异步调用时的APC函数);这个函数它的原理就类似于上面的三个函数,如果是同步操作,当需要监控的目录发生指定的事件时函数返回,并将监控得到的信息填充到结构体中,它会将数据以FILE_NOTIFY_INFORMATION结构的形式返回。该结构的定义如下:typedef struct _FILE_NOTIFY_INFORMATION { DWORD NextEntryOffset; DWORD Action; DWORD FileNameLength; WCHAR FileName[1]; } FILE_NOTIFY_INFORMATION, *PFILE_NOTIFY_INFORMATION;这个结构体中存储文件名称的成员为FileName,这个成员只是起到一个变量名称标识的作用,在存储文件名称时用到了越界访问的方式,所以定义缓冲的大小一定要大于这个结构,让其有足够的空间容纳FileName这个字符串。结构体中的Action表示当前发生了何种操作,具体的类型可以参考MSDN,它的意思根据字面的单词很容易理解下面是使用它的具体代码:void WatchFileChange(LPCTSTR lpFilePath) { DWORD cbBytes; char notify[1024]; HANDLE dirHandle = CreateFile(lpFilePath,GENERIC_READ | GENERIC_WRITE | FILE_LIST_DIRECTORY, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL); if(dirHandle == INVALID_HANDLE_VALUE) //若网络重定向或目标文件系统不支持该操作,函数失败,同时调用GetLastError()返回ERROR_INVALID_FUNCTION { cout<<"error"+GetLastError()<<endl; } memset(notify,0,strlen(notify)); FILE_NOTIFY_INFORMATION *pnotify = (FILE_NOTIFY_INFORMATION*)notify; cout<<"start...."<<endl; while(true) { if(ReadDirectoryChangesW(dirHandle,¬ify,1024,true, FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_SIZE, &cbBytes,NULL,NULL)) { //设置类型过滤器,监听文件创建、更改、删除、重命名等 switch(pnotify->Action) { case FILE_ACTION_ADDED: _tprintf(_T("add file: %s\n"), pnotify->FileName); break; case FILE_ACTION_MODIFIED: _tprintf(_T("modify file:%s\n"), pnotify->FileName); break; case FILE_ACTION_REMOVED: _tprintf(_T("file removed %s\n"), pnotify->FileName); break; case FILE_ACTION_RENAMED_OLD_NAME: _tprintf(_T("file renamed:%s\n"), pnotify->FileName); break; default: cout<<"unknow command!"<<endl; } } } CloseHandle(dirHandle); }这段代码很容易理解,但是需要注意几点:之前说过的分配的缓冲一定要大于FILE_NOTIFY_INFORMATION 结构这个函数也是用来监控目录的,所以这里要传入一个目录路径,不能传入文件路径在使用CreateFile来打开目录的时候这个函数要求传入的文件句柄必须要以FILE_LIST_DIRECTORY标识打开,否则在调用的时候会报“参数错误”这个错文件映射Windows中,文件映射是文件内容到进程的虚拟地址空间的映射,这个映射称之为File Mapping,文件内容的拷贝就是文件视图(File View),从内存管理的角度来看,文件映射只是将磁盘的真实地址通过页表映射到进程的虚拟地址空间中,读写这段虚拟地址空间其实就是在读写磁盘。而文件视图就是将文件中的内容整个读到内存中,并将这段虚拟地址空间与真实物理内存对应。最终在关闭整个文件映射的时候如果存在文件视图,操作系统会将视图中的内容写会到磁盘,其实也就是简单的进行了下物理内存到磁盘的页面交换,从内存管理的角度来看,文件映射其实就是操作系统将磁盘上的数据与物理内存之间的页面交换,操作系统在二者之间来回倒腾数据而已文件映射本身是一个内核对象,操作系统在内核中维护了一个相关的数据结构,这个结构中记录了被映射到虚拟地址空间中的起始地址和被映射的数据的大小。由于内核对象的数据结构是在内核中被维护,而内核被所有进程共享,所以从理论上将不同的进程是可以共享同一个内核对象的,虽然它们的对象句柄会在不同进程中呈现不同的值,但是在内核中,却是指向同一个结构,那么虽然不同进程的文件映射对象不同,但是通过寻址得到的物理内存肯定是同一个,所以这就提供了另一种进程间共享内存的方法——文件映射。创建文件映射主要使用函数CreateFileMapping,这个函数第一个参数是一个文件句柄,这个句柄可以是一个真实存在在磁盘上的文件,这样创建的文件映射最终就是将磁盘中的数据映射到进程的虚拟地址空间,也可以传入一个INVALID_HANDLE_VALUE,这个时候也会返回成功,传入INVALID_HANDLE_VALUE一般是用来在进程间共享内存的。注意:这个函数只是创建了一个内核对象并返回它的句柄,并没有进行内存映射的相关操作。同时由于它第一个句柄参数可以填INVALID_HANDLE_VALUE,在使用CreateFile函数后一定要注意校验,不然可能看到CreateFileMapping函数返回的是一个有效句柄,但是并没有成功创建这个文件的映射然后调用MappingViewOfFile函数,将对应文件与一段进程的虚拟地址空间关联并将文件映射到内存,也就是将磁盘文件中的数据交换到物理内存中当我们不使用这块真实内存的时候,调用UnMapViewOfFile将内存中的数据交换到磁盘,最终使用文件映射完毕后,调用CloseHandle关闭所有句柄使用文件映射一般有几个好处:针对文件来说,文件映射本质上是磁盘到物理内存之间的页面交换,由操作系统的内存管理机制统一调度,效率比一般的文件读写要高,而且在使用完毕后,操作系统会自动的将内存中的数据写到磁盘中,不用手动的更新文件针对不同进程来说,使用文件映射来共享内存本质上是在使用同样一块内存,相比于管道油槽等方式传输数据来说显得更为高效下面通过几个例子来说明在这两种情况下使用文件映射void GetFileNameByHandle(HANDLE hFile) { HANDLE hMapping = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL); if (INVALID_HANDLE_VALUE == hMapping) { _tprintf(_T("create file mapping error\n")); return; } LPVOID lpMappingMemeory = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 1); if (NULL == lpMappingMemeory) { _tprintf(_T("MapViewOfFile error\n")); return; } TCHAR szFileName[MAX_PATH] = _T(""); if(0 == GetMappedFileName(GetCurrentProcess(), lpMappingMemeory, szFileName, MAX_PATH)) { _tprintf(_T("GetMappedFileName error\n")); return; } TCHAR szTemp[MAX_PATH] = _T(""); GetLogicalDriveStrings(MAX_PATH, szTemp); TCHAR szDriver[4] = _T(" :"); LPCTSTR p = szTemp; while (*p != _T('\0')) { *szDriver = *p; TCHAR szName[MAX_PATH] = _T(""); QueryDosDevice(szDriver, szName, MAX_PATH); size_t nPathLen = 0; StringCchLength(szName, MAX_PATH, &nPathLen); if(CSTR_EQUAL == CompareString(LOCALE_USER_DEFAULT, NORM_IGNORECASE, szName, nPathLen, szFileName, nPathLen)) { TCHAR szFullPath[MAX_PATH] = _T(""); StringCchCopy(szFullPath, MAX_PATH, p); //在这使用文件带卷名的字符串首地址 + 卷名长度 + 1(+1是为了偏移到卷名后面的"\"的下一个字符,因为这个盘符中自己带了"/"字符) StringCchCat(szFullPath, MAX_PATH, szFileName + nPathLen + 1); _tprintf(_T("文件全路径:%s"), szFullPath); break; } size_t dwLen = 0; StringCchLength(p, MAX_PATH, &dwLen); p = p + dwLen + 1; } UnmapViewOfFile(lpMappingMemeory); CloseHandle(hMapping); return; }该函数利用文件映射的方式,通过一个文件的句柄获取它的绝对路径。该函数首先根据文件句柄创建一个文件映射并调用GetMappedFileName获取文件的全路径,但是获取到的是类似于“\Device\HarddiskVolume6\Program\FileDemo\FileMapping\FileMapping.cpp”这样的卷名加上文件的相对路径,而不是我们常见的类似于C D E这样的盘符名称,所以为了获取对应的盘符,使用的方式是利用GetLogicalDriverString函数来获取系统所有逻辑卷的盘符,然后调用QueryDosDevice函数将盘符转化为卷名,再与之前获取到的路径中的卷名进行比较,在这使用了一个技巧,就是首先获取卷名对应的长度,然后调用比较函数时传入卷名的长度让其只比较卷名对应的字符,如果相同,就找到了卷名对应的盘符名称,最后将卷名与在卷中的相对路径进行拼接就得到了它的文件全路径。下面来看一个使用文件映射在不同进程间共享内存的例子//Process A #define BUFF_SIZE 1024 int _tmain(int argc, _TCHAR* argv[]) { TCHAR szHandleName[] = _T("Global\\ShareMemMapping"); HANDLE hMapping = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, BUFF_SIZE, szHandleName); if (INVALID_HANDLE_VALUE == hMapping) { printf("create file mapping error\n"); return GetLastError(); } LPVOID pMem = MapViewOfFile(hMapping, FILE_MAP_ALL_ACCESS, 0, 0, BUFF_SIZE); if (NULL == pMem) { printf("MapViewOfFile Error\n"); return GetLastError(); } ZeroMemory(pMem, BUFF_SIZE); TCHAR pszData[] = _T("this is written by process A"); CopyMemory(pMem, pszData, sizeof(pszData)); _tsystem(_T("PAUSE")); UnmapViewOfFile(pMem); CloseHandle(hMapping); return 0; }#define BUFF_SIZE 1024 int _tmain(int argc, _TCHAR* argv[]) { TCHAR szHandleName[] = _T("Global\\ShareMemMapping"); HANDLE hMapping = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, szHandleName); if (INVALID_HANDLE_VALUE == hMapping) { printf("OpenFileMapping"); return GetLastError(); } LPCTSTR pMem = (LPCTSTR)MapViewOfFile(hMapping, FILE_MAP_ALL_ACCESS, 0, 0, BUFF_SIZE); if (NULL == pMem) { printf("MapViewOfFile Error\n"); return GetLastError(); } printf("read date: %ws\n", pMem); _tsystem(_T("PAUSE")); UnmapViewOfFile(pMem); CloseHandle(hMapping); return 0; } 在上面的例子中,进程A做了如下工作:创建一个命名的文件映射对象构建文件映射的视口,并写入一段内存等待关闭相关句柄在进程B中做了如下工作:打开之前A创建的文件映射对象构建文件映射的视口,读取内存关闭相关句柄在使用文件映射共享内存时需要注意:使用命名对象的时候,对象前面必须要加上“Global//”表示该对象是一个全局的对象不同进程在使用文件映射共享内存时调用函数MapViewOfFile填写内存的起始偏移,视口大小必须完全一样这个例子中只是简单的一个进程写,另一个进程读,如果想要两个进程同时读写共享内存,可以使用Event等方式进行同步。直接读写磁盘扇区CreateFile可以打开许多设备,一般来说,它可以打开所有的字符设备,向串口,管道,油槽等等,在编写某些硬件的驱动程序时如果将其以字符设备的方式来操作,那么理论上在应用层是可以用CreateFile打开这个硬件设备的句柄,并操作它的,这里介绍下如何使用CreateFile来直接读取物理磁盘。读写物理磁盘只需要改变一下CreateFile中代表文件名称的第一个参数,将这个参数改为\.\PhysicalDrive0,后面的数字代表的是第几块物理硬盘,如果有多块硬盘,后面还可以是1、2等等注意这是在直接读写物理磁盘,当你不了解文件系统的时候,不要随意往里面写数据,以免造成磁盘损坏下面是一个简单的例子 DWORD dwSectorsPerCluster = 0; DWORD dwBytesPerSector = 0; DWORD dwNumberOfFreeClusters = 0; DWORD dwTotalNumberOfClusters = 0; TCHAR pDiskName[] = _T("\\\\.\\PhysicalDrive0"); //get disk info if(GetDiskFreeSpace(_T("c:\\"), &dwSectorsPerCluster, &dwBytesPerSector, &dwNumberOfFreeClusters, &dwTotalNumberOfClusters)) { printf("磁盘信息:\n"); LARGE_INTEGER size_disk = {0}; size_disk.QuadPart = (LONGLONG)dwTotalNumberOfClusters * (LONGLONG)dwSectorsPerCluster * (LONGLONG)dwBytesPerSector; printf("\t总大小 %dG", size_disk.QuadPart / (1024 * 1024 * 1024)); printf("\t簇总数%d, 簇中扇区总数:%d, 扇区大小:%d\n", dwTotalNumberOfClusters, dwSectorsPerCluster, dwBytesPerSector); } else { dwBytesPerSector = 512; } HANDLE hDisk = CreateFile(pDiskName,GENERIC_READ,FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,NULL,OPEN_EXISTING,0,NULL); if(hDisk == INVALID_HANDLE_VALUE) { printf("create file error\n"); return GetLastError(); } char* pMem = (char*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwBytesPerSector * 8); DWORD dwRead = 0; if(!ReadFile(hDisk, pMem, dwBytesPerSector * 8, &dwRead, NULL)) { printf("read file error\n"); return GetLastError(); } for(int i = 0; i < dwBytesPerSector * 8; i++) { if(i % 16 == 0 && i != 0) { printf("\n"); } printf("0x%02x ", pMem[i]); } CloseHandle(hDisk);上面的例子调用了GetDiskFreeSpace函数获取了逻辑卷的相关信息,它需要传入一个盘符,表示要获取哪个盘的数据,它会通过输出参数返回多个逻辑卷的信息,它们分别是:每个簇有多少个扇区,每个扇区的大小,有多少个空闲的簇,卷中簇的个数。根据这些信息就可以计算出逻辑卷的大小哦,在计算的时候由于磁盘空间一定是大于4G的,所以在这要用64位整数保存。知道了扇区大小后,直接调用文件操作函数,读取8个扇区的数据,然后输出。文件的异步操作在常规文件读写方式中,是严格串行化的,只有当读写操作完全完成时才会返回,由于磁盘读写相对于CPU的运行效率来说实在是太慢的,这就造成了程序长时间处理等待状态,这种读写方式称之为阻塞方式,早期的磁盘在进行读写时是需要CPU来控制,这样CPU必须来配合慢速的硬盘,造成了效率低下,于是硬件工程师在在磁盘中加入了一个控制设备,专门用来控制磁盘的读写,这个设备被称之为DMA,由于DMA的存在,使得CPU从漫长的磁盘操作中解放出来,一般在进行磁盘读写时,CPU主要向DMA发出一个读写命令,然后就继续执行后面的工作,当读写完成后DMA向CPU发出完成的指令,这个时候CPU会停下手上的工作,来处理这个通知,程序此时会陷入中断,直到CPU完成对应的操作。由于DMA的出现使得CPU从慢速的磁盘操作中解放出来,但是在同步的读写方式中,CPU发出磁盘的读写指令后什么都不做,一直等待磁盘的读写玩成,使CPU长时间陷入等待状态,浪费了宝贵的CPU的资源。所以为了程序效率,在读写磁盘时一般使用异步的方式,在发出读写命令后立即返回,然后执行后面的操作,这样就在一定程度上利用了闲置的CPU资源。重叠IO在Windows中默认使用同步的方式进行读写操作,如果要使用异步的方式,在创建文件句柄的时候,需要在CreateFile函数的dwFlagsAndAttributes参数中加上FILE_FLAG_OVERLAPPED标识,然后可以设置一个完成函数,并在对应线程中调用waitex函数或者使用SleepEx函数使线程陷入可警告状态,当读写操作完成时会将完成函数插入线程的APC队列,当线程进入可警告状态的时候会调用APC函数,这样就可以知道读写操作已经完成。这是一种方式,还可以使用一个OVERLAPPED结构,并给这个结构中填上一个事件对象,在需要进行同步的地方等待这个事件对象,在磁盘操作完成的时候会将其设置为有信号,上面的两种方式都利用的Windows提供的重叠IO模型不管使用哪种方式,在进行文件的异步操作时都需要自己维护并偏移文件指针。在同步的方式时Windows是完成之后返回,它一次只会写入一条数据到磁盘,而且它也知道具体写入了多少数据,这时候系统帮助我们完成了文件指针的偏移,但是在进行异步操作的时候可能会同时有多条数据写入,并且系统不知道具体会成功写入多少数据,所以它不可能帮我们进行文件指针的偏移,这个时候就需要自己进行偏移操作完成函数使用完成函数主要需要如下步骤:调用CreateFile在dwFlagsAndAttributes参数中加上FILE_FLAG_OVERLAPPED标识表示我们需要使用异步的方式来进行磁盘操作准备一个完成函数,函数的原型为:VOID CALLBACK FileIOCompletionRoutine(DWORD dwErrorCode,DWORD dwNumberOfBytesTransfered,LPOVERLAPPED lpOverlapped);函数的最后一个参数是一个OVERLAPPED结构,该结构的定义如下:typedef struct _OVERLAPPED { ULONG_PTR Internal; ULONG_PTR InternalHigh; union { struct { DWORD Offset; DWORD OffsetHigh; }; PVOID Pointer; }; HANDLE hEvent; } OVERLAPPED, *LPOVERLAPPED;这个结构中有一个共用体,其实这个共用体都可以用来操作文件指针,如果用其中的结构体,那么需要分别给其中的高32位和低32位赋值,如果使用指针,这个时候指针变量不指向任何内存,这个指针变量仅仅是作为一个变量名罢了,使用时也是将其作为正常变量来使用,虽然它是一个指针占4个字节,但是由于是一个共用体,它后面还有4个字节的剩余空间可以使用,所以使用它来存储文件指针的偏移没有任何问题。调用ReadFileEx或者WriteFileEx函数(ReadFile WriteFile不支持完成函数的方式)并将完成函数作为最后一个参数传入调用WaitEx族的等待函数或者SleepEx函数使线程陷入可警告状态,这个时候会执行完成函数下面是一个演示的例子LARGE_INTEGER g_FilePointer = {0}; //全局的文件指针 struct ST_EXT_OVERLAPPED { OVERLAPPED m_ol; //后面的代码在使用的时候后 HANDLE m_hFile; //操作的文件句柄 LPVOID m_pData; //操作的内存 DWORD m_dwLen; //操作的数据长度 }; VOID CALLBACK FileIOCompletionRoutine(DWORD dwErrorCode,DWORD dwNumberOfBytesTransfered,LPOVERLAPPED lpOverlapped) { ST_EXT_OVERLAPPED* pExOl = (ST_EXT_OVERLAPPED*)lpOverlapped; printf("线程[%04x]完成写入操作\n", GetCurrentThreadId()); HeapFree(GetProcessHeap(), 0, pExOl->m_pData); HeapFree(GetProcessHeap(), 0, pExOl); pExOl = NULL; } DWORD WriteThreadProc(LPVOID lpParameter) { HANDLE hFile = *(HANDLE*)(lpParameter); ST_EXT_OVERLAPPED* pExOl = NULL; TCHAR szBuf[256] = _T(""); StringCchPrintf(szBuf, 256, _T("这是一条模拟日志写入信息,由线程[%04x]写入\r\n"), GetCurrentThreadId()); size_t dwLen = 0; StringCchLength(szBuf, 256, &dwLen); dwLen += 1; //保存字符串结尾的\0 for (int i = 0; i < 100; i++) { pExOl = (ST_EXT_OVERLAPPED*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(ST_EXT_OVERLAPPED)); pExOl->m_dwLen = dwLen * sizeof(TCHAR); pExOl->m_pData = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwLen * sizeof(TCHAR)); StringCchCopy((TCHAR*)pExOl->m_pData, 256, szBuf); pExOl->m_hFile = hFile; //使用锁无关的方式进行同步操作 *((LONGLONG*)&pExOl->m_ol.Pointer) = InterlockedCompareExchange64(&g_FilePointer.QuadPart, g_FilePointer.QuadPart + pExOl->m_dwLen, g_FilePointer.QuadPart); WriteFileEx(pExOl->m_hFile, pExOl->m_pData, pExOl->m_dwLen, (OVERLAPPED*)&pExOl->m_ol, FileIOCompletionRoutine); //do something if(WAIT_IO_COMPLETION == SleepEx(INFINITE, TRUE)) { } } return 0; } int _tmain(int argc, _TCHAR* argv[]) { HANDLE hFile = CreateFile(_T("log.txt"), GENERIC_ALL, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL);//让其支持异步操作 if (hFile == INVALID_HANDLE_VALUE) { printf("CreateFile error\n"); return GetLastError(); } ST_EXT_OVERLAPPED* pExOl = (ST_EXT_OVERLAPPED*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(ST_EXT_OVERLAPPED)); pExOl->m_hFile = hFile; pExOl->m_dwLen = sizeof(WORD); pExOl->m_pData = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(WORD)); *((WORD*)pExOl->m_pData) = MAKEWORD(0xff,0xfe); //文件指针的偏移 pExOl->m_ol.Offset = g_FilePointer.LowPart; pExOl->m_ol.OffsetHigh = g_FilePointer.HighPart; g_FilePointer.QuadPart += pExOl->m_dwLen; WriteFileEx(pExOl->m_hFile, pExOl->m_pData, pExOl->m_dwLen, (LPOVERLAPPED)&pExOl->m_ol, FileIOCompletionRoutine); HANDLE hThreads[20] = {NULL}; for (int i = 0; i < 20; i++) //创建20个写线程 { hThreads[i] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)WriteThreadProc, &hFile, 0, NULL); } while(WAIT_IO_COMPLETION == WaitForMultipleObjectsEx(20, hThreads, TRUE, INFINITE, TRUE)) //函数返回WAIT_IO_COMPLETION 表示执行了完成函数 { printf("有一个读写操作完成\n"); } for (int i = 0; i < 20; i++) { CloseHandle(hThreads[i]); } CloseHandle(hFile); _tsystem(_T("PAUSE")); return 0; }在上面的例子中,我们首先向文件中写入0xff, 0xfe这两个值,在Windows中存储Unicode字符串的文件都是以0xff 0xfe开头,所以在写入Unicode字符串之前需要写入这两个值然后创建了20个线程,每个线程负责往文件中写入100条数据。线程先创建了一个包含OVERLAPPED结构的数据类型,然后再使用InterlockedCompareExchange64同步文件指针,这句话的意思是,向将高速缓存中的数据与内存中的数据进行比较,如果二者的值相同,那么久更改全局的文件指针,否则就不进行变化。实际上在Intel架构的机器上存在大量的高速缓存,为了效率,有的时候会将一些数据放置到高速缓存中,这样造成高速缓存中一份,内存中也有一份,有的时候在进行值得更改时它只会改变内存中的值,而高速缓存中的值不会更新,在调用这个函数的时候第一个参数传入的是一个指针,取值操作会强制CPU到内存中进行访问,这样这句话实质上是比较高速缓存与内存中的值是否一致,如果不一致,那么说明它被其他的线程进行过修改,将新的文件指针进行了替换,那么这个时候不需要进行任何操作,在之前写入文件的末尾进行追加即可,如果没有发生修改,那么其他线程可能会在当前位置写入,本线程也在当前位置写的话会造成覆盖,所以往后偏移文件指针,使其他线程使用新偏移的位置,本线程使用当前的位置,这样就不会发生覆盖在完成历程中完成清理内存的任务。每个WriteFileEx都对应着内存的分配,完成后都会调用这个完成历程清理对应的内存,这样就不会造成内存泄露。最后在主线程中等待子线程的完成,然后关闭句柄并结束进程事件模型事件模型与之前的完成历程相似,只是它不需要设置完成函数,需要在OVERLAPPED结构中设置一个事件,当IO操作完成时会将这个事件设置为有信号,然后在需要进行同步的位置等待这个事件即可下面是它的具体的例子LARGE_INTEGER g_FilePointer = {0}; //全局的文件指针 struct ST_EXT_OVERLAPPED { OVERLAPPED m_ol; //后面的代码在使用的时候后 HANDLE m_hFile; //操作的文件句柄 LPVOID m_pData; //操作的内存 DWORD m_dwLen; //操作的数据长度 }; DWORD WriteThreadProc(LPVOID lpParameter) { HANDLE hFile = *(HANDLE*)(lpParameter); ST_EXT_OVERLAPPED* pExOl = NULL; TCHAR szBuf[256] = _T(""); StringCchPrintf(szBuf, 256, _T("这是一条模拟日志写入信息,由线程[%04x]写入\r\n"), GetCurrentThreadId()); size_t dwLen = 0; StringCchLength(szBuf, 256, &dwLen); dwLen += 1; //保存字符串结尾的\0 for (int i = 0; i < 100; i++) { pExOl = (ST_EXT_OVERLAPPED*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(ST_EXT_OVERLAPPED)); pExOl->m_dwLen = dwLen * sizeof(TCHAR); pExOl->m_pData = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwLen * sizeof(TCHAR)); StringCchCopy((TCHAR*)pExOl->m_pData, 256, szBuf); pExOl->m_hFile = hFile; pExOl->m_ol.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); //使用锁无关的方式进行同步操作 *((LONGLONG*)&pExOl->m_ol.Pointer) = InterlockedCompareExchange64(&g_FilePointer.QuadPart, g_FilePointer.QuadPart + pExOl->m_dwLen, g_FilePointer.QuadPart); DWORD dwWritten = 0; WriteFile(pExOl->m_hFile, pExOl->m_pData, pExOl->m_dwLen, &dwWritten, (OVERLAPPED*)&pExOl->m_ol); //do something if(WAIT_OBJECT_0 == WaitForSingleObject(pExOl->m_ol.hEvent, INFINITE)) { printf("线程[%04x],写入操作完成一次,继续等待写入.....\n", GetCurrentThreadId()); HeapFree(GetProcessHeap(), 0, pExOl->m_pData); HeapFree(GetProcessHeap(), 0, pExOl); } } return 0; } int _tmain(int argc, _TCHAR* argv[]) { HANDLE hFile = CreateFile(_T("log.txt"), GENERIC_ALL, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL);//让其支持异步操作 if (hFile == INVALID_HANDLE_VALUE) { printf("CreateFile error\n"); return GetLastError(); } ST_EXT_OVERLAPPED* pExOl = (ST_EXT_OVERLAPPED*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(ST_EXT_OVERLAPPED)); pExOl->m_hFile = hFile; pExOl->m_dwLen = sizeof(WORD); pExOl->m_pData = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(WORD)); *((WORD*)pExOl->m_pData) = MAKEWORD(0xff,0xfe); pExOl->m_ol.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); //文件指针的偏移 pExOl->m_ol.Offset = g_FilePointer.LowPart; pExOl->m_ol.OffsetHigh = g_FilePointer.HighPart; g_FilePointer.QuadPart += pExOl->m_dwLen; DWORD dwWritten = 0; WriteFile(pExOl->m_hFile, pExOl->m_pData, pExOl->m_dwLen, &dwWritten, (LPOVERLAPPED)&pExOl->m_ol); HANDLE hThreads[20] = {NULL}; //等待当前写入完成 if (WAIT_OBJECT_0 == WaitForSingleObject(pExOl->m_ol.hEvent, INFINITE)) { printf("写入头部操作完成\n"); HeapFree(GetProcessHeap(), 0, pExOl->m_pData); HeapFree(GetProcessHeap(), 0, pExOl); } for (int i = 0; i < 20; i++) //创建20个写线程 { hThreads[i] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)WriteThreadProc, &hFile, 0, NULL); } WaitForMultipleObjects(20, hThreads, TRUE, INFINITE); for (int i = 0; i < 20; i++) { CloseHandle(hThreads[i]); } CloseHandle(hFile); _tsystem(_T("PAUSE")); return 0; }上面的例子与之前的完成历程的例子基本上一样,只是在OVERLAPPED结构中加入EVENT对象,并且没有完成历程,内存的清理工作需要在本线程中进行清理完成端口上述重叠IO在一定程度上解决的线程陷入等待的问题,但是从上面的代码上来看,仍然需要在本线程中进行等待操作,也就是说,如果在IO函数返回后进行某项操作,但是这项操作完成后而IO操作并没有完成,那么仍然要陷入等待,现在有一个想法,就是同步操作不在本线程中完成,另外开辟一个线程,将所有的等待操作都放到新线程中,而本线程就不必进行等待,同步线程只需要在操作完成的时候启动执行,这样几乎就不存在CPU等待IO设备的问题。主要的问题是,怎么向新线程传递同步对象,就像上面的例子来说,等待IO操作完成就是为了清理内存而已,这个时候如果创建新线程进行等待的话,总共有2000个写入操作,为了清理每块内存,需要定义一个2000O包含VERLAPPED结构的数组,然后当所有线程启动后将数组指针传入,如果为每个如果动态添加新的写入线程,那就必须修改数组大小。这给编程造成了很大的麻烦,为了解决这个问题,VC中引入了完成端口模型本质上完成端口利用了线程池机制并结合了重叠IO的优势,在Windows下这种IO模型是最高效的一种。完成端口首先创建对应数量的线程的线程池,然后将相关的文件句柄与完成端口对象绑定,并传入一个OVERLAPPED结构的指针,然后进行等待,一旦有IO操作完成,就会启动完成端口中的线程,完成后续的操作。完成端口的使用一般经过下面几个步骤:调用CreateIoCompletionPort创建完成端口对象,并制定最大并发线程数(一般制定CPU核数或者核数的两倍)创建用于完成端口的线程,一般大于等于最大并发数调用函数CreateIoCompletionPort,将文件句柄与完成端口绑定在IO操作中传入一个OVERLAPPED结构在完成端口的线程中调用GetQueuedCompletionStatus进行等待,当有IO操作完成时函数会返回,对应的线程就可以启动执行函数CreateIoCompletionPort原型如下HANDLE WINAPI CreateIoCompletionPort( __in HANDLE FileHandle, __in_opt HANDLE ExistingCompletionPort, __in ULONG_PTR CompletionKey, __in DWORD NumberOfConcurrentThreads );第一个参数是文件句柄,第二参数是完成端口句柄,第三个参数是一个完成的标识。一般给NULL,第四个是最大线程数。一般在操作的时候如果是创建完成端口句柄,那么只需要指定最大并发线程数,如果是将文件句柄和完成端口对象进行绑定,只需要提供前连个参数。在下面的例子中可以很清楚的看到它的用法下面是一个使用完成端口的例子:LARGE_INTEGER g_FilePointer = {0}; //全局的文件指针 struct ST_EXT_OVERLAPPED { OVERLAPPED m_ol; //后面的代码在使用的时候后 HANDLE m_hFile; //操作的文件句柄 LPVOID m_pData; //操作的内存 DWORD m_dwLen; //操作的数据长度 BOOL bExit; }; DWORD WriteThreadProc(LPVOID lpParameter) { HANDLE hFile = *(HANDLE*)(lpParameter); ST_EXT_OVERLAPPED* pExOl = NULL; TCHAR szBuf[256] = _T(""); StringCchPrintf(szBuf, 256, _T("这是一条模拟日志写入信息,由线程[%04x]写入\r\n"), GetCurrentThreadId()); size_t dwLen = 0; StringCchLength(szBuf, 256, &dwLen); dwLen += 1; //保存字符串结尾的\0 for (int i = 0; i < 100; i++) { pExOl = (ST_EXT_OVERLAPPED*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(ST_EXT_OVERLAPPED)); pExOl->m_dwLen = dwLen * sizeof(TCHAR); pExOl->m_pData = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwLen * sizeof(TCHAR)); StringCchCopy((TCHAR*)pExOl->m_pData, 256, szBuf); pExOl->m_hFile = hFile; pExOl->bExit = FALSE; //使用锁无关的方式进行同步操作 *((LONGLONG*)&pExOl->m_ol.Pointer) = InterlockedCompareExchange64(&g_FilePointer.QuadPart, g_FilePointer.QuadPart + pExOl->m_dwLen, g_FilePointer.QuadPart); DWORD dwWritten = 0; WriteFile(pExOl->m_hFile, pExOl->m_pData, pExOl->m_dwLen, &dwWritten, (OVERLAPPED*)&pExOl->m_ol); } return 0; } DWORD IocpThreadProc(LPVOID lpParameter) { HANDLE hIocp = *(HANDLE*)lpParameter; DWORD dwBytesTransfered = 0; DWORD dwFlags = 0; LPOVERLAPPED pOl = NULL; while (TRUE) { ST_EXT_OVERLAPPED* pExOl = NULL; BOOL bRet = GetQueuedCompletionStatus(hIocp, 0, 0, &pOl, INFINITE);//MSDN上说如果完成端口队列为空,那么函数会返回FLASE,并且pOl为NUULL, 所以在这进行判断,如果为FLASE,就不往下执行,否则程序会崩溃 if (!bRet) { continue; } pExOl = (ST_EXT_OVERLAPPED*)pOl; if (pExOl->bExit) { printf("收到退出消息,IOCP线程[%04x]退出", GetCurrentThreadId()); HeapFree(GetProcessHeap(), 0, pExOl); return 0; } printf("有一个线程的写入操作完成\n"); HeapFree(GetProcessHeap(), 0, pExOl->m_pData); HeapFree(GetProcessHeap(), 0, pExOl); } } int _tmain(int argc, _TCHAR* argv[]) { HANDLE hFile = CreateFile(_T("log.txt"), GENERIC_ALL, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL);//让其支持异步操作 if (hFile == INVALID_HANDLE_VALUE) { printf("CreateFile error\n"); return GetLastError(); } //创建IOCP内核对象并制定最大并发线程数 SYSTEM_INFO si = {0}; GetSystemInfo(&si); HANDLE hIocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, NULL, 2 * si.dwNumberOfProcessors); //创建IOCP线程 HANDLE* hIocpThreads = (HANDLE*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 2 * si.dwNumberOfProcessors * sizeof(HANDLE)); for (int i = 0; i < 2 * si.dwNumberOfProcessors; i++) { hIocpThreads[i] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)IocpThreadProc, &hIocp, 0, NULL); } //将文件句柄与IOCP句柄绑定 CreateIoCompletionPort(hFile, hIocp, NULL, 0); ST_EXT_OVERLAPPED* pExOl = (ST_EXT_OVERLAPPED*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(ST_EXT_OVERLAPPED)); pExOl->m_hFile = hFile; pExOl->m_dwLen = sizeof(WORD); pExOl->m_pData = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(WORD)); *((WORD*)pExOl->m_pData) = MAKEWORD(0xff,0xfe); pExOl->bExit = FALSE; //文件指针的偏移 pExOl->m_ol.Offset = g_FilePointer.LowPart; pExOl->m_ol.OffsetHigh = g_FilePointer.HighPart; g_FilePointer.QuadPart += pExOl->m_dwLen; DWORD dwWritten = 0; WriteFile(pExOl->m_hFile, pExOl->m_pData, pExOl->m_dwLen, &dwWritten, (LPOVERLAPPED)&pExOl->m_ol); HANDLE hThreads[20] = {NULL}; for (int i = 0; i < 20; i++) //创建20个写线程 { hThreads[i] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)WriteThreadProc, &hFile, 0, NULL); } //等待写入线程的完成 WaitForMultipleObjects(20, hThreads, TRUE, INFINITE); for (int i = 0; i < 20; i++) { CloseHandle(hThreads[i]); } //关闭IOCP线程 for (int i = 0; i < 2 * si.dwNumberOfProcessors; i++) { ST_EXT_OVERLAPPED* pExitMsg = (ST_EXT_OVERLAPPED*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(ST_EXT_OVERLAPPED)); pExitMsg->bExit = TRUE; PostQueuedCompletionStatus(hIocp, 0, 0, &pExitMsg->m_ol); } //关闭IOCP线程句柄 for (int i = 0; i < 2 * si.dwNumberOfProcessors; i++) { CloseHandle(hIocpThreads[i]); } CloseHandle(hFile); _tsystem(_T("PAUSE")); return 0; }
2017年06月11日
5 阅读
0 评论
0 点赞
2017-06-01
使用MSHTML解析HTML页面
最近在写一个爬虫项目,本来打算用C/C++来实现,在网上查找有关资料的时候发现了微软的这个MSHTML库,最后发现在解析动态页面的时候它的表现实在是太差:在项目中需要像浏览器那样,执行JavaScript等脚本然后形成静态的HTML页面,最后才分析这个静态页面。但是MSHTML在执行JavaScript等脚本时需要配合WebBroswer这个ActiveX控件,这个控件又必须在GUI程序中使用,但是我做的这个功能最终是嵌入到公司产品中发布,不可能为它专门生成一个GUI页面,所以这个方案就作废了。虽然最终没有采用这个方案,但是我在开始学习MSHTML并写Demo的过程中还是收益匪浅,所以在这记录下我的成果解析Html页面MSHTML是一个典型的DOM类型的解析库,它基于COM组件,在解析Html页面时需要一个IHTMLDocument2类型的接口。在GUI程序中很容易就获取这个接口,获取它的方法很容易就可以在网上找到,在这主要说一下如何通过一段HTML字符串来生成对应的IHTMLDocument2接口。至于如何生成这个HTML字符串,我们可以通过向web服务器发送http请求,并获取它的返回,解析这个返回的数据包即可获取到对应的HTML页面数据。获取这个接口主要需要经过下面的几个步骤:使用CoCreateInstance创建一个接口,对于IHTMLDocument2接口一般是使用下面的语句:HRESULT hr = CoCreateInstance(CLSID_HTMLDocument, NULL, CLSCTX_INPROC_SERVER, IID_IHTMLDocument2, (void**)&m_spDoc);2.创建一个COM中的数组,将HTML字符串写到数组中。这个数组主要用来进行VC与VB的交互,以便VB程序能够很方便的使用COM接口。在使用这个数组时不需要关注它的具体成员,VC提供了具体的接口来使用它,在初始化它的时候只需要调用下面几个:a)SafeArrayCreateVector:这个函数用来创建一个对应的数组结构。函数有三个参数,第一个参数表示数组中元素类型,一般给VT_VARIANT表示它是一个自动类型,第二个参数数组元素起始位置的下标,对于VC来说,数组元素总是从0开始,所以这个位置一般给0,第三个参数是数组的维数,在这我们只是简单的将它作为一个字符数组,所以它是一个一维数组。b)SafeArrayAccessData:允许用户操作这个数组,在需要读写这个数组时都需要调用这个函数,以便获取这个数组的操作权。它有两个参数,第一个参数是数组变量,第二个参数是一个输出参数,当调用这个函数成功,会提供一个缓冲区,我们操作这个缓冲区就相当于操作了这个数组。c)SafeArrayUnaccessData:每当操作数组完成时需要调用这个函数,函数与SafeArrayAccessData配套使用,这个函数用来回收这个权限,并使我们对数组的操作生效调用接口的write方法,将接口与HTML字符串绑定经过这样几步就可以利用这个接口来访问HTML中的元素了,下面是它的详细代码:IHTMLDocument2* CreateIHTMLDocument2(const string &strHtml) { IHTMLDocument2 *m_spDoc = NULL; HRESULT hr = CoCreateInstance(CLSID_HTMLDocument, NULL, CLSCTX_INPROC_SERVER, IID_IHTMLDocument2, (void**)&m_spDoc); HRESULT hresult = S_OK; VARIANT *param; SAFEARRAY *sfArray; // Creates a new one-dimensional array sfArray = SafeArrayCreateVector(VT_VARIANT, 0, 1); if (sfArray == NULL || m_spDoc == NULL) { return; } hresult = SafeArrayAccessData(sfArray,(LPVOID*) ¶m); param->vt = VT_BSTR; param->bstrVal = _com_util::ConvertStringToBSTR(strHtml.c_str()); hresult = SafeArrayUnaccessData(sfArray); hresult = m_spDoc->write(sfArray); return m_spDoc; }HTML元素的遍历MSHTML中,将元素的对应信息封装为IHTMLElement接口,得到对应元素的接口后可以使用它里面的get系列方法来获取它里面的各种信息,这些函数我没有一一列举,当需要时看看MSDN即可。当获取到了HTML文档的IID_IHTMLDocument2接口时,可以使用下面的步骤进行元素的遍历:接口的get_all方法获取所有的标签节点。这个函数通过一个输出参数输出IHTMLElementCollection类型的接口指针然后通过IHTMLElementCollection接口的get_length方法获取标签的总数量,根据这个数量写一个循环,在循环进行元素的遍历在循环中使用IHTMLElementCollection接口的item方法进行迭代,依次获取各个元素对应的IDispatch接口指针调用IDispatch接口指针的QueryInterface方法生成对应的IHTMLElement接口。通过这个接口获取元素的各中信息。它对应的代码如下:void EnumElements(IHTMLDocument2* m_spDoc) { CComPtr<IHTMLElementCollection> pCollec; m_spDoc->get_all(&pCollec); if (NULL == pCollec) { return ; } VARIANT varName; long len = 0; pCollec->get_length(&len); for (int i = 0; i < len; i++) { varName.vt = VT_I4; varName.llVal = i; CComPtr<IHTMLElement> pElement; CComPtr<IDispatch> pDisp; pCollec->item(varName, varName, &pDisp); if (NULL == pDisp) { continue; } pDisp->QueryInterface(IID_IHTMLElement, (LPVOID*)&pElement); if (NULL != pElement) { BSTR bstrTag; pElement->get_tagName(&bstrTag); string strTag = _com_util::ConvertBSTRToString(bstrTag); cout<<strTag.c_str()<<endl; } } }这个方法不能很好的体现各个元素的层次结构,它可以遍历所有的元素,但是默认将元素都作为同一层来表示,如果需要得到对应的子节点,可以调用get_children方法,它可以获取下面的所有子节点,使用方法与get_all类似调用JavaScript方法在这,调用JavaScript函数只能想调用普通的函数一样,根据函数名,给它参数,并获取返回值,但是不能得到它执行到中间的某个步骤,比如说这样一个函数function add(a, b){ window.location.href = "https://www.baidu.com"; return a + b }调用这个函数,只能得到a + b的值,但是并不知道它会跳转到另一个页面,在编写爬虫时如果存在这样的跳转或者通过某条语句生成了一个链接,那么使用后面说的方法是获取不到的言归正传,下面来说下如何实现调用JavaScript。调用JavaScript方法一般是使用IDispatch接口中的Invoke方法,但是使用这个略显麻烦,我在网上找到了更简单的方法,就是使用CComDispatchDriver接口中的Invoke方法,这个接口中主要有Invoke0、Invoke1、Invoke2、InvokeN几个用于调用JavaScript函数的方法,分别表示传入0个参数、1个参数、2个参数、任意个参数。一般使用如下步骤来调用:1.调用IID_IHTMLDocument2的get_Script方法,获取CComDispatchDriver接口调用CComDispatchDriver接口的GetIDOfName,传入JavaScript函数名称,获取JS函数对应的元素接口,这个函数会通过一个输出参数输出一个DISPID类型的变量。这个主要是一个ID,用来唯一标识一个js函数调用CComDispatchDriver接口的invoke函数,传入对应的参数,并调用js函数。下面是一个例子代码:bool CallJScript(IID_IHTMLDocument2* m_spDoc, const CString strFunc, CComVariant* paramArray,int nArgCnt,CComVariant* pVarResult) { CComDispatchDriver spScript; GetJScript(spScript); if (NULL == spScript) { return false; } DISPID pispid; BSTR bstrText = _com_util::ConvertStringToBSTR(strFunc); spScript.GetIDOfName(bstrText, &pispid); HRESULT hr = spScript.InvokeN(pispid, paramArray, nArgCnt, pVarResult); if(FAILED(hr)) { ShowError(GetSystemErrorMessage(hr)); return false; } return true; }在调用的时候需要组织一个CComVariant类型的数组,并提供一个数组元素个数作为参数。而对于Invoke0这样有确定函数参数的情况则要简单的多。获取js函数返回值js返回参数最终会被包装成一个VARIANT结构,在COM中为了方便操作这个结构,封装了一个CComVariant类。在操作返回值时就是围绕着CComVariant类来进行返回确定值当它返回一个确定值时很好解决,由于事先知道返回值得类型,只需要调用结构体的不同成员即可CComVariant varResult; parse.CallJScript("Add", CComVariant(1), CComVariant(2), &varResult); cout<<varResult.lVal<<endl;当它返回一个数组时,一般需要经过这样几步的处理:创建一个CComDispatchDriver,并将返回值得pdispVal赋值给它调用CComDispatchDriver接口的GetPropertyByName方法,将它的第一个参数传入"length"字符串,让其返回数组元素的个数在循环中调用GetPropertyByName方法,传入索引,获取对应索引位置的CComVariant值。CComVariant varResult; parse.CallJScript("Add", CComVariant(1), CComVariant(2), &varResult); CComVariant varArrayLen; CComDispatchDriver spDisp = varResult.pdispVal; spDisp.GetPropertyByName(L"length", &varArrayLen); for (int i = 0; i < varArrayLen.intVal; i++) { CComVariant varValue; CStringW csIndex; csIndex.Format(L"%d", i); spDisp.GetPropertyByName(csIndex, &varValue); cout<<varValue.intVal<<endl; }返回一个object对象js的object对象中可以有不同的属性,不同的属性对应不同的值,类似于一个字典结构,当返回这个类型,并且我们知道这个对象中的相关属性名称的时候可以通过下面的方法来获取各个属性中的值:创建一个CComDispatchDriver,并将返回值得pdispVal赋值给它调用CComDispatchDriver接口的GetPropertyByName方法,将它的第一个参数传入对应属性名称的字符串,让其返回属性的值//在这假设JavaScript方法返回一个object对象,其中有两个属性,str属性中保存字符串,value属性保存一个整型数据 CComVariant varResult; parse.CallJScript("Add", CComVariant(1), CComVariant(2), &varResult); CComVariant varValue; CComDispatchDriver spDisp = varResult.pdispVal; spDisp.GetPropertyByName(L"result", &varValue); cout<<"result:"<<varValue.intVal<<endl; spDisp.GetPropertyByName(L"str", &varValue); string strValue = _com_util::ConvertBSTRToString(varValue.bstrVal); cout<<"str:"<<strValue.c_str()<<endl;返回类型不确定的object对象上面这种情况只有当JavaScript代码由自己编写或者与他人进行过相关的约定的时候才可能非常清楚js函数中将会返回何种类型的值,但是大多数情况下,是不知道将会返回何种数据,比如像我们在编写爬虫的时候。这种情况下一般使用IDispatchEx接口来枚举返回对象中的属性名称然后再根据上面的方法来获取属性的值CComVariant varResult; parse.CallJScript("Add", CComVariant(1), CComVariant(2), &varResult); CComQIPtr<IDispatchEx> pDispEx = varResult.pdispVal; CComDispatchDriver spDisp = varResult.pdispVal; DISPID dispid; HRESULT hr = pDispEx->GetNextDispID(fdexEnumAll, DISPID_STARTENUM, &dispid); //枚举返回对象中所有属性对应的值 while (hr == NOERROR) { BSTR bstrName; pDispEx->GetMemberName(dispid, &bstrName); if (NULL != bstrName) { DISPPARAMS params; CComVariant varVaule; cout<<_com_util::ConvertBSTRToString(bstrName)<<endl; spDisp.GetPropertyByName(bstrName, &varVaule); SysFreeString(bstrName); } hr = pDispEx->GetNextDispID(fdexEnumAll, dispid, &dispid); }这些差不多就是我当初学会的一些东西,当初在利用这个方案实现爬虫的时候还是有许多坑,也看到了它的许多局限性,以至于我最终放弃了它,采用其他的解决方案。目前在使用的时候的我发现这样几个问题:在调用js时,如果不知道函数的名称,目前为止没有方法可以调用,这样就需要我们在HTML中使用正则表达式等方法进行提取,但是在HTML中调用js的方法实在太多,而有的只有一个函数,并没有调用,这些情况给工作带来了很大的挑战MSHTML提供的功能主要是用来与IE进行交互,以便很容易实现一个类似于IE的浏览器或者与IE进行交互,但是如果要在控制台下进行相关功能的编写,则显的力不从心在控制台下它没有提供一个很好的方式来进行HTML页面的渲染。在于js进行交互的时候,只能简单的获取到一个VARIANT结构,这个结构可以表示所有常见的类型,但是在很多情况下,我们并不知道它具体代表哪个类型最后放上demo的下载地址:http://download.csdn.net/detail/lanuage/9857075
2017年06月01日
9 阅读
0 评论
0 点赞
2017-05-21
Windows资源
Windows资源是一种二进制数据,由链接器链接进程序成为程序的一部分,通过资源的方式可以很方便的对应用程序进行扩展。在Windows中资源可以是系统自定义的,也可以是用户自定义的。在VC++中资源是以被称为资源脚本的文本文件描述的(扩展名为rc),另外为了方便代码中调用资源,VC++环境中还会自动生成一个resource.h的头文件供C++代码使用,这个文件中主要定义了各个资源的ID,在vc++中使用ID来唯一标识一个资源,这个ID可以是数字也可以是字符串,其实在VC中真正用来标识资源的是字符串,通过宏MAKEINTRESOURCE可以将数字型的ID转化为对应的字符串,一般的资源函数在操作资源时都需要提供一个资源的字符串,而这个串就是利用这个宏传入ID生成的。在VC中资源脚本的基本格式为:资源名(ID串) 类型名 [语言] 资源数据资源数据可以是一段指定格式的文本或者一个文件,比如我们将wav作为资源加入到程序中,可以这样写:MY_WAVE_RES IDR_WAVE sample.wav.其中语言如果没有指定,那么默认为操作系统当前的语言环境。另外我们也可以将不同的资源放入不同的文本文件中,先定义好,然后在.rc文件中使用#include 来包含进来,比如在一个名为wav.resinclude文件中定义了一个WAV资源,然后可以在.rc文件中加上一句"#include <wav.resinclude> ”下面介绍下资源的操作中比较高级的技术引用自定义资源对于系统自定义资源,系统都提供了专门的函数来进行加载和操作,但是对于自定义资源,在操作时相对比较复杂,一般先使用FindResource和FindResourceEx在进程中找到对应的资源句柄,然后使用LoadResource将资源加载到内存中,以后就可以使用这个资源了。下面的一个例子演示了如何在当前exe中如何将另一个EXE作为资源加载,并执行它。__inline VOID GetAppPath(LPTSTR pszBuf) { DWORD dwLen = 0; if(0 == (dwLen = ::GetModuleFileName(NULL,pszBuf,MAX_PATH))) { printf("获取APP路径失败,错误码0x%08x\n",GetLastError()); return; } DWORD i = dwLen; for(; i > 0; i -- ) { if( '\\' == pszBuf[i] ) { pszBuf[i + 1] = '\0'; break; } } } int _tmain(int argc, _TCHAR* argv[]) { HMODULE hModule = GetModuleHandle(NULL); HRSRC hRsrc = FindResource(hModule, MAKEINTRESOURCE(IDR_RCDATA1), RT_RCDATA); if (INVALID_HANDLE_VALUE == hRsrc) { printf("加载自定义资源失败!\n"); return 0; } HGLOBAL hGlobalRes = LoadResource(hModule, hRsrc); LPVOID pResMem = LockResource(hGlobalRes); DWORD dwSize = SizeofResource(hModule, hRsrc); if (NULL == pResMem) { printf("获取资源所在内存失败!\n"); return 0; } TCHAR szFilePath[MAX_PATH] = _T(""); GetAppPath(szFilePath); StringCchCat(szFilePath, MAX_PATH, _T("test.exe")); HANDLE hFile = CreateFile(szFilePath, GENERIC_WRITE | GENERIC_READ, 0, NULL, CREATE_ALWAYS, 0, NULL); if(!WriteFile(hFile, pResMem, dwSize, &dwSize, NULL)) { printf("写文件失败\n"); return 0; } CloseHandle(hFile); STARTUPINFO si = {0}; PROCESS_INFORMATION pi = {0}; CreateProcess(szFilePath, NULL, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi); WaitForSingleObject(pi.hProcess, INFINITE); CloseHandle(pi.hThread); CloseHandle(pi.hProcess); return 0; }为了执行上面的代码需要在该项目工程中新加一个资源,将目标EXE添加到资源中,其中资源文件会多出一行"IDR_RCDATA1 RCDATA "E:\Program\ResourcesDemo\Debug\stdWindow.exe" 在resource.h文件中生成了一个资源对应的ID,做好这些工作,该程序就能正常运行在上面的代码中,依次调用FindResource、 LoadResource、LockResource,获取资源在进程空间中的地址,并将它对应的物理页面锁定在内存中,不允许其进行内存交换。然后将这个部分的内存写入到文件形成一个新的exe,最后执行这个exe,最终上面的程序编译运行后我们会发现在程序对应的目录下会生成一个test.exe文件。更新资源在有的时候需要对程序中的资源进行更新,这种情况下一般是在原始的工程下 更改资源,然后重新编译,但是这个时候用户需要下载新的更新程序,在原始程序比较大的情况下,为了更改一个简单的资源就要重新花大量的时间下载并更新程序,可能有点杀鸡用牛刀的意思,在我们只需要更新程序中的资源的情况下,Windows提供了一种方法。首先使用BeginUpdateResource建立可执行程序文件模块的更新句柄使用UpdateResource传入之前的更新句柄,更新资源数据使用EndUpdateResource函数关闭修改句柄,如果想让整个更改存盘需要将函数的第二个参数传入FALSE,这个参数的意思是是否放弃更新,传入false表示保存更新下面是一个简单的例子 HMODULE hModule = GetModuleHandle(NULL); //加载资源 HRSRC hRsrc = FindResource(hModule, MAKEINTRESOURCE(IDI_ICON1), RT_GROUP_ICON); if (hRsrc == NULL) { printf("加载资源失败\n"); return GetLastError(); } HGLOBAL hIcon = LoadResource(hModule, hRsrc); PVOID pIconBuf = LockResource(hIcon); int nIconSize = SizeofResource(hModule, hRsrc); //更新资源 HANDLE hUpdate = BeginUpdateResource(_T("E:\\Program\\ResourcesDemo\\Debug\\stdWindow.exe"), TRUE); BOOL bRet = UpdateResource(hUpdate, MAKEINTRESOURCE(RT_GROUP_ICON), MAKEINTRESOURCE(IDI_STDWINDOW), GetUserDefaultLangID(), pIconBuf, nIconSize); bRet = EndUpdateResource(hUpdate, FALSE); return 0;枚举资源枚举资源主要使用函数EnumResourceTypes EnumResourceNames, 和EnumResourceLanguages,这几个函数分别枚举资源类型,名称和语言,在msdn中查找函数的定义发现他们的调用顺序必须是type name language,下面是一个简单的枚举的例子:BOOL CALLBACK EnumResLangProc(HANDLE hModule, LPCTSTR lpszType, LPCTSTR lpszName, WORD wIDLanguage, LONG_PTR lParam) { printf("\tlanguage :%d\n", wIDLanguage); return TRUE; } BOOL CALLBACK EnumRe1sNameProc(HMODULE hModule, LPCTSTR lpszType, LPTSTR lpszName, LONG_PTR lParam) { if ((ULONG)lpszName & 0xffff0000) { printf("\t名称:%s\n", lpszName); }else { printf("\t名称:%d\n", (USHORT)lpszName); } return EnumResourceLanguages(hModule, lpszType, lpszName, (ENUMRESLANGPROCW)EnumResLangProc, NULL); } BOOL CALLBACK EnumResTypeProc(HMODULE hModule, LPTSTR lpszType,LONG_PTR lParam) { if ((ULONG)lpszType & 0xFFFF0000) { printf("类型:%s\n", lpszType); }else { printf("类型:%d\n", (USHORT)lpszType); } return EnumResourceNames(hModule, lpszType, (ENUMRESNAMEPROCW)EnumRe1sNameProc, NULL); } int _tmain(int argc, _TCHAR* argv[]) { HMODULE hExe = LoadLibrary(_T("E:\\Program\\ResourcesDemo\\Debug\\stdWindow.exe")); if (hExe == NULL) { printf("加载目标程序出错!\n"); return GetLastError(); } printf("目标程序中包含以下资源:\n"); EnumResourceTypes(hExe, EnumResTypeProc, NULL); return 0; }这段代码有以下几点需要注意:LoadLibrary不仅仅可以用来加载dll,实际上它可以加载任意的PE文件到内存,而GetModuleHandle是在内存中查找已经存在的一个模块的句柄,而我们这个地方这个exe事先并没有加载到内存,所以这里用GetModuleHandle是不能正确加载的,只有使用LoadLibrary这几个枚举函数都需要一个回调函数,这些函数指针类型可以在msdn中查找到,在VC环境下也定义了这些函数指针,但是不知道为什么在填入函数指针时需要强制转化,否则会报错资源可以使用字符串表示,也可以使用ID表示,这些回调函数虽说传入的都是枚举到的字符串指针,但是它仍然可能是ID,所以在这不能简单的直接把他们作为字符串使用,需要进行判断,判断的依据是它是否大于65536,因为我们说只有在ID值大于这个时,系统才会将ID作为字符串来使用
2017年05月21日
7 阅读
0 评论
0 点赞
2017-05-16
枚举进程中的模块
在Windows中枚举进程中的模块主要是其中加载的dll,在VC上主要有2种方式,一种是解析PE文件中导入表,从导入表中获取它将要静态加载的dll,一种是利用查询进程地址空间中的模块,根据模块的句柄来得到对应的dll,最后再补充一种利用Windows中的NATIVE API获取进程内核空间中的模块,下面根据给出这些方式的具体的代码片段:解析PE文件来获取其中的dll在之前介绍PE文件时说过PE文件中中存在一个导入表,表中记录了程序中加载的导入dll以及这些dll中函数的信息,这个结构的定义如下:typedef struct _IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; DWORD OriginalFirstThunk; }; DWORD TimeDateStamp; DWORD ForwarderChain; DWORD Name; DWORD FirstThunk; } IMAGE_IMPORT_DESCRIPTOR;我只需要获取这个结构并且根据RVA计算出它在文件中的偏移即可找到对应名称,利用之前PE解析器中的CPeFileInfo类来解析它即可,下面是具体的代码:void EnumModulesByPe(LPCTSTR pszPath) { CPeFileInfo peFile; peFile.strFilePath = pszPath; peFile.LoadFile(); if (!peFile.IsPeFile()) { printf("is not a pe file!\n"); return ; } peFile.InitDataDirectoryTable(); PIMAGE_IMPORT_DESCRIPTOR pImportTable = peFile.GetImportDescriptor(); while(!peFile.IsEndOfImportTable(pImportTable)) { printf("%s\n", peFile.RVA2fOffset(pImportTable->Name, (DWORD)peFile.pImageBase)); pImportTable++; } }利用之前的PE解析的类,首先给类中的文件路径赋值,然后加载到内存,并初始化它的数据目录表信息,从表中取出导入表的结构,根据结构中的Name字段的值来计算它的真实地址,即可解析出它里面的模块,这里我们只能解析出PE文件中自身保存的信息,如果dll是在程序运行之时调用LoadLibrary动态加载的,利用这个方法是找不到的。解析进程地址空间中的模块这个方法首先通过OpenProcess函数获取对应进程的句柄,然后调用EnumProcessModules枚举进程地址空间中当前存在的模块,这个函数会返回一个HMODULE句柄的数组,我们遍历这个数组,对其中的每个句柄调用GetModuleFileNameEx(很多模块GetModuleFileName获取不到,具体原因我没有深入研究)获取对应的文件路径。下面是具体的代码: HMODULE* phMods = NULL; HANDLE hProcess = NULL; DWORD dwNeeded = 0; DWORD i = 0; TCHAR szModName[MAX_PATH] = {}; hProcess = OpenProcess( PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, dwProcessId ); if (NULL == hProcess) { printf("不能打开进程[ID:0x%x]句柄,错误码:0x%08x\n",dwProcessId); return; } EnumProcessModules(hProcess, NULL, 0, &dwNeeded); phMods = (HMODULE*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwProcessId); if( EnumProcessModules(hProcess, phMods, dwNeeded, &dwNeeded)) { for ( i = 0; i < (dwNeeded / sizeof(HMODULE)); i++ ) { ZeroMemory(szModName,MAX_PATH*sizeof(TCHAR)); //在这如果使用GetModuleFileName,有的模块名称获取不到,函数返回无法找到该模块的错误 if ( GetModuleFileNameEx(hProcess, phMods[i], szModName,MAX_PATH)) { printf("%ws\n", szModName); } } } HeapFree(GetProcessHeap(), 0, phMods); CloseHandle( hProcess );由于静态加载的dll在进程启动之时就已经被加载到内存中,所以利用这个方法自然可以获取静态加载的dll,但是由于它是获取进程地址空间中加载的dll,所以要求进程要正在运行,毕竟进程如果没有运行,那么也就不存在地址空间,也就无法获取其中加载的dll,另外它只能获取当前进程地址空间中的dll,有的dll这个时候还没有被加载的话,它自然也获取不到。所以这个方法也不是能获取所有加载的dll使用进程快照获取模块这种方式是先使用 CreateToolhelp32Snapshot 函数来创建一个进程模块的快照,然后通过 Module32First 和 Module32Next 函数来遍历快照中的模块信息Module32First 和Module32Next 函数会通过参数返回一个 MODULEENTRY32结构的模块信息,该结构的定义如下:typedef struct tagMODULEENTRY32 { DWORD dwSize; DWORD th32ModuleID; DWORD th32ProcessID; DWORD GlblcntUsage; DWORD ProccntUsage; BYTE* modBaseAddr; DWORD modBaseSize; HMODULE hModule; TCHAR szModule[MAX_MODULE_NAME32 + 1]; TCHAR szExePath[MAX_PATH]; } MODULEENTRY32, *PMODULEENTRY32;dwSize 参数表示该结构体的大小,在调用Module32First 和Module32Next之前需要先设置这个成员的值th32ProcessID:对应进程的IDmodBaseAddr:模块的起始地址,也就是模块被加载到内存的基地址hModule: 模块的句柄szModule:模块名称szExePath: 模块对应文件所在的路径下面是一个使用该方式获取加载模块的例子BOOL GetModulesTH32(WORD wPID) { HANDLE hSnap; MODULEENTRY32 me; me.dwSize = sizeof(me); hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, wPID); if (hSnap == INVALID_HANDLE_VALUE) { FreeLibrary(hToolhelp); return FALSE; } keepGoing = Module32First(hSnap, &me); while (keepGoing) { printf("%d, %08x, %s, %s\r\n", me.modBaseAddr, me.modBaseSize, me.szModule, me.szExePath); keepGoing = Module32Next(hSnap, &me); } CloseHandle(hSnap); FreeLibrary(hToolhelp); return TRUE; }获取内核地址空间中的模块不管是解析PE文件还是调用EnumProcessModules都只能获取用户层地址空间中的模块,但是进程不光有用户空间,还有内核空间,所以在这再提供一种枚举内核地址空间的模块的方法。枚举内核地址空间主要使用函数ZwQuerySystemInformation(也可以使用NtQuerySystemInformation)在msdn中明确指出,这两个函数未来可能不在使用,不推荐使用,但是至少现在是仍然支持的,并且可以很好的完成任务。这两个函数主要在ntdll.dll中导出,两个函数的参数用法完全相同,只是一个是比较上层一个比较底层而已。在这主要说明一个,另一个完全一样:NTSTATUS WINAPI ZwQuerySystemInformation( __in SYSTEM_INFORMATION_CLASS SystemInformationClass, __inout PVOID SystemInformation, __in ULONG SystemInformationLength, __out_opt PULONG ReturnLength );函数的第一个参数是一个枚举类型,用来表示我们将要调用此函数来获取系统哪方面的信息,第二个参数是一个缓冲区,用来存储该函数输出的值,第三个参数是缓冲区的长度,第四个参数是实际需要缓冲区的长度,说到这应该很快就可以反应过来,我们可以第一次调用这个函数传入一个NULL缓冲,缓冲长度给0,让他返回具体的长度,然后根据这个长度,动态分配一块内存,再次调用传入正确的缓冲和长度,获取数据。在调用这个函数时需要注意下面几点:这个函数是未导出的,所以在微软的开发环境中是没有它的定义的,要使用它需要我们自己定义,定义的代码如下://这个NTSTATUS结构在应用层有定义,直接使用即可 typedef NTSTATUS(WINAPI *ZWQUERYSYSTEMINFORMATION)( __in SYSTEM_INFORMATION_CLASS SystemInformationClass, __inout PVOID SystemInformation, __in ULONG SystemInformationLength, __out_opt PULONG ReturnLength );这个函数使用的一些结构是在内核开发环境DDK中定义的,在应用层中可能没有它的定义,所以在这我们也需要对它们进行定义:#define NT_SUCCESS(status) ((NTSTATUS)(status)>=0) typedef enum _SYSTEM_INFORMATION_CLASS { SystemBasicInformation, SystemProcessorInformation, SystemPerformanceInformation, SystemTimeOfDayInformation, SystemPathInformation, SystemProcessInformation, SystemCallCountInformation, SystemDeviceInformation, SystemProcessorPerformanceInformation, SystemFlagsInformation, SystemCallTimeInformation, SystemModuleInformation, SystemLocksInformation, SystemStackTraceInformation, SystemPagedPoolInformation, SystemNonPagedPoolInformation, SystemHandleInformation, SystemObjectInformation, SystemPageFileInformation, SystemVdmInstemulInformation, SystemVdmBopInformation, SystemFileCacheInformation, SystemPoolTagInformation, SystemInterruptInformation, SystemDpcBehaviorInformation, SystemFullMemoryInformation, SystemLoadGdiDriverInformation, SystemUnloadGdiDriverInformation, SystemTimeAdjustmentInformation, SystemSummaryMemoryInformation, SystemNextEventIdInformation, SystemEventIdsInformation, SystemCrashDumpInformation, SystemExceptionInformation, SystemCrashDumpStateInformation, SystemKernelDebuggerInformation, SystemContextSwitchInformation, SystemRegistryQuotaInformation, SystemExtendServiceTableInformation, SystemPrioritySeperation, SystemPlugPlayBusInformation, SystemDockInformation, SystemProcessorSpeedInformation, SystemCurrentTimeZoneInformation, SystemLookasideInformation } SYSTEM_INFORMATION_CLASS, *PSYSTEM_INFORMATION_CLASS;缓冲区中存储的数据是一个表示返回数组中元素个数的DWORD类型的数据和一个对应结构体的数组,在MSDN上对这个缓冲进行解释时说这个缓冲区的头4个字节存储了对应数组的元素个数,而后面的存储的是对应结构的数组,所以在获取这个结构的数组时需要向后偏移4个字节。这个结构与我们传入的枚举值有关,比如我们在这获取的是进程内核空间中加载的模块信息,即传入的枚举值是SystemModuleInformation,它对应的结构应该是SYSTEM_MODULE_INFORMATION,它们之间的对应关系可以在MSDN中找到。这个结构也需要自己定义,它的定义如下:typedef struct _SYSTEM_MODULE_INFORMATION // Information Class 11 { ULONG Reserved[2]; PVOID pBase; ULONG Size; ULONG Flags; USHORT Index; USHORT Unknown; USHORT LoadCount; USHORT ModuleNameOffset; CHAR ImageName[256]; } SYSTEM_MODULE_INFORMATION, *PSYSTEM_MODULE_INFORMATION;下面就是这个的代码:void EnumKernelModules() { HMODULE hNtDll = LoadLibrary(_T("ntdll.dll")); if (INVALID_HANDLE_VALUE == hNtDll) { printf("加载ntdll.dll失败\n"); return ; } ZWQUERYSYSTEMINFORMATION ZwQuerySystemInformation = (ZWQUERYSYSTEMINFORMATION)GetProcAddress(hNtDll, "ZwQuerySystemInformation"); if (NULL == ZwQuerySystemInformation) { printf("导出函数失败\n"); return; } PULONG pBuffInfo = NULL; DWORD dwSize = 0; ZwQuerySystemInformation(SystemModuleInformation, pBuffInfo, 0, &dwSize); pBuffInfo = (PULONG)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwSize); NTSTATUS status = ZwQuerySystemInformation(SystemModuleInformation, pBuffInfo, dwSize, &dwSize); if (!NT_SUCCESS(status)) { return; } //在这为了验证之前说的,通过这两句输出发现他们的结果相同 printf("%d\n", *pBuffInfo); printf("%d\n", dwSize / sizeof(SYSTEM_MODULE_INFORMATION)); PSYSTEM_MODULE_INFORMATION pModuleInfo = (PSYSTEM_MODULE_INFORMATION)((ULONG)pBuffInfo + 4); for (int i = 0; i < *pBuffInfo; i++) { printf("%s\n", pModuleInfo[i].ImageName); } }
2017年05月16日
8 阅读
0 评论
0 点赞
2015-11-04
Win32 API 三态按钮的实现
Windows平台提供了丰富的控件,但是在使用中我们不会使用它提供的默认风格,有时候需要对控件进行改写,让它展现出更友好的一面,这次主要是说明三态按钮的实现。三态按钮指的是按钮在鼠标移到按钮上时显示一种状态,鼠标在按下时展现一种状态,在鼠标移开时又展现出另外一种状态,总共三种。当然鼠标按下和移出按钮展示的状态系统自己提供的有,这个时候在处理这两种状态只需要贴相应的图片就行了,三态按钮的实现关键在于如何判断鼠标已经移动到按钮上以及鼠标移出按钮,然后根据鼠标的位置将按钮做相应的调整。判断鼠标在按钮的相应位置,系统提供了一个函数_TrackMouseEvent用户处理鼠标移出、移入按钮。函数原型如下:BOOL _TrackMouseEvent( LPTRACKMOUSEEVENT lpEventTrack );函数需要传入一个TRACKMOUSEEVENT类型的指针,该结构的原型如下:typedef struct tagTRACKMOUSEEVENT { 2 DWORD cbSize;//该结构体所占空间大小 3 DWORD dwFlags;//指定服务的请求(指定它需要侦听的事件),这次主要用到的是TME_HOVER和TME_LEAVE(侦听鼠标移开和移入事件) 4 HWND hwndTrack;//指定我们需要侦听的控件的句柄 5 DWORD dwHoverTime;//HOVER消耗的时间,可以用系统提供的一个常量HOVER_DEFAULT由系统默认给出,也可以自己填写,单位是毫秒 6 } TRACKMOUSEEVENT, *LPTRACKMOUSEEVENT;在使用该函数时需要包含头文件commctrl.h和lib文件comctl32.lib解决了鼠标行为的检测之后,就是针对不同的鼠标行为重绘相应的按钮。重绘按钮需要在消息WM_DRAWITEM中,这个消息的处理是在相应控件的父窗口中实现的,而在一般情况下父窗口不会收到该消息,需要我们手工指定控件资源的属性为的OWNERDRAW为真,或者在创建相应的按钮窗口时将样式设置为BS_OWNERDRAW 。设置完成后就可以在对应的父窗口处理函数中接收并处理WM_DRAWITEM,在该消息中重绘按钮该消息中主要使用的参数是lpParam它里面包含的是一个指向DRAWITEMSTRUCT的结构体:typedef struct tagDRAWITEMSTRUCT { UINT CtlType; //控件类型 UINT CtlID; //控件ID UINT itemID; //子菜单项的ID主要用于菜单 UINT itemAction; //控件发出的动作,如ODA_SELECT表示控件被选中 UINT itemState; //控件状态,这次需要用到的状态为ODS_SELECTED表示按钮被按下 HWND hwndItem; //控件句柄 HDC hDC; RECT rcItem;//控件的矩形区域 ULONG_PTR itemData; } DRAWITEMSTRUCT;该结构体中的一些成员需要根据控件类型赋值,同时结构体中的itemAction、itemState是可以由多个值通过位或组成在判断是否具有某种状态时需要使用位与运算而绘制控件时我们可以使用函数DrawFrameControl,该函数可以根据指定的控件类型、控件所处的状态来绘制控件的样式,绘制出来的任然是系统的之前的标准样式,处理WM_DRAWITEN消息的具体代码如下:LPDRAWITEMSTRUCT lpdis = (LPDRAWITEMSTRUCT)lParam; char szBuf[50]; GetWindowText(lpdis->hwndItem,szBuf,50); if (ODT_BUTTON ==lpdis->CtlType)<BR>{ UINTuState = DFCS_BUTTONPUSH; if(lpdis->itemState & ODS_SELECTED) { uState |= DFCS_PUSHED; } DrawFrameControl(lpdis->hDC,&(lpdis->rcItem),DFC_BUTTON,uState); SetTextColor(lpdis->hDC,RGB(255,0,0)); DrawText(lpdis->hDC,szBuf,strlen(szBuf) + 1,&(lpdis->rcItem),DT_CENTER | DT_VCENTER | DT_SINGLELINE); }函数_TrackMouseEvent根据其检测的鼠标状态不同可以返回不同的消息,这次主要用的是WM_MOUSEHOVER(表示鼠标移动到按钮上)、WM_MOUSELEAVE(鼠标移出按钮),还需要注意的是这个函数每次检测完成返回后不会再次检测,需要我们自己主动调用函数检测鼠标状态,由于要多次调用,而每次调用都需要初始化所需要的结构体指针,所以我们封装一个函数专门用于调用_TrackMouseEvent:void Track(HWND hWnd) { TRACKMOUSEEVENT tme; tme.cbSize =sizeof(TRACKMOUSEEVENT); tme.dwFlags = TME_HOVER | TME_LEAVE; tme.dwHoverTime = 10; tme.hwndTrack = hWnd; _TrackMouseEvent(&tme); }消息WM_MOUSEHOVER和消息WM_MOUSELEAVE的处理是在对应的窗口过程中处理的,而按钮的窗口过程由系统提供我们并不知道,所以只有使用子类化的方法在我们的窗口过程中处理这两个消息。在按钮创建后立马要检测鼠标所以可以按钮对应的父窗口完成创建后子类化,对于窗口可以在它的WM_CREATE消息中处理,对于对话框可以在WM_INITDIALOG消息中处理,子类化调用函数SetWindowLong:g_OldProc = (LRESULT*)SetWindowLong(GetDlgItem(hDlg,IDC_BUTTON1),GWL_WNDPROC,(LONG)BtnProc); return0; 在新的窗口过程中处理消息,完成三态按钮:switch(uMsg) { caseWM_MOUSEMOVE: Track(hBtn);//当鼠标移动时检测 break; caseWM_MOUSEHOVER: { charszBuf[50]; RECT rtBtn;<BR> GetClientRect(hBtn,&rtBtn); HDChDc = GetDC(hBtn); DrawFrameControl(hDc,&(rtBtn),DFC_BUTTON,DFCS_BUTTONPUSH); HBRUSHhBr = CreateSolidBrush(RGB(255,255,255)); FillRect(hDc,&rtBtn,hBr); GetWindowText(hBtn,szBuf,50); SetBkMode(hDc,TRANSPARENT); DrawText(hDc,szBuf,strlen(szBuf),&rtBtn,DT_CENTER | DT_VCENTER | DT_SINGLELINE); ReleaseDC(hBtn,hDc); } break; caseWM_MOUSELEAVE: { charszBuf[50]; RECT rtBtn; GetClientRect(hBtn,&rtBtn); HDChDc = GetDC(hBtn); DrawFrameControl(hDc,&(rtBtn),DFC_BUTTON,DFCS_BUTTONPUSH); GetWindowText(hBtn,szBuf,50); SetBkMode(hDc,TRANSPARENT);//设置字体背景为透明 DrawText(hDc,szBuf,strlen(szBuf),&rtBtn,DT_CENTER | DT_VCENTER | DT_SINGLELINE); ReleaseDC(hBtn,hDc); } break; default: returnCallWindowProc((WNDPROC)g_OldProc,hBtn,uMsg,wParam, lParam);//在处理完我们感兴趣的消息后一定要记得将按钮的窗口过程还原 } return0;到这个地方为止,已经实现了三态按钮的基本样式,通过检测鼠标的位置设置按钮样式,上述代码只是改变了按钮的背景颜色和文字颜色,可能效果不好看。
2015年11月04日
6 阅读
0 评论
0 点赞
2015-10-05
程序隐藏到任务栏的实现
我们在使用软件的时候,有的软件允许最小化到任务栏,然后双击任务栏的图标时又会显示出来,这篇文章主要说明如何实现这种功能;实现这种功能主要分为两步,一是将程序窗口隐藏,二是将程序图标添加到任务栏,再次显示也是分为两步:第一步是将任务栏上的图标删除,第二步是将窗口显示出来。窗口的隐藏与显示我们用API函数ShowWindow,而添加和删除任务栏中的程序图标用的是Shell_NotifyIcon函数,ShowWindow函数平时用的比较多,而且也比较简单,这里就不在阐述,下面主要说明Shell_NotifyIcon的用法:BOOL Shell_NotifyIcon( DWORD dwMessage, PNOTIFYICONDATA lpdata ); 该函数有两个参数,第一个表示你希望对图标做何种操作主要有这几个值:NIM_ADD、NIM_DELETE、NIM_MODIFY、NIM_SETFOCUS、NIM_SETVERSION;常用的是前面3个主要是向任务栏添加图标、删除图标、修改图标;第二个参数是一个结构体该结构体的定义如下:typedef struct _NOTIFYICONDATA { DWORD cbSize;//该结构的大小 HWND hWnd; //表明当对任务栏图标进行操作是将消息发送给那个窗口 UINT uID; //应用程序的ID UINT uFlags; //一个标志 UINT uCallbackMessage;//对任务栏图标操作时向窗口发送的一个消息 HICON hIcon; //放到任务栏中的图标句柄 WCHAR szTip[64]; //当鼠标停在图标上时显示的提示信息 } NOTIFYICONDATA, *PNOTIFYICONDATA;UINT uID 参数是应用程序的ID,这个ID并不是必须的可以任意给值UINT uFlags 参数是一个标志,主要用于控制图标的行为:NIF_ICON:有这个标志hIcon才是有效值NIF_MESSAGE:有这个标志uCallbackMessage才有效,也就是说有这个标志,当我们进行操作时才有消息产生NIF_TIP:当有这个标志时szTip,才有效,才会出现提示信息;UINT uCallbackMessage:当我们对任务栏图标进行操作时会发送一条消息这个消息由用户自己定义,并且在窗口过程中处理。函数介绍完了,接下来就是实现的代码://这里是将移出图标与添加图标放到一个函数中,根据第二个参数判断是需要移出或是添加 BOOL TrackIcon(HWND hWnd, BOOL bTrak) { NOTIFYICONDATA nid = {0}; nid.cbSize = sizeof(NOTIFYICONDATA); nid.hWnd = hWnd; nid.uID = 0; if (bTrak) { nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP; nid.uCallbackMessage = WM_TRAKICON_MSG; nid.hIcon = LoadIcon(NULL, IDI_APPLICATION); _tcscpy_s(nid.szTip, sizeof(nid.szTip), _T("提示信息")); ShowWindow(hWnd, SW_MINIMIZE); ShowWindow(hWnd, SW_HIDE); return Shell_NotifyIcon(NIM_ADD, &nid); }else { //当需要移出图标时,窗口也应该完全显示,因此不需要对图标操作,后面的几个值就是无效值,这里可以不用管它们 ShowWindow(hWnd, SW_SHOWNA); return Shell_NotifyIcon(NIM_DELETE, &nid); } } //这是对我们自定义的消息进行处理,这个消息中的lParam保存了相应的消息ID case WM_TRAKICON_MSG: { switch (lParam) { case WM_LBUTTONDBLCLK: TrackIcon(hwndDlg,FALSE); ShowWindow(hwndDlg,SW_SHOWNORMAL); break; } } break;最后程序的运行结果如下:
2015年10月05日
6 阅读
0 评论
0 点赞
2015-09-21
对话框伸缩功能的实现
对话框的伸缩功能是指当触发某一操作时只显示部分对话框的内容,再次触发时显示全部的对话框范围,就比如画图软件上的选择颜色对话框,我们可以使用系统预定义的颜色,也可以选择自定义颜色,当点击自定义的按钮时,对话框出现原先隐藏的另一边,让用户填写颜色的RGB值。为了实现这个功能,我们需要记录两个矩形范围:全部对话框的大小和我们希望显示的部分对话框的大小,利用函数SetWindowPos来设置显示的对话框的大小,该函数的原型如下:BOOL SetWindowPos( HWND hWnd, // 需要设置的窗口的句柄 HWND hWndInsertAfter, // Z序中下一个窗口的句柄 int X, // int Y, // 窗口所在矩形的顶点坐标(x, y) int cx, // 矩形宽 int cy, // 矩形高 UINT uFlags // 显示属性 ); 下面是对该函数的补充:hWndInsertAfter:除了给出具体的窗口句柄外还可以是这几个值:HWND_BOTTOM、HWND_NOTOPMOST、HWND_TOP、HWND_TOPMOST;uFlags主要的一些标志: SWP_NOMOVE:调用该函数不改变窗口之前的顶点位置,当设置这个这个值的时候,x、y参数将被忽略; SWP_NOZORDER:忽略Z序,这个标志被设置时将忽略hWndInsertAfter参数;具体的信息可以在MSDN中查找;以下是具体的实现代码://按钮的WM_COMMAND消息处理 case WM_COMMAND: { if (LOWORD(wParam) == IDC_BUTTON) { if (HIWORD(wParam) == BN_CLICKED) { TCHAR szBuf[255] = TEXT(""); GetWindowText(GetDlgItem(hDlg, IDC_BUTTON), szBuf, 255); if (0 == _tcscmp(szBuf, TEXT("收缩>>"))) { SetWindowText(GetDlgItem(hDlg, IDC_BUTTON), TEXT("扩张<<")); }else { SetWindowText(GetDlgItem(hDlg, IDC_BUTTON), TEXT("收缩>>")); } Extern(hDlg, szBuf); } } }//改变对话框大小的函数 void Extern(HWND hWnd, const TCHAR *pszStr) { //保存对话框在扩张和收缩状态下的矩形大小 static RECT rtSmall; static RECT rtLarge; //当两个量不是有效值时,获取这两种状态下的矩形大小 if (IsRectEmpty(&rtLarge)) { RECT rtSpecrator; GetWindowRect(GetDlgItem(hWnd, IDC_SPERATOR), &rtSpecrator); rtSmall.left = rtLarge.left; rtSmall.top = rtLarge.top; rtSmall.right = rtSpecrator.right; rtSmall.bottom = rtSpecrator.bottom; } if (0 == _tcscmp(pszStr, TEXT("收缩>>"))) { SetWindowPos(hWnd, NULL, 0, 0, rtSmall.right - rtSmall.left, rtSmall.bottom - rtSmall.top, SWP_NOZORDER | SWP_NOMOVE); } else { SetWindowPos(hWnd, NULL, 0, 0, rtLarge.right - rtLarge.left, rtLarge.bottom - rtLarge.top, SWP_NOZORDER | SWP_NOMOVE); } }IDC_SPERATOR是一个分割线的ID,分割线我们采用的是一个图片控件,将这个控件的高度尽量缩小,这个控件本身也是一个矩形,可以用GetWindowRect函数获取它的矩形大小,缩小时只保存控件之上的部分;
2015年09月21日
5 阅读
0 评论
0 点赞
2015-09-12
socket模型处理多个客户端
最近学完了简单的socket编程,发现其实socket的网络编程其实并没有什么难度,只是简单的函数调用,记住客户端与服务端的步骤,写起来基本没有什么问题。在服务器程序的设计中,一个服务器不可能只相应一个客户端的链接,为了响应多个客户端的链接,需要使用多线程的方式,每当有一个客户端连接进来,我们就开辟一个线程,用来处理双方的交互(主要是利用recv或者recvfrom用于收发信息),由于但是在网络中可能出现这样一种情况:由于处理比较复杂,下一条信息到来之后,上一条信息的处理还没有完成,这样信息太多了之后系统的缓冲占满之后可能会发生丢包的现象,所以为了解决这个问题,需要另外再开一个线程,专门用来处理接收到的数据,这样总共至少有3个线程,主线程,收发信息的线程,处理线程;这样可能也不完整,处理的操作种类多了的话可能需要根据不同的请求来开辟不同的线程用来处理这一类请求,下面是实现这一思路的部分代码:全局变量:DWORD WINAPI AcceptThread(LPVOID lpParameter); DWORD WINAPI RecvThread(LPVOID lpParameter); DWORD g_nAcceptID = 123; DWORD g_nRecvID = 234; HANDLE g_hAccpetThread; HANDLE g_hRecvThread;主线程函数:int _tmain(int argc, _TCHAR* argv[]) { WSADATA wd; WSAStartup(MAKEWORD(2, 2), &wd); SOCKET sockListen = socket(AF_INET, SOCK_STREAM, IPPROTO_IP); if (INVALID_SOCKET == sockListen) { cout << "创建侦听套接字失败,错误码为:" << WSAGetLastError() << endl; return -1; } SOCKADDR_IN srvAddr = { 0 }; srvAddr.sin_family = AF_INET; srvAddr.sin_port = htons(6666); srvAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); if (SOCKET_ERROR == bind(sockListen, (SOCKADDR*)&srvAddr, sizeof(SOCKADDR))) { cout << "绑定失败,错误码为:" << WSAGetLastError() << endl; WSACleanup(); return -1; } if (SOCKET_ERROR == listen(sockListen, 5)) { cout << "侦听失败,错误码为:" << WSAGetLastError() << endl; WSACleanup(); return -1; } while (true) { SOCKET sockConn = accept(sockListen, NULL, 0); if (INVALID_SOCKET == sockConn) { cout << "本次连接失败,即将进入下一次连接,错误码为:" << WSAGetLastError() << endl; closesocket(sockConn); closesocket(sockListen); WSACleanup(); continue; } g_hAccpetThread = CreateThread(NULL, 0, AcceptThread, &sockConn, 0, &g_nAcceptID); } WaitForSingleObject(g_hAccpetThread, INFINITE); WSACleanup(); return 0; }收发数据函数:DWORD WINAPI AcceptThread(LPVOID lpParameter) { cout << "有客户端连接进来" << endl; SOCKET sockConn = *(SOCKET*)lpParameter; while (true) { char *pszBuf = (char*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY | HEAP_NO_SERIALIZE, 255); if (SOCKET_ERROR == recv(sockConn, pszBuf, 255, 0)) { cout << "接受数据失败,错误码为:" << WSAGetLastError() << endl; cout << "准备进行下一次接受数据....." << endl; continue; } g_hRecvThread = CreateThread(NULL, 0, RecvThread, pszBuf, 0, &g_nRecvID); WaitForSingleObject(g_hRecvThread, INFINITE); if (0 == strcmp("exit", pszBuf)) { cout << "正在断开与该客户端的连接" << endl; HeapFree(GetProcessHeap(), 255, pszBuf); return 0; } } return 0; }信息处理子线程:DWORD WINAPI RecvThread(LPVOID lpParameter) { cout << "接受到客户端的数据:" << (char*)lpParameter << endl; return 0; }虽说这个解决了多个客户端与服务器通信的问题,但是这样写确定也很明显:所有的与客户端通信的socket都有程序员自己管理,无疑加重了程序员的负担;每有一个连接都需要创建一个线程,当有大量的客户端连接进来开辟的线程数是非常多的,线程是非常耗资源的,所以为了解决这些问题就提出了异步的I/O模型,它们解决了这些问题,由系统管理套接字,不要要人为的一个个管理,同时不需要开辟多个线程来处理与客户端的连接,我们可以将线程主要用于处理客户端的请求上;
2015年09月12日
6 阅读
0 评论
0 点赞
2015-08-05
菜单的使用
# Windows菜单的基本知识:1)顶级菜单:紧贴在标题栏下面的菜单称为顶级菜单,也可以叫做程序的主菜单;2)弹出式菜单:一般在顶级菜单上都有很多菜单项,单击这些菜单项时会弹出一个下拉式的菜单项,我们点击的这个菜单称为弹出式菜单3)菜单项:每一个可选菜单项被赋予一个唯一的ID,当用户单击某个菜单项时Windows会将该菜单项的ID发送给父窗口,父窗口通过WM_COMMAND消息处理菜单的单击消息,但是弹出式菜单没有ID,WM_COMMAND消息也不处理弹出式菜单的点击信息4)菜单加速键:主要是多个键的组合,当同时按下这些键的时候相当于点击了菜单的某个菜单项5)菜单项一般具有“可用”(Enabled)、“不可用”(disabled)、“变灰”(gray)几种选项,其中变灰选项将菜单项变成不可用的同时也会将菜单项变成灰色,所以当我们需要禁用某个菜单项的时候最好将它变灰,以便提示用户;6)菜单句柄:每一种菜单都有一个菜单句柄,包括弹出式菜单的菜单项,顶级菜单,弹出式菜单;菜单的创建:Windows中菜单有两种方式,一种是通过资源的方式通过可视化或者编写rc文件来创建一个菜单资源,并在代码中显示的加载,另一种是通过调用CreateMenu、AppendMenu、InsertMenu等函数创建菜单并插入相应的菜单项,下面对这两种方式一一进行说明:1)采用rc文件的方式:可以在visual studio中利用可视化的方式编辑菜单,在这里就不在说明,而需要手工编写rc文件请参考我的另外一篇博文当我们编辑好了rc文件之后有三种方法添加菜单:通过在创建窗口类的时候在lpszMenuName项的后面添加一个用于标示菜单的字符串,若菜单使用的是ID号作为标示那么可以使用宏MAKEINTRESOURCE;在函数CreateWindow或者CreateWindowEx中的相应参数中填入菜单句柄,为了获取这个句柄需要提前使用LoadMenu函数加载菜单,这个函数的功能是将资源文件中的菜单加载到内存,并返回一个菜单句柄,函数的原型如下:HMENU LoadMenu( HINSTANCE hInstance, // 当前应用程序的实例句柄 LPCTSTR lpMenuName // 菜单唯一标示,可以是字符串或者用MAKEINTRESOURCE转化而来的字符串 );第三种方式是先通过LoadMenu函数获取菜单句柄后在窗口创建后通过SetMenu函数设置菜单,该函数用于为指定窗口加载一个顶级菜单、该函数原型如下:BOOL SetMenu( HWND hWnd, // 需加载菜单的窗口句柄 HMENU hMenu // 菜单句柄 );各个方式的源代码如下:WNDCLASS wd = {0}; wd.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); wd.hCursor = LoadCursor(NULL, IDC_ARROW); wd.hIcon = LoadIcon(NULL, IDI_APPLICATION); wd.hInstance = hInstance; wd.lpfnWndProc = WindowProc; wd.lpszClassName = "MenuClass"; //第一种方式 //wd.lpszMenuName = MAKEINTRESOURCE(IDM_MENU); wd.style = CS_HREDRAW | CS_VREDRAW; HMENU hMenu = LoadMenu(hInstance, MAKEINTRESOURCE(IDM_MENU)); //加载加速键 HACCEL hAccelerator = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDA_MAIN)); if (!RegisterClass(&wd)) { int nErr = GetLastError(); return nErr; } //第二种方式 //HWND hWnd = CreateWindow("MenuClass", "Menu", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, hMenu, hInstance, NULL); //第三种方式 HWND hWnd = CreateWindow("MenuClass", "Menu", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL); SetMenu(hWnd, hMenu);如果采用函数动态创建的方式,需要如下几个步骤:1)通过函数CreateMenu()创建一个顶级菜单;2)通过CreateMenu()创建一个弹出式菜单;3)利用AppendMenu()或者InsertMenu()向弹出式菜单中插入菜单项;4)利用AppendMenu()将弹出式菜单插入到顶级菜单中;5)用SetMenu函数将创建好的菜单加到程序下面分别说明这些函数的功能和用法:CreateMenu()用于创建一个菜单(函数会将菜单初始化为空菜单),并返回一个菜单句柄,函数原型如下:HMENU CreateMenu(VOID) AppendMenu()用于在顶级菜单、弹出式菜单的最后面的菜单项后查入新菜单项,函数原型如下: BOOL AppendMenu( HMENU hMenu, // 菜单项的句柄 UINT uFlags, // 新菜单项的类型,主要使用的是MF_STRING、MF_POUP(弹出式菜单) UINT uIDNewItem, // 新菜单项的ID,如果是弹出式菜单、则使用菜单的句柄 LPCTSTR lpNewItem //该值取决于第二个参数,若为MF_STRING则应该是一个以0结尾的字符串 ); InsterMenu()函数作用与AppendMenu相同,函数原型如下: BOOL InsertMenu( HMENU hMenu, // 菜单项的句柄 UINT uPosition, // 新菜单项的识别方式,主要有两种MF_BYCOMMAND和MF_BYPOSITION,在以后我们取菜单项的句柄或者对菜单项做其他操作,需要辨认时会有一定的作用,主要表明是靠ID号辨别还是靠在菜单中的相对位置(以0为第一个菜单项) UINT uFlags, // 新菜单项的类型,主要使用的是MF_STRING、MF_POUP(弹出式菜单) UINT uIDNewItem, // 新菜单项的ID,如果是弹出式菜单、则使用菜单的句柄 LPCTSTR lpNewItem //该值取决于第三个个参数,若为MF_STRING则应该是一个以0结尾的字符串 );下面是一个使用这种方式的例子#include <Windows.h> LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); #define IDM_FILE 100 #define IDM_ABOUT 200 #define IDM_CLOSE 300 int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd) { WNDCLASS wd = {0}; wd.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); wd.hCursor = LoadCursor(NULL, IDC_ARROW); wd.hIcon = LoadIcon(NULL, IDI_APPLICATION); wd.hInstance = hInstance; wd.lpfnWndProc = WindowProc; wd.lpszClassName = "MenuClass"; wd.style = CS_HREDRAW | CS_VREDRAW; if (!RegisterClass(&wd)) { int nErr = GetLastError(); return nErr; } HWND hWnd = CreateWindow("MenuClass", "Menu", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL); //创建主菜单 HMENU hMenu = CreateMenu(); //创建弹出式菜单 HMENU hPopup = CreateMenu(); //向弹出式菜单中插入菜单项 AppendMenu(hPopup, MF_STRING, IDM_FILE, TEXT("文件")); AppendMenu(hPopup, MF_STRING, IDM_ABOUT, TEXT("关于")); InsertMenu(hPopup, MF_BYCOMMAND, MF_STRING, IDM_CLOSE, TEXT("关闭")); //将弹出式菜单插入到主菜单中 AppendMenu(hMenu, MF_POPUP,(UINT_PTR)hPopup, TEXT("系统")); SetMenu(hWnd,hMenu); if (NULL == hWnd) { int nErr = GetLastError(); return nErr; } ShowWindow(hWnd, nShowCmd); MSG msg; while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return msg.wParam; } LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { case WM_COMMAND: { if (IDM_ABOUT == LOWORD(wParam)) { MessageBox(hWnd, TEXT("About"), TEXT("TEST"), MB_OK); } } break; case WM_CLOSE: DestroyWindow(hWnd); break; case WM_DESTROY: PostQuitMessage(0); break; default: return DefWindowProc(hWnd, uMsg, wParam, lParam); } return 0; }键菜单的使用:创建一个右键菜单有如下步骤(在WM_RBUTTONDOWN消息下处理):1)创建一个可用的菜单(一般是主菜单);2)根据主菜单获取弹出式菜单的句柄,使用函数GetSubMenu()2)加载菜单项3)获取鼠标点击的位置4)将客户区坐标转化为屏幕坐标(这一步千万别忘了)5)调用TrackPopupMenu函数,该函数用来显示一个快捷菜单,这个函数中需要填入菜单显示的位置,这个位置值为屏幕坐标,这也就是我们为什么需要转化坐标的原因;该函数的原型为:BOOL TrackPopupMenu( HMENU hMenu, // 快捷菜单的句柄 UINT uFlags, // 快捷菜单显示的类型 int x, // int y, //菜单显示点的坐标,根据第二个参数确定如何显示,一般有左对齐(最左边顶点为该坐标)、右对齐(右上角坐标为该坐标)、中间对齐(上边线的中点坐标为该坐标); int nReserved, // 该参数必须给0 HWND hWnd, // 显示快捷菜单的窗口句柄 CONST RECT *prcRect // 该参数被忽略,一般给NNULL );下面是一段例子代码:HMENU hMenu = LoadMenu(GetModuleHandle(NULL), MAKEINTRESOURCE(IDM_MENU)); hMenu = GetSubMenu(hMenu, 0); POINT ptChick = {LOWORD(lParam), HIWORD(lParam)}; ClientToScreen(hWnd, &ptChick); TrackPopupMenu(hMenu, TPM_LEFTALIGN, ptChick.x, ptChick.y, 0, hWnd, NULL);其他菜单操作的函数主要有:GetSystemMenu()获取系统菜单句柄;Deletemenu()从菜单中删除某一菜单项并销毁它RemoveMenu()从菜单中移出某一菜单项但不销毁它InsertMenu()在菜单中插入一个菜单项NodifyMenu()修改一个已存在的菜单项
2015年08月05日
6 阅读
0 评论
0 点赞
2015-07-15
Windows程序设计学习笔记(五)——菜单资源和加速键的使用
菜单可能是Windows提供的统一用户界面中最重要的一种方式,菜单通常在标题栏的下一行显示,这一栏叫做菜单栏,菜单栏中的每一项称之为菜单项,菜单栏中的每一个菜单项在激活时会显现一个下拉菜单(也可以说是它的子菜单),下拉菜单中也可以有多个菜单项,每个菜单项又可以有子菜单,每个菜单项都有一个唯一的数字标示,称为菜单项的ID,但是有子菜单的菜单项没有ID。用户点击某项后,会产生一个WM_COMMAND消息发送到其父窗口,该消息中包含了这个菜单项的ID。菜单的创建可以通过可视化的方法创建,也可以通过编写资源脚本的方式创建菜单资源,在这里重点说明如何通过脚本编写的方式创建菜单//Menu IDM_MENU MENU BEGIN POPUP "文件(&F)" BEGIN MENUITEM "打开(&O)", IDM_OPEN MENUITEM "关闭(&C)", IDM_OPTION MENUITEM SEPARATOR MENUITEM "关闭(&X)", IDM_EXIT END POPUP "查看(&V)" BEGIN MENUITEM "字体(&V)...\tAlt + F", IDM_SETFONT MENUITEM "背景色(&B)...\tCtrl + Alt + B", 40009 MENUITEM SEPARATOR MENUITEM "被禁用的菜单项", ID_40010, INACTIVE MENUITEM "变绘的菜单项", ID_40011, GRAYED MENUITEM "大图标(&G)", 40012 MENUITEM "小图标(&M)", IDM_SMALL MENUITEM "列表(&L)", 40015 MENUITEM SEPARATOR MENUITEM "详细信息(&D)", IDM_DETAIL POPUP "工具栏" BEGIN MENUITEM "标准按钮(&S)", 40019 MENUITEM "文字标签(&C)", 40020 MENUITEM "命令栏(&I)", 40021 END MENUITEM "状态栏(&U)", 40022 END POPUP "帮助(&H)", HELP BEGIN MENUITEM "帮助主题(&H)\tF1", IDM_HELP MENUITEM "关于本程序(&A)...", 40025 END END IDA_MAIN ACCELERATORS BEGIN VK_F1, IDM_HELP, VIRTKEY, NOINVERT "B", IDM_SETCOLOR, VIRTKEY, CONTROL, ALT, NOINVERT "F", IDM_SETFONT, VIRTKEY, ALT, NOINVERT END下面来分析这段代码:首先是通过一些列的宏定义来定义各种菜单项的ID,菜单ID用于唯一标识一个菜单项,不同的菜单项所用的ID号应该不同除非这些菜单项完成相同的工作,菜单项的ID可以是16位的整数,同时菜单项也可以用字符串来表示,在调用相应的API函数的时候检测到这个值大于10000h的时候将它作为字符串指针,这个时候用字符串唯一标示菜单项,当这个数小于10000h时表示的是一个数字,这个时候用数字唯一标示。菜单在脚本中的定义格式为:菜单ID MENU [DISCARDABLE] BEGIN 菜单项的定义 END菜单ID:每个菜单都有的一个唯一的标示,可以是字符串,可以是数字。DISCARDABLE:菜单的内存属性,标示菜单在不再使用的时候可以暂时从内存中释放以节省内存菜单项的定义方法有3种分别对应不同类型的菜单项:MENUITEM 菜单文字,命令ID, [选项列表](用法1) MENUITEM SEPARATOR (用法2) popup 菜单文字 [,选项] BEGIN MENUITEM 菜单文字,命令ID, [选项列表] ......................... END (用法3)用法1:用于创建一个菜单项;用法2:用于创建一个分割符;用法3:用于创建一个菜单项的子菜单项;菜单文字:显示在菜单项上的文字,需要字符串中某个字母带下划线的话,可以在字母前面加上一个&符号,比如上面的“状态栏(&U)”,带下划线的字母被系统当做快捷键,比如我们点击查看菜单项,打开它的子菜单,在按下字母U就相当于直接点击菜单中的状态栏一项;命令ID:上述我们定义的菜单ID项,父窗口的WM_COMMAND消息的参数中带有这个值,通过这个值判断是哪个菜单项被点击;选项列表:用来定义菜单项的各种属性,他可以是下面的值:CHECKED——表示打上选定标志(菜单项前有一个钩)GRAYED——菜单项变灰INAVTIVE——菜单项不可用MENUBREAK或者MENUBARBREAK——表示这个菜单项和以后的菜单项在新的一列显示;对于popup后面的选项可以是下面值的一个:GRAYED——菜单项变灰INAVTIVE——菜单项不可用HELP——菜单项靠右边显示快捷键的定义格式为:快捷键ID ACCELERATORSBEGIN 键名, 命令 [, 类型] [,选项] END键名:表示加速键对应的按键,可以有3中方式定义:“^字母” :表示Ctrl加上字母”字母“:表示字母,这时类型必须指明为VIRTKEY数值:表示ASCii码为该数值的字母,这个时候类型必须指明为ASCii命令ID:按下加速键以后Windows向程序发送的命令ID,如果想把加速键和菜单项关联起来,这里就是相应的菜单项的ID类型:用以指定键的定义方式,可以是ASCii或者VIRTKEY选项:可以是Alt、control、shift中的一个或多个,表示这些键和键名定义的键一起组成一个快捷键菜单项的消息响应:菜单项的处理一般由菜单父窗口处理,菜单被选中中时会向其父窗口发送一条WM_COMMAND的消息,将该项的相关信息告诉给其父窗口,该消息的说明如下:WM_COMMAND wNotifyCode = HIWORD(wParam); // 通知码 若对应的资源为加速键该值为1,若为菜单项则为0 wID = LOWORD(wParam); // 菜单项、加速键、控件的ID hwndCtl = (HWND) lParam; // 控件句柄我们可以在WM_COMMAND消息的处理中添加如下的内容,让其显示我们选中的是那一项:if (IDM_HELP == LOWORD(wParam)) { MessageBox(hWnd, "您选中了帮助主题菜单项","提示", MB_OK); }当选择“帮助主题”的时候,会弹出一个消息框,如果按下F1键也会显示这样一个消息框,因为我们已经将加速键绑定到对应的菜单项上面。
2015年07月15日
6 阅读
0 评论
0 点赞
1
...
3
4
5