首页
归档
友情链接
关于
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
elisp
文本编辑器
Java
反汇编
OLEDB
数据库编程
数据结构
内核编程
Masimaro
累计撰写
315
篇文章
累计收到
31
条评论
首页
栏目
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
Thinking
FIRE
菜谱
页面
归档
友情链接
关于
搜索到
147
篇与
的结果
2018-02-12
数据更新接口与延迟更新
title: 数据更新接口与延迟更新tags: [OLEDB, 数据库编程, VC++, 数据库]date: 2018-02-12 14:29:35categories: windows 数据库编程keywords: OLEDB, 数据库编程, VC++, 数据库,数据库数据更新, 延迟提交在日常使用中,更新数据库数据经常使用delete 、update等SQL语句进行,但是OLEDB接口提供了额外的接口,来直接修改和更新数据库数据。OLEDB数据源更新接口为何不使用SQL语句进行数据更新常规情况下,使用SQL语句比较简单,利用OLEDB中执行SQL语句的方法似乎已经可以进行数据库的任何操作,普通的增删改查操作似乎已经够用了。确实,在某种情况下,这些内容已经够了,能够执行SQL语句并得到结果集已经够了,但是某些情况下并不合适使用SQL语句。SQL语句的执行一般经过这样几个步骤:数据库通过sql语句对SQL语句进行分析,生成一些可以被数据库识别的步骤,在这里我们叫它计划任务数据库根据计划任务中的相关操作,调用对应的核心组件来执行SQL语句中规定的操作。将操作得到的结果集返回到应用程序我们可以简单的将SQL语句理解为一种运行在数据库平台上的一个脚本语言,它与一般的脚本语言一样需要对每句话进行解释执行。根据解释的内容调用对应的功能模块来完成用户的请求。如果我们能够跳过SQL语句的解释,直接调用对应的核心组件,那么就能大幅度的提升程序的性能。OLEDB中的数据更新的相关接口就是完成这个操作的。至此我们可能有点明白为什么不用SQL语句而是用OLEDB的相关接口来实现对应的更新操作。主要是为了提高效率。数据修改的相关接口数据修改的相关接口主要是IRowsetChange,接口中提供的主要方法如下:DeleteRows: 删除某行InsertRows: 插入行SetData: 更新行通过这些接口就可以直接对数据库进行相关的增删改操作,而由于查询的操作条件复杂特别是涉及到多表查询的时候,所以OLEDB没有提供对应的查询接口。更新数据更新数据需要IRowsetChange接口,而打开该接口需要设置结果集的相关属性。一般需要设置下面两个属性:DBPROP_UPDATABILITY:该属性表示是否允许对数据进行更新,它是一个INT型的数值,该属性有3个标志的候选值:DBPROPVAL_UP_CHANGE,允许对数据进行更新,也就是打开SetData方法;DBPROPVAL_UP_DELETE:允许对数据进行删除,打开的是DeleteRows方法。DBPROPVAL_UP_INSERT,允许插入新行,该标志打开InsertRows方法。这3个值一般使用或操作设置对应的标识位。DBPROP_IRowsetUpdate:该属性是一个BOOL值,表示是否打开IRowsetChange接口。如果要使用IRowsetChange接口,需要将该属性设置为TRUE。它们属于属性集DBPROPSET_ROWSET。使用命令对象来设置设置完属性后,调用Execute执行SQL语句并获取到接口IRowsetChange。 PS:使用IRowsetChange接口有一个特殊的要求,必须使用IRowsetChange替代IRowset等接口,作为创建并打开结果集对象的第一个接口。也就是说Execute方法中的最后一个表示结果集对象的参数必须是IRowsetChange。数据更新模式一般来说,使用OLEDB的接口对数据库中的数据进行操作时,操作的结果是实时的反映到数据库中的。对于一般的应用程序来说。采用数据更新的接口虽然在一定程度上解决的效率的问题,但是使用实时更新的模式仍然有一些问题:修改立即反映到数据库中,不利于数据库中数据完整性维护和数据安全如果是网络中的数据库,会形成很多小的网络数据包传输,造成网络传输效率低下。因此OLEDB提供了另外一种更新模式——延迟更新延迟更新延迟更新本质上提供了一种将所有更新都在本地中缓存起来,最后再一口气将所有更新都一次性提交的机制,它与数据库中的事务不同,事务是将一组操作组织起来,当其中某一步操作失败,那么回滚事务中的所有数据,强调的是一个完整性维护,而延迟提交并不会自己校验某一步是否错误,它强调的是将某些更改一次性的提交。延迟提交与实时提交有下面几个优点:当多个客户端都在修改数据库中的数据时,有机会将某些客户端对数据的修改通知到其他客户端。可以合并对一行数据多列的修改并一次提交到数据源上网络数据库中可以将对不同表的不同操作合并成一个大的网络数据包,提高网络的使用效率。当更新不合适的时候有机会进行回滚打开延迟更新的接口要使用延迟更新必须申请打开OLEDB的IRowsetUpdate接口,这个申请主要通过设置结果集的DBPROP_IRowsetUpdate属性来实现,这个属性是一个bool类型的值。同时要打开延迟更新的接口必须先打开IRowsetChange接口,所以它们二者一般都是同时打开的。另外为了方便操作一般也会将结果集的DBPROP_CANHOLDROWS属性打开,该属性允许在上一次对某行数据的更改未提交的情况下插入新行。如果不设置该属性,那么在调用SetData方法进行更新后就必须调用IRowsetUpdate的Update接口进行提交,否则在提交之前数据库不允许进行Insert操作(但是允许进行SetData操作)在使用延迟更新的时候需要注意一个问题。当我们使用了DBPROP_CANHOLDROWS属性后,数据源为了维护方便,会额外返回一个第0行的数据。通常改行数据是一个INT型,由数据提供者进行维护,它一般是只读的,如果尝试对它进行修改可能会返回一个错误造成对数据的其他修改操作也失败。在这种情况下,可以考虑建立2个访问器,一个包含第0行,只用来做显示使用,而另外一个更新的访问器不绑定第0行。一般情况下可以通过检测返回结果集中的列信息中的标志字段来确定哪些列可以进行变更,哪些列是只读列等标志来创建多个不同用途的行访问器下面是延迟更新的例子:BOOL ExecSql(IOpenRowset *pIOpenRowset, IRowsetChange* &pIRowsetChange) { COM_DECLARE_BUFFER(); COM_DECALRE_INTERFACE(IDBCreateCommand); COM_DECALRE_INTERFACE(ICommandText); COM_DECALRE_INTERFACE(ICommandProperties); LPOLESTR lpSql = OLESTR("select * from aa26;"); BOOL bRet = FALSE; //TODO:query出ICommandProperties接口并设置对应属性 //设置属性,允许返回的结果集对数据进行增删改操作 dbProp[0].colid = DB_NULLID; dbProp[0].dwOptions = DBPROPOPTIONS_REQUIRED; dbProp[0].dwPropertyID = DBPROP_UPDATABILITY; dbProp[0].vValue.vt = VT_I4; dbProp[0].vValue.intVal = DBPROPVAL_UP_CHANGE | DBPROPVAL_UP_DELETE | DBPROPVAL_UP_INSERT; //设置属性,打开IRowsetUpdate接口实现延迟更新 dbProp[1].colid = DB_NULLID; dbProp[1].dwOptions = DBPROPOPTIONS_REQUIRED; dbProp[1].dwPropertyID = DBPROP_IRowsetUpdate; dbProp[1].vValue.vt = VT_BOOL; dbProp[1].vValue.boolVal = VARIANT_TRUE; //设置属性,允许结果集在不提交上次行的更改的情况下插入行 dbProp[2].colid = DB_NULLID; dbProp[2].dwOptions = DBPROPOPTIONS_REQUIRED; dbProp[2].dwPropertyID = DBPROP_CANHOLDROWS; dbProp[2].vValue.vt = VT_BOOL; dbProp[2].vValue.boolVal = VARIANT_TRUE; dbPropset[0].cProperties = 3; dbPropset[0].guidPropertySet = DBPROPSET_ROWSET; dbPropset[0].rgProperties = dbProp; hRes = pICommandProperties->SetProperties(1, dbPropset); COM_SUCCESS(hRes, _T("设置属性失败,错误码为:%08x\n"), hRes); hRes = pICommandText->SetCommandText(DBGUID_DEFAULT, lpSql); COM_SUCCESS(hRes, _T("设置SQL失败,错误码为:%08x\n"), hRes); hRes = pICommandText->Execute(NULL, IID_IRowsetChange, NULL, NULL, (IUnknown**)&pIRowsetChange); COM_SUCCESS(hRes, _T("执行SQL语句失败,错误码为:%08x\n"), hRes); bRet = TRUE; __CLEAR_UP: SAFE_RELEASE(pIDBCreateCommand); SAFE_RELEASE(pICommandText); SAFE_RELEASE(pICommandProperties); return bRet; } int _tmain(int argc, TCHAR* argv[]) { CoInitialize(NULL); COM_DECLARE_BUFFER(); COM_DECALRE_INTERFACE(IOpenRowset); COM_DECALRE_INTERFACE(IRowsetChange); COM_DECALRE_INTERFACE(IColumnsInfo); COM_DECALRE_INTERFACE(IAccessor); COM_DECALRE_INTERFACE(IRowset); COM_DECALRE_INTERFACE(IRowsetUpdate); HRESULT hRes = S_FALSE; DBORDINAL cColumns = 0; DBCOLUMNINFO* ColumnsInfo = NULL; OLECHAR *pColumnsName = NULL; DBBINDING *pDBBindinfo = NULL; HACCESSOR hAccessor = NULL; HROW* phRow = NULL; HROW hNewRow = NULL; DBCOUNTITEM cRows = 10; //一次读取10行 DBCOUNTITEM dbObtained = 0; //真实读取的行数 LPVOID pInsertData = NULL; LPVOID pData = NULL; LPVOID pCurrData = NULL; DWORD dwOffset = 0; if(!CreateSession(pIOpenRowset)) { COM_PRINTF(_T("创建回话对象失败\n")); goto __CLEAR_UP; } if (!ExecSql(pIOpenRowset, pIRowsetChange)) { COM_PRINTF(_T("执行SQL语句失败\n")); goto __CLEAR_UP; } hRes = pIRowsetChange->QueryInterface(IID_IRowset, (void**)&pIRowset); COM_SUCCESS(hRes, _T("查询接口IRowsets失败,错误码为:%08x\n"), hRes); hRes = pIRowset->QueryInterface(IID_IColumnsInfo, (void**)&pIColumnsInfo); COM_SUCCESS(hRes, _T("查询接口IColumnsInfo失败,错误码为:%08x\n"), hRes); hRes = pIColumnsInfo->GetColumnInfo(&cColumns, &ColumnsInfo, &pColumnsName); COM_SUCCESS(hRes, _T("获取结果集列信息失败,错误码为:%08x\n"), hRes); pDBBindinfo = (DBBINDING*)MALLOC(sizeof(DBBINDING) * (cColumns - 1)); for(int i = 0; i < cColumns; i++) { //取消第0行的绑定,防止修改数据时出错 if (ColumnsInfo[i].iOrdinal == 0) { continue; } pDBBindinfo[i - 1].iOrdinal = ColumnsInfo[i].iOrdinal; pDBBindinfo[i - 1].bPrecision = ColumnsInfo[i].bPrecision; pDBBindinfo[i - 1].bScale = ColumnsInfo[i].bScale; pDBBindinfo[i - 1].cbMaxLen = ColumnsInfo[i].ulColumnSize * sizeof(WCHAR); if (ColumnsInfo[i].wType == DBTYPE_I4) { //整型数据转化为字符串时4个字节不够,需要根据表中的长度来分配对应的空间 //数据库中表示行政单位的数据长度为6个字节 pDBBindinfo[i - 1].cbMaxLen = 7 * sizeof(WCHAR); } pDBBindinfo[i - 1].dwMemOwner = DBMEMOWNER_CLIENTOWNED; pDBBindinfo[i - 1].dwPart = DBPART_STATUS | DBPART_LENGTH | DBPART_VALUE; pDBBindinfo[i - 1].eParamIO = DBPARAMIO_NOTPARAM; pDBBindinfo[i - 1].obStatus = dwOffset; pDBBindinfo[i - 1].obLength = dwOffset + sizeof(DBSTATUS); pDBBindinfo[i - 1].obValue = dwOffset + sizeof(DBSTATUS) + sizeof(ULONG); pDBBindinfo[i - 1].wType = DBTYPE_WSTR; //为了方便,类型由数据库进行转化 dwOffset = dwOffset + sizeof(DBSTATUS) + sizeof(ULONG) + pDBBindinfo[i - 1].cbMaxLen; dwOffset = COM_UPROUND(dwOffset, 8); //8字节对齐 } //读取数据 hRes = pIRowsetChange->QueryInterface(IID_IAccessor, (void**)&pIAccessor); COM_SUCCESS(hRes, _T("查询IAccessor接口失败,错误码为:%08x\n"), hRes); hRes = pIAccessor->CreateAccessor(DBACCESSOR_ROWDATA, cColumns - 1, pDBBindinfo, 0, &hAccessor, NULL); COM_SUCCESS(hRes, _T("创建访问器失败,错误码为:%08x\n"), hRes); hRes = pIRowset->GetNextRows(DB_NULL_HCHAPTER, 0, cRows, &dbObtained, &phRow); COM_SUCCESS(hRes, _T("获取行数据失败,错误码为:%08x\n"), hRes); pData = MALLOC(cRows * dwOffset); for (int i = 0; i < dbObtained; i++) { pCurrData = (BYTE*)pData + dwOffset * i; hRes = pIRowset->GetData(phRow[i], hAccessor, pCurrData); //获取当前行 COM_SUCCESS(hRes, _T("获取数据失败, 错误码为:%08x\n"), hRes); DisplayColumnData(pDBBindinfo, cColumns - 1, pCurrData); // 将前10行第3列的数据修改为中国 LPOLESTR pUpdateData = (LPOLESTR)((BYTE*)pCurrData + pDBBindinfo[1].obValue); ULONG* pLen = (ULONG*)((BYTE*)pCurrData + pDBBindinfo[1].obLength); *pLen = 4; StringCchCopy(pUpdateData, pDBBindinfo[1].cbMaxLen, _T("中国")); hRes = pIRowsetChange->SetData(phRow[i], hAccessor, pCurrData); COM_SUCCESS(hRes, _T("修改数据失败, 错误码为:%08x\n"), hRes); } //插入一行数据 pInsertData = MALLOC(dwOffset); ZeroMemory(pInsertData, dwOffset); //为了方便直接在原来数据的基础上进行修改 CopyMemory(pInsertData, pData, dwOffset); DBSTATUS status[4] = {0}; ULONG uSize[4] = {0}; LPOLESTR lpValues[4] = {0}; for (int i = 0; i < 4; i++) { status[i] = *(DBSTATUS*)((BYTE*)pInsertData + pDBBindinfo[i].obStatus); uSize[i] = *(DBSTATUS*)((BYTE*)pInsertData + pDBBindinfo[i].obLength); lpValues[i] = (LPOLESTR)((BYTE*)pInsertData + pDBBindinfo[i].obValue); } StringCchCopy(lpValues[0], pDBBindinfo[0].cbMaxLen, _T("100001")); StringCchCopy(lpValues[1], pDBBindinfo[1].cbMaxLen, _T("测试")); hRes = pIRowsetChange->InsertRow(NULL, hAccessor, pInsertData, &hNewRow); COM_SUCCESS(hRes, _T("插入数据失败, 错误码为:%08x\n"), hRes); //删除一行数据 hRes = pIRowsetChange->DeleteRows(NULL, 1, &phRow[9], NULL); COM_SUCCESS(hRes, _T("查询IRowsetChange接口失败,错误码为:%08x\n"), hRes); hRes = pIRowsetChange->QueryInterface(IID_IRowsetUpdate, (void**)&pIRowsetUpdate); COM_SUCCESS(hRes, _T("查询IRowsetChange接口失败,错误码为:%08x\n"), hRes); ////提交上述修改 hRes = pIRowsetUpdate->Update(DB_NULL_HCHAPTER, 0, NULL, NULL, NULL, NULL); COM_SUCCESS(hRes, _T("提交数据更新失败, 错误码为:%08x\n"), hRes); //取消修改 //hRes = pIRowsetUpdate->Undo(DB_NULL_HCHAPTER, 0, NULL, NULL, NULL, NULL); COM_SUCCESS(hRes, _T("取消更新失败, 错误码为:%08x\n"), hRes); pIRowset->ReleaseRows(dbObtained, phRow, NULL, NULL, NULL); pIRowset->ReleaseRows(1, &hNewRow, NULL, NULL, NULL); __CLEAR_UP: SAFE_RELEASE(pIOpenRowset); SAFE_RELEASE(pIRowsetChange); SAFE_RELEASE(pIColumnsInfo); SAFE_RELEASE(pIRowsetUpdate); SAFE_RELEASE(pIRowset); SAFE_RELEASE(pIAccessor); CoTaskMemFree(ColumnsInfo); CoTaskMemFree(pColumnsName); FREE(pInsertData); FREE(pData); FREE(pDBBindinfo); CoUninitialize(); return 0; }在例子中仍然是首先进行数据库连接,创建出Session对象,创建Command对象,设置结果集对象的相关属性并执行sql语句。但是与之前不同的是,在执行SQL语句时不再返回IRowset接口而是返回IRowsetChange接口。然后利用IRowsetChange接口Query出其他需要的接口。接着仍然是绑定,与之前不同的是,在绑定中加了一个判断。跳过了第0行的绑定,以免它影响到后面的更新操作,然后打印输出对应的查询结果。并且在显示每行数据之后,调用SetData对数据进行更改。接着准备一个对应的缓冲,放入插入新行的数据。在这为了方便我们直接先拷贝了之前的返回的结果集中的一行的数据,然后再在里面进行修改,修改后调用InsertRows,插入一行数据。插入操作完成后,调用DeleteRows函数删除一行数据最后调用IRowsetUpdate接口的Update方法提交之前的更新,或者调用Undo取消之前的更改源代码查看
2018年02月12日
4 阅读
0 评论
0 点赞
2018-01-28
SQL语句执行与结果集的获取
上次说到命令对象是用来执行SQL语句的。数据源在执行完SQL语句后会返回一个结果集对象,将SQL执行的结果返回到结果集对象中,应用程序在执行完SQL语句后,解析结果集对象中的结果,得到具体的结果,这次的主要内容是如何解析结果集对象并获取其中的值。如何执行SQL语句执行SQL语句一般的步骤如下:创建ICommandText接口.使用ICommandText接口的SetCommandText方法设置SQL命令使用ICommandText接口的Excute方法执行SQL语句并接受返回的结果集对象,这个结果集对象一般是IRowset.其实OLEDB并不一定非要传入SQL语句,他可以传入简单的命令,只要数据源能够识别,也就是说我们可以根据数据源的不同传入那些只有特定数据源才会支持的命令,已达到简化操作或者实现某些特定功能的目的.针对有的SQL语句,我们并不是那么关心它返回了那些数据,比如说Delete语句,insert语句,针对这种情况我们可以将对应返回结果集的参数设置为NULL,比如像下面这样pICommandText->Execute(NULL, IID_NULL, NULL, NULL, NULL)明确告诉数据源程序不需要返回结果集,这样数据源不会准备结果集,减少了数据源的相关操作,从某种程度上减轻了数据源的负担。设置command对象的属性与之前数据源对象和会话对象的属性不同,command对象的属性是作用在返回的数据源对象上的,比如我们没有设置对应的更新属性,那么数据源就不允许我们使用结果集进行更新数据的操作。这些属性必须在执行SQL语句得到结果集的操作之前定义好。因为在获得数据源返回的结果集的时候数据源已经设置了对应的属性。command对象的属性集ID是PROPSET_ROWSET.该属性集中有很多能够影响结果集对象的属性。下面是一个执行SQL语句的例子: LPOLESTR lpSql = OLESTR("select * from aa26"); CreateDBSession(pIOpenRowset); HRESULT hRes = pIOpenRowset->QueryInterface(IID_IDBCreateCommand, (void**)&pIDBCreateCommand); COM_SUCCESS(hRes, _T("查询接口IDBCreateCommand失败,错误码:%08x\n"), hRes); hRes = pIDBCreateCommand->CreateCommand(NULL, IID_ICommandText, (IUnknown**)&pICommandText); COM_SUCCESS(hRes, _T("创建接口IDBCreateCommand失败,错误码:%08x\n"), hRes); hRes = pICommandText->SetCommandText(DBGUID_DEFAULT, lpSql); COM_SUCCESS(hRes, _T("设置sql语句失败,错误码:%08x\n"), hRes); DBPROP dbProp[16] = {0}; DBPROPSET dbPropset[1] = {0}; //设置结果集可以进行增删改操作 dbProp[0].colid = DB_NULLID; dbProp[0].dwOptions = DBPROPOPTIONS_REQUIRED; dbProp[0].dwPropertyID = DBPROP_UPDATABILITY; dbProp[0].vValue.vt = VT_I4; dbProp[0].vValue.lVal = DBPROPVAL_UP_DELETE | DBPROPVAL_UP_CHANGE | DBPROPVAL_UP_INSERT; //申请打开书签功能 dbProp[1].colid = DB_NULLID; dbProp[1].dwOptions = DBPROPOPTIONS_OPTIONAL; dbProp[1].dwPropertyID = DBPROP_BOOKMARKS; dbProp[1].vValue.vt = VT_BOOL; dbProp[1].vValue.boolVal = VARIANT_TRUE; //申请打开行查找功能 dbProp[2].colid = DB_NULLID; dbProp[2].dwOptions = DBPROPOPTIONS_OPTIONAL; dbProp[2].dwPropertyID = DBPROP_IRowsetFind; dbProp[2].vValue.vt = VT_BOOL; dbProp[2].vValue.boolVal = VARIANT_TRUE; //申请打开行索引 dbProp[3].colid = DB_NULLID; dbProp[3].dwOptions = DBPROPOPTIONS_OPTIONAL; dbProp[3].dwPropertyID = DBPROP_IRowsetIndex; dbProp[3].vValue.vt = VT_BOOL; dbProp[3].vValue.boolVal = VARIANT_TRUE; //申请打开行定位功能 dbProp[4].colid = DB_NULLID; dbProp[4].dwOptions = DBPROPOPTIONS_OPTIONAL; dbProp[4].dwPropertyID = DBPROP_IRowsetLocate; dbProp[4].vValue.vt = VT_BOOL; dbProp[4].vValue.boolVal = VARIANT_TRUE; //申请打开行滚动功能 dbProp[5].colid = DB_NULLID; dbProp[5].dwOptions = DBPROPOPTIONS_OPTIONAL; dbProp[5].dwPropertyID = DBPROP_IRowsetScroll; dbProp[5].vValue.vt = VT_BOOL; dbProp[5].vValue.boolVal = VARIANT_TRUE; //申请打开行集视图功能 dbProp[6].colid = DB_NULLID; dbProp[6].dwOptions = DBPROPOPTIONS_OPTIONAL; dbProp[6].dwPropertyID = DBPROP_IRowsetView; dbProp[6].vValue.vt = VT_BOOL; dbProp[6].vValue.boolVal = VARIANT_TRUE; //申请打开行集刷新功能 dbProp[7].colid = DB_NULLID; dbProp[7].dwOptions = DBPROPOPTIONS_OPTIONAL; dbProp[7].dwPropertyID = DBPROP_IRowsetRefresh; dbProp[7].vValue.vt = VT_BOOL; dbProp[7].vValue.boolVal = VARIANT_TRUE; //申请打开列信息扩展接口 dbProp[8].colid = DB_NULLID; dbProp[8].dwOptions = DBPROPOPTIONS_OPTIONAL; dbProp[8].dwPropertyID = DBPROP_IColumnsInfo2; dbProp[8].vValue.vt = VT_BOOL; dbProp[8].vValue.boolVal = VARIANT_TRUE; //申请打开数据库同步状态接口 dbProp[9].colid = DB_NULLID; dbProp[9].dwOptions = DBPROPOPTIONS_OPTIONAL; dbProp[9].dwPropertyID = DBPROP_IDBAsynchStatus; dbProp[9].vValue.vt = VT_BOOL; dbProp[9].vValue.boolVal = VARIANT_TRUE; //申请打开行集分章功能 dbProp[10].colid = DB_NULLID; dbProp[10].dwOptions = DBPROPOPTIONS_OPTIONAL; dbProp[10].dwPropertyID = DBPROP_IChapteredRowset; dbProp[10].vValue.vt = VT_BOOL; dbProp[10].vValue.boolVal = VARIANT_TRUE; dbProp[11].colid = DB_NULLID; dbProp[11].dwOptions = DBPROPOPTIONS_OPTIONAL; dbProp[11].dwPropertyID = DBPROP_IRowsetCurrentIndex; dbProp[11].vValue.vt = VT_BOOL; dbProp[11].vValue.boolVal = VARIANT_TRUE; dbProp[12].colid = DB_NULLID; dbProp[12].dwOptions = DBPROPOPTIONS_OPTIONAL; dbProp[12].dwPropertyID = DBPROP_IGetRow; dbProp[12].vValue.vt = VT_BOOL; dbProp[12].vValue.boolVal = VARIANT_TRUE; //申请打开行集更新功能 dbProp[13].colid = DB_NULLID; dbProp[13].dwOptions = DBPROPOPTIONS_OPTIONAL; dbProp[13].dwPropertyID = DBPROP_IRowsetUpdate; dbProp[13].vValue.vt = VT_BOOL; dbProp[13].vValue.boolVal = VARIANT_TRUE; dbProp[14].colid = DB_NULLID; dbProp[14].dwOptions = DBPROPOPTIONS_OPTIONAL; dbProp[14].dwPropertyID = DBPROP_IConnectionPointContainer; dbProp[14].vValue.vt = VT_BOOL; dbProp[14].vValue.boolVal = VARIANT_TRUE; dbPropset[0].cProperties = 15; dbPropset[0].guidPropertySet = DBPROPSET_ROWSET; dbPropset[0].rgProperties = dbProp; hRes = pICommandText->QueryInterface(IID_ICommandProperties, (void**)&pICommandProperties); COM_SUCCESS(hRes, _T("查询接口ICommandProperties失败,错误码:%08x\n"), hRes); hRes = pICommandProperties->SetProperties(1, dbPropset); COM_SUCCESS(hRes, _T("设置属性失败,错误码:%08x\n"), hRes); hRes = pICommandText->Execute(NULL, IID_IRowset, NULL, NULL, (IUnknown**)&pIRowset); COM_SUCCESS(hRes, _T("执行sql语句失败,错误码:%08x\n"), hRes);这段代码详细的展示了如何执行SQL语句获取结果集并设置COMMANDUI对象的属性。结果集对象结果集一般是执行完SQL语句后返回的一个代表二维结构化数组的对象。这个结构化对象可以理解为一个与数据表定义相同的一个结构体。而结果集中保存了这个结构体的指针下面是结果集对象的详细定义CoType TRowset { [mandatory] interface IAccessor; [mandatory] interface IColumnsInfo; [mandatory] interface IConvertType; [mandatory] interface IRowset; [mandatory] interface IRowsetInfo; [optional] interface IChapteredRowset; [optional] interface IColumnsInfo2; [optional] interface IColumnsRowset; [optional] interface IConnectionPointContainer; [optional] interface IDBAsynchStatus; [optional] interface IGetRow; [optional] interface IRowsetChange; [optional] interface IRowsetChapterMember; [optional] interface IRowsetCurrentIndex; [optional] interface IRowsetFind; [optional] interface IRowsetIdentity; [optional] interface IRowsetIndex; [optional] interface IRowsetLocate; [optional] interface IRowsetRefresh; [optional] interface IRowsetScroll; [optional] interface IRowsetUpdate; [optional] interface IRowsetView; [optional] interface ISupportErrorInfo; [optional] interface IRowsetBookmark; }结果集对象的一般用法得到结果集后,它的使用步骤一般如下:首先Query出IColumnsInfo接口通过调用IColumnsInfo::GetColumnInfo方法得到关于结果集的列的详细信息DBCOLUMNINFO结构的数组,包括:列序号,列名,类型,字节长度,精度,比例等3.通过该结构数组,准备一个对应的DBBINDING结构数组,并计算每行数据实际需要的缓冲大小,并填充结构DBBINDING。这个过程一般叫做绑定4.利用DBBINDING数组和IAccessor::CreateAccessor方法创建一个数据访问器并得到句柄HACCESSOR调用IRowset::GetNextRow遍历行指针到下一行,第一次调用就是指向第一行,并得到行句柄HROW,这个行句柄表示我们访问的当前是结果中的第几行,一般它的值是一个依次递增的整数调用IRowset::GetData传入准备好的行缓冲内存指针,以及之前创建的访问器HACCESSOR句柄和HROW句柄。最终行数据就被放置到了指定的缓冲中。循环调用GetNextRow和GetData即可遍历整个二维结果集。列信息的获取取得结果集对象后,紧接着的操作一般就是获取结果集的结构信息,也就是获取结果集的列信息(有些材料中称为字段信息)要获取列信息,就需要QueryInterface出结果集对象的IColumnsInfo接口,并调用IColumnsInfo::GetColumnInfo方法获得一个称为DBCOLUMNINFO结构体的数组该结构体中反映了列的逻辑结构信息(抽象数据类型)和物理结构信息(内存需求大小等信息)函数GetColumnInfo定义如下:HRESULT GetColumnInfo ( DBORDINAL *pcColumns, DBCOLUMNINFO **prgInfo, OLECHAR **ppStringsBuffer);第一个参数表示总共有多少列,第二个参数是一个DBCOLUMNINFO,返回一个列信息的数组指针,第三个参数返回一个字符串指针,这个字符串中保存的是个列的名称,每个名称间以\0\0分割。但是我们一般不使用它来获取列名,我们一般使用DBCOLUMNINFO结构的pwszName成员。DBCOLUMNINFO定义如下:typedef struct tagDBCOLUMNINFO { LPOLESTR pwszName; //列名 ITypeInfo *pTypeInfo; //列的类型信息 DBORDINAL iOrdinal; //列序号 DBCOLUMNFLAGS dwFlags; //列的相关标识 DBLENGTH ulColumnSize; //列最大可能的大小,对于字符串来说,它表示的是字符个数 DBTYPE wType; //列类型 BYTE bPrecision; //精度(它表示小数点后面的位数) BYTE bScale; //表示该列的比例,目前没有用处,一般给的是0 DBID columnid; //列信息在数据字典表中存储的ID } DBCOLUMNINFO;对于columnid成员,DBMS系统一般会有多个系统表来表示众多的信息,比如用户信息,数据库信息,数据表信息等等,其中针对每个表中的列的相关信息DBMS系统使用特定的系统表来存储,而查询这个系统表来获取列信息时使用的就是这个columnid值。数据绑定一般绑定需要两步,1是获取列信息的DBCOLUMNINFO结构,接着就是根据列信息来填充DBBINDING数据结构。有的时候可能会觉得绑定好麻烦啊,还不如直接返回一个缓冲,将所有结果放入里面,应用程序根据需求自己去解析它,这样岂不是更方便。之所以需要绑定,有下面一个理由:并不是所有的数据类型都能被应用程序支持,比如说数据库中的NUMBER类型在VC++中找不到对应的数据结构来支持。有时一行数据并不能完全读取到内存中,比如说我们给的缓冲不够或者是数据库中的数据本身比较大,比如存储了一个视频文件等等。在程序中并不是所有的访问器都是为了读取数据,而且使用返回所有结果的方式太简单粗暴了,比如我只想要一列的数据那个数据可能占用内存不足1K,但是数据库表中某一列数据特别大,可能占用内存会超过一个G,如果全都返回的话太浪费内存了。所以在绑定时候可以灵活的指定返回那些数据,返回数据长度是多少,针对特别大的数据,我们可以指定它只返回部分,比如只返回前面的1K使用绑定可以灵活的安排返回数据在内存中的摆放形式。绑定结构的定义如下:typedef struct tagDBBINDING { DBORDINAL iOrdinal; //列号 DBBYTEOFFSET obValue; DBBYTEOFFSET obLength; DBBYTEOFFSET obStatus; ITypeInfo *pTypeInfo; DBOBJECT *pObject; DBBINDEXT *pBindExt; DBPART dwPart; DBMEMOWNER dwMemOwner; DBPARAMIO eParamIO; DBLENGTH cbMaxLen; //数据的最大长度,一般给我们为这个列的数据准备的缓冲的长度 DWORD dwFlags; DBTYPE wType; //将该列转化为何种数据类型展示 BYTE bPrecision; BYTE bScale; }DBBINDING;参数的详细说明:obValue、obLength、obStatus:数据源在返回结果的时候一般会返回该列信息的三中数据,数据长度、数据状态、数据值。数据状态表示数据源在提供数据的一个状态信息,比如该列信息为空时它会返回一个DBSTATUS_S_ISNULL,列数据比较长,而提供的数据可能不够,这个时候会返回一个状态表示发生了截断。而数据长度表示返回结果的长度。这个值是返回的数据对应的字节长度,注意这里需要与前面ulColumnSize区分开来。三个数据在内存中摆放的顺序如下:其中每列数据都是按照status length value结构排布,而不同的列的数据按照顺序依次向后排放,这个内存的布局有点像结构体数组在内存中的的布局方式。而绑定结构中的obValue、obLength、obStatus规定了它们三者在一块内存缓冲中的偏移,要注意后面一列的开始位置是在前面一列的结束位置而不是所有数据都是从0开始。dwPart:前面说数据源返回结果中有3个部分,但是我们可以指定数据源返回这3个部分的哪些部分,它的值是一些标志位,根据这些标志来决定需要返回哪些数据,不需要返回哪些数据.它的值主要有:DBPART_LENGTH、 DBPART_STATUS、DBPART_VALUE;dwMemOwner,这个值表示将使用何种内存缓冲来保存数据源返回的结果,我们一般使用DBMEMOWNER_CLIENTOWNED,表示使用用户自定义内存的方式,即这个缓冲需要自行准备。eParamIO:我们将返回的值做何种用途,DBPARAMIO_NOTPARAM表示不做特殊用途,DBPARAMIO_INPUT,作为输入值,一般在需要更新列数据的时候使用这个标志,DBPARAMIO_OUTPUT,作为输出值,这个输出时相对于数据源来说的,表示输出到应用程序程序缓冲,作为展示用。wType:将数据源中的原始数据做何种类型的转化,比如原来数据库中存储的是整数的123456,而这个值是DBTYPE_WSTR的话,数据源中的结果会被转化为字符串的"123456",放入到缓冲中。DBBINDING 与DBCOLUMNSINFO结构的比较它们二者中有许多数据成员是相同的,表示的含义也基本相同,但是二者也有显著的区别:DBCOLUMNINFO是数据提供者给使用者的信息,它是固定的,对相同的查询来说,列总是相同的,因此数据提供者返回的 DBCOLUMNINFO数组也是固定的.而DBBINDING是作为数据消费者创建之后给数据提供者的一个结构数组,它的内容则由调用者来完全控制,通过这个结构可以指定数据提供者最终将数据摆放成调用者指定的格式,并进行指定的数据类型转换.针对相同的查询我们可以指定不同的DBBINDINGS结构。DBCOLUMNINFO反映的是二维结果集的原始列结构信息而DBBINDING则反映的是二维结果集数据最终按要求摆放在内存中的样式下面是一个针对绑定的列子:ExecSql(pIOpenRowset, pIRowset); //创建IColumnsInfo接口 HRESULT hRes = pIRowset->QueryInterface(IID_IColumnsInfo, (void**)&pIColumnsInfo); COM_SUCCESS(hRes, _T("查询接口IComclumnsInfo,错误码:%08x\n"), hRes); //获取结果集的详细信息 hRes = pIColumnsInfo->GetColumnInfo(&cClumns, &rgColumnInfo, &lpClumnsName); COM_SUCCESS(hRes, _T("获取列信息失败,错误码:%08x\n"), hRes); //绑定 pDBBindings = (DBBINDING*)MALLOC(sizeof(DBBINDING) * cClumns); for (int iRow = 0; iRow < cClumns; iRow++) { pDBBindings[iRow].bPrecision = rgColumnInfo[iRow].bPrecision; pDBBindings[iRow].bScale = rgColumnInfo[iRow].bScale; pDBBindings[iRow].cbMaxLen = rgColumnInfo[iRow].ulColumnSize * sizeof(WCHAR); if (rgColumnInfo[iRow].wType == DBTYPE_I4) { //数据库中行政单位的长度最大为6位 pDBBindings[iRow].cbMaxLen = 7 * sizeof(WCHAR); } pDBBindings[iRow].dwMemOwner = DBMEMOWNER_CLIENTOWNED; pDBBindings[iRow].dwPart = DBPART_LENGTH | DBPART_STATUS | DBPART_VALUE; pDBBindings[iRow].eParamIO = DBPARAMIO_NOTPARAM; pDBBindings[iRow].iOrdinal = rgColumnInfo[iRow].iOrdinal; pDBBindings[iRow].obStatus = dwOffset; pDBBindings[iRow].obLength = dwOffset + sizeof(DBSTATUS); pDBBindings[iRow].obValue = dwOffset + sizeof(DBSTATUS) + sizeof(ULONG); pDBBindings[iRow].wType = DBTYPE_WSTR; dwOffset = dwOffset + sizeof(DBSTATUS) + sizeof(ULONG) + pDBBindings[iRow].cbMaxLen * sizeof(WCHAR); dwOffset = COM_ROUNDUP(dwOffset); //进行内存对齐 } //创建访问器 hRes = pIRowset->QueryInterface(IID_IAccessor, (void**)&pIAccessor); COM_SUCCESS(hRes, _T("查询IAccessor接口失败错误码:%08x\n"), hRes); hRes = pIAccessor->CreateAccessor(DBACCESSOR_ROWDATA, cClumns, pDBBindings, 0, &hAccessor, NULL); COM_SUCCESS(hRes, _T("创建访问器失败错误码:%08x\n"), hRes); //输出列名信息 DisplayColumnName(rgColumnInfo, cClumns); //分配对应的内存 pData = MALLOC(dwOffset * cRows); while (TRUE) { hRes = pIRowset->GetNextRows(DB_NULL_HCHAPTER, 0, cRows, &uRowsObtained, &phRows); if (hRes != S_OK && uRowsObtained != 0) { break; } ZeroMemory(pData, dwOffset * cRows); //显示数据 for (int i = 0; i < uRowsObtained; i++) { pCurrData = (BYTE*)pData + dwOffset * i; pIRowset->GetData(phRows[i], hAccessor, pCurrData); DisplayData(pDBBindings, cClumns, pCurrData); } //清理hRows pIRowset->ReleaseRows(uRowsObtained, phRows, NULL, NULL, NULL); CoTaskMemFree(phRows); phRows = NULL; } //显示列名称 void DisplayColumnName(DBCOLUMNINFO *pdbColumnInfo, DBCOUNTITEM iDbCount) { COM_DECLARE_BUFFER(); for (int iColumn = 0; iColumn < iDbCount; iColumn++) { COM_CLEAR_BUFFER(); TCHAR wszColumnName[MAX_DISPLAY_SIZE + 1] =_T(""); size_t dwSize = 0; StringCchLength(pdbColumnInfo[iColumn].pwszName, MAX_DISPLAY_SIZE, &dwSize); dwSize = min(dwSize, MAX_DISPLAY_SIZE); StringCchCopy(wszColumnName, MAX_DISPLAY_SIZE, pdbColumnInfo[iColumn].pwszName); COM_PRINTF(wszColumnName); COM_PRINTF(_T("\t")) } COM_PRINTF(_T("\n")); } //显示数据 void DisplayData(DBBINDING *pdbBindings, DBCOUNTITEM iDbColCnt, void *pData) { COM_DECLARE_BUFFER(); for (int i = 0; i < iDbColCnt; i++) { COM_CLEAR_BUFFER(); DBSTATUS status = *(DBSTATUS*)((PBYTE)pData + pdbBindings[i].obStatus); ULONG uSize = (*(ULONG*)((PBYTE)pData + pdbBindings[i].obLength)) / sizeof(WCHAR); PWSTR pCurr = (PWSTR)((PBYTE)pData + pdbBindings[i].obValue); switch (status) { case DBSTATUS_S_OK: case DBSTATUS_S_TRUNCATED: COM_PRINTF(_T("%s\t"), pCurr); break; case DBSTATUS_S_ISNULL: COM_PRINTF(_T("%s\t"), _T("(null)")); break; default: break; } } COM_PRINTF(_T("\n")); }在使用前一个列子中的方法设置对应的属性并执行SQL语句后,得到一个结果集,然后调用对应的Query方法,得到一个pIColumnsInfo接口,接着调用接口的GetColumnsInfo方法,获取结构的具体信息。最需要注意的是绑定部分的代码,根据返回的具体列数,我们定义了一个对应的绑定结构的数组,将每个赋值,赋值的时候定义了一个dwOffset结构来记录当前使用内存的情况,这样每次在循环执行一次后,它的位置永远在上一个列信息缓冲的尾部,这样我们可以很方便的进行偏移的计算。绑定完成后这个dwOffset的值就是所有列使用的内存的总大小,因此在后面利用这个值分配一个对应长度的内存。然后循环调用GetNextRows、GetData方法依次获取每行、每列的数据。最后调用相应的函数来进行显示,至此就完成了数据的读取操作。最后,我发现码云上的代码片段简直就是为保存平时例子代码而生的,所以后面的代码将不再在GitHub上更新了,而换到码云上面。源代码查看
2018年01月28日
31 阅读
5 评论
0 点赞
2018-01-21
事务对象和命令对象
上次说到数据源对象,这次接着说事务对象和命令对象。事务是一种对数据源的一系列更新进行分组或批处理以便当所有更新都成功时同时提交这些更新,或者如果任何一个更新失败则不提交任何更新并且回滚整个事务的方法.命令对象一般是用来执行sql语句并生成结果集的对象会话对象在OLEDB中通过以下3中方式支持事务:ITransactionLocal::StartTransactionITransaction::commitITransaction::AbortOLEDB中定义事务和回话对象的接口如下:CoType TSession { [mandatory] interface IGetDataSource; [mandatory] interface IOpenRowset; [mandatory] interface ISessionProperties; [optional] interface IAlterIndex; [optional] interface IAlterTable; [optional] interface IBindResource; [optional] interface ICreateRow; [optional] interface IDBCreateCommand; [optional] interface IDBSchemaRowset; [optional] interface IIndexDefinition; [optional] interface ISupportErrorInfo; [optional] interface ITableCreation; [optional] interface ITableDefinition; [optional] interface ITableDefinitionWithConstraints; [optional] interface ITransaction; [optional] interface ITransactionJoin; [optional] interface ITransactionLocal; [optional] interface ITransactionObject; }在创建了数据库连接之后使用QueryInterface 查询出IDBCreateSeesion对象,然后调用IDBCreateSession的CreateSession方法创建一个回话对象。需要注意的是,一个数据源连接可以创建多个回话对象,这里只能通过这种方式创建回话对象,而不能直接通过CoCreateInstance 来创建。一般来说应用中至少创建一个会话对象Command对象Command对象的定义如下:CoType TCommand { [mandatory] interface IAccessor; [mandatory] interface IColumnsInfo; [mandatory] interface ICommand; [mandatory] interface ICommandProperties; [mandatory] interface ICommandText; [mandatory] interface IConvertType; [optional] interface IColumnsRowset; [optional] interface ICommandPersist; [optional] interface ICommandPrepare; [optional] interface ICommandWithParameters; [optional] interface ISupportErrorInfo; [optional] interface ICommandStream; }一般创建Command对象是通过Session对象Query出来一个IDBCreateCommand接口让后调用CreateCommand方法创建。与会话对象相似,一个会话对象可以创建多个命令对象,但是从上面会话对象的定义可以看出IDBCreateCommand接口是一个可选接口,并不是所有的数据库都支持,因此在创建命令对象的时候一定要注意判断是否支持。对于不支持的可以采用其他办法(一般采用直接打开数据库表的方式)。下面是一个演示例子:#define CHECK_OLEDB_INTERFACE(I, iid)\ hRes = (I)->QueryInterface(IID_##iid, (void**)&(p##iid));\ if(FAILED(hRes))\ {\ OLEDB_PRINTF(_T("不支持接口:%s\n"), _T(#iid));\ }\ else\ {\ OLEDB_PRINTF(_T("支持接口:%s\n"), _T(#iid));\ } BOOL CreateDBSession(IOpenRowset* &pIOpenRowset) { DECLARE_BUFFER(); DECLARE_OLEDB_INTERFACE(IDBPromptInitialize); DECLARE_OLEDB_INTERFACE(IDBInitialize); DECLARE_OLEDB_INTERFACE(IDBCreateSession); BOOL bRet = FALSE; HWND hDesktop = GetDesktopWindow(); HRESULT hRes = CoCreateInstance(CLSID_DataLinks, NULL, CLSCTX_INPROC_SERVER, IID_IDBPromptInitialize, (void**)&pIDBPromptInitialize); OLEDB_SUCCESS(hRes, _T("创建接口IDBPromptInitialize失败,错误码:%08x\n"), hRes); hRes = pIDBPromptInitialize->PromptDataSource(NULL, hDesktop, DBPROMPTOPTIONS_WIZARDSHEET, 0, NULL, NULL, IID_IDBInitialize, (IUnknown**)&pIDBInitialize); OLEDB_SUCCESS(hRes, _T("弹出接口数据源配置对话框失败,错误码:%08x\n"), hRes); hRes = pIDBInitialize->Initialize(); OLEDB_SUCCESS(hRes, _T("链接数据库失败,错误码:%08x\n"), hRes); hRes = pIDBInitialize->QueryInterface(IID_IDBCreateSession, (void**)&pIDBCreateSession); OLEDB_SUCCESS(hRes, _T("创建接口IDBCreateSession失败,错误码:%08x\n"), hRes); hRes = pIDBCreateSession->CreateSession(NULL, IID_IOpenRowset, (IUnknown**)&pIOpenRowset); OLEDB_SUCCESS(hRes, _T("创建接口IOpenRowset失败,错误码:%08x\n"), hRes); bRet = TRUE; __CLEAR_UP: OLEDB_SAFE_RELEASE(pIDBPromptInitialize); OLEDB_SAFE_RELEASE(pIDBInitialize); OLEDB_SAFE_RELEASE(pIDBCreateSession); return bRet; } int _tmain(int argc, TCHAR *argv[]) { DECLARE_BUFFER(); DECLARE_OLEDB_INTERFACE(IOpenRowset); DECLARE_OLEDB_INTERFACE(IDBInitialize); DECLARE_OLEDB_INTERFACE(IDBCreateCommand); DECLARE_OLEDB_INTERFACE(IAccessor); DECLARE_OLEDB_INTERFACE(IColumnsInfo); DECLARE_OLEDB_INTERFACE(ICommand); DECLARE_OLEDB_INTERFACE(ICommandProperties); DECLARE_OLEDB_INTERFACE(ICommandText); DECLARE_OLEDB_INTERFACE(IConvertType); DECLARE_OLEDB_INTERFACE(IColumnsRowset); DECLARE_OLEDB_INTERFACE(ICommandPersist); DECLARE_OLEDB_INTERFACE(ICommandPrepare); DECLARE_OLEDB_INTERFACE(ICommandWithParameters); DECLARE_OLEDB_INTERFACE(ISupportErrorInfo); DECLARE_OLEDB_INTERFACE(ICommandStream); CoInitialize(NULL); if (!CreateDBSession(pIOpenRowset)) { OLEDB_PRINTF(_T("调用函数CreateDBSession失败,程序即将推出\n")); return 0; } HRESULT hRes = pIOpenRowset->QueryInterface(IID_IDBCreateCommand, (void**)&pIDBCreateCommand); OLEDB_SUCCESS(hRes, _T("创建接口IDBCreateCommand失败,错误码:%08x\n"), hRes); hRes = pIDBCreateCommand->CreateCommand(NULL, IID_IAccessor, (IUnknown**)&pIAccessor); OLEDB_SUCCESS(hRes, _T("创建接口IAccessor失败,错误码:%08x\n"), hRes); CHECK_OLEDB_INTERFACE(pIAccessor, IColumnsInfo); CHECK_OLEDB_INTERFACE(pIAccessor, ICommand); CHECK_OLEDB_INTERFACE(pIAccessor, ICommandProperties); CHECK_OLEDB_INTERFACE(pIAccessor, ICommandText); CHECK_OLEDB_INTERFACE(pIAccessor, IConvertType); CHECK_OLEDB_INTERFACE(pIAccessor, IColumnsRowset); CHECK_OLEDB_INTERFACE(pIAccessor, ICommandPersist); CHECK_OLEDB_INTERFACE(pIAccessor, ICommandPrepare); CHECK_OLEDB_INTERFACE(pIAccessor, ICommandWithParameters); CHECK_OLEDB_INTERFACE(pIAccessor, ISupportErrorInfo); CHECK_OLEDB_INTERFACE(pIAccessor, ICommandStream); __CLEAR_UP: OLEDB_SAFE_RELEASE(pIOpenRowset); OLEDB_SAFE_RELEASE(pIDBInitialize); OLEDB_SAFE_RELEASE(pIDBCreateCommand); OLEDB_SAFE_RELEASE(pIAccessor); OLEDB_SAFE_RELEASE(pIColumnsInfo); OLEDB_SAFE_RELEASE(pICommand); OLEDB_SAFE_RELEASE(pICommandProperties); OLEDB_SAFE_RELEASE(pICommandText); OLEDB_SAFE_RELEASE(pIConvertType); OLEDB_SAFE_RELEASE(pIColumnsRowset); OLEDB_SAFE_RELEASE(pICommandPersist); OLEDB_SAFE_RELEASE(pICommandPrepare); OLEDB_SAFE_RELEASE(pICommandWithParameters); OLEDB_SAFE_RELEASE(pISupportErrorInfo); OLEDB_SAFE_RELEASE(pICommandStream); CoUninitialize(); return 0; }在上述的例子中,首先定义了一个宏,用来判断是否支持对应的接口。同时定义了一个CreateDBSession方法来创建一个会话对象。在该函数中首先利用上一节的方法创建一个数据库连接,然后在数据源对象上调用QueryInterface来获取接口IDBCreateSeesion,接着利用IDBCreateSeesion接口的CreateSeesion方法创建一个会话对象,由于IDBCreateCommand接口并不是所有的数据源都支持,所以为了保证一定能创建会话对象,我们选择会话对象必须支持的接口IOpenRowset。在得到会话对象后,尝试创建IDBCreateSession对象,如果它不支持,那么程序直接退出。接着调用IDBCreateCommand接口来创建一个命令对象并尝试query命令对象的其他接口,得出数据源支持哪些接口。这个例子非常简单,只是为了演示如何创建会话对象和数据源对象罢了。本节例子代码
2018年01月21日
3 阅读
0 评论
0 点赞
2018-01-12
OLEDB数据源
数据源在oledb中指数据提供者,这里可以简单的理解为数据库程序。数据源对象代表数据库的一个连接,是需要创建的第一个对象。而数据源对象主要用于配置数据库连接的相关属性如连接数据库的用户名密码等等
2018年01月12日
6 阅读
0 评论
0 点赞
2018-01-06
Windows数据库编程接口简介
数据库是计算机中一种专门管理数据资源的系统,目前几乎所有软件都需要与数据库打交道(包括操作系统,比如Windows上的注册表其实也是一种数据库),有些软件更是以数据库为核心因此掌握数据库系统的使用方法以及数据库系统编程接口的使用方法是程序员非常重要的基本技能之一。所以我花了一定的时间学习了在Windows平台上使用COM接口的方式操作数据库。这段时间我会将自己学习过程中掌握的知识和其中的一些坑都发布出来,供个人参考,也方便他人学习
2018年01月06日
3 阅读
0 评论
0 点赞
2017-11-02
COM学习(四)——COM中的数据类型
上一次说到,COM为了跨语言,有一套完整的规则,只要COM组件按照规则编写,而不同的语言也按照对应的规则调用,那么就可以实现不同语言间相互调用。但是根据那套规则,只能识别接口,并调用没有参数和返回类型的接口,毕竟不同语言里面的基本数据类型不同,可能在VC++中char * 就表示字符串,而在Java或者c#中string是一个对象,二者的内存结构不同,不能简单的进行内存数据类型的强制转化。为了实现数据的正常交互,COM中又定义了一组公共的数据类型。HRESULT类型:在COM中接口的返回值强制定义为该类型,用于表示当前执行的状态是完成或者是出错,这个类型一般在VC中使用,别的语言在调用时根据接口的这个值来确定接下来该如何进行。HRESULT类型的定义如下:typedef _Return_type_success_(return >= 0) long HRESULT;其实它就是一个32位的整数,微软将这个整数分成几个部分,各个部分都有详细的含义,这个值的详细解释在对应的winerror.h中。// // Note: There is a slightly modified layout for HRESULT values below, // after the heading "COM Error Codes". // // Search for "**** Available SYSTEM error codes ****" to find where to // insert new error codes // // Values are 32 bit values laid out as follows: // // 3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 // 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 // +-+-+-+-+-+---------------------+-------------------------------+ // |S|R|C|N|r| Facility | Code | // +-+-+-+-+-+---------------------+-------------------------------+ // // where // // S - Severity - indicates success/fail // // 0 - Success // 1 - Fail (COERROR) // // R - reserved portion of the facility code, corresponds to NT's // second severity bit. // // C - reserved portion of the facility code, corresponds to NT's // C field. // // N - reserved portion of the facility code. Used to indicate a // mapped NT status value. // // r - reserved portion of the facility code. Reserved for internal // use. Used to indicate HRESULT values that are not status // values, but are instead message ids for display strings. // // Facility - is the facility code // // Code - is the facility's status code // // // Define the facility codes //根据上面的注释可以看到,以及我自己查阅相关资料,它里面总共有7个部分,各个部分代表的含义如下:S - 严重性 - 表示成功或失败0 - 成功,1 - 失败R - 设施代码的保留部分,对应于NT的第二严重性位。1 - 严重故障C - 第三方。 此位指定值是第三方定义还是Microsoft定义的。0 - Microsoft-定义,1 - 第三方定义N - 保留部分设施代码。 用于指示映射的NT状态值。X - 保留部分设施代码。 保留供内部使用。 用于指示不是状态值的HRESULT值,而是用于显示字符串的消息标识。Facility - 表示引发错误的系统服务. 示例Facility代码如下所示:2 - 调度(COM调度)3 - 存储 (OLE存储)4 - ITF (COM/OLE 接口管理)7 - (原始 Win32 错误代码)8 - Windows9 - SSPI10 - 控制11 - CERT (客户端或服务器认证)...Code - 设施的状态代码其实这些没有必要知道的很详细,只需要知道里面常用的几个即可:S_OK:成功S_FALSE:失败E_NOINTERFACE:没有接口,一般是由QueryInterface或者CoCreateInterface函数返回,当我们传入的ID不对它找不到对应的接口时返回该值E_OUTOFMEMORY:当内存不足时返回该值。一般在COM的调用者看来,有的时候只要最高位不为0就表示成功,这个时候可能会继续使用,所以在我们自己编写组件的时候要根据具体情况选择返回值,不要错误了就返回S_FALSE,其实我们看它的定义可以知道它是等于1的,最高位为0,仍然是成功的。如果返回S_FALSE可能会造成意想不到的错误,而且还难以调试。BSTRCOM中规定了一种通用的字符串类型BSTR,查看BSTR的定义如下:typedef /* [wire_marshal] */ OLECHAR *BSTR; typedef WCHAR OLECHAR;从上面的定义上不难看出BSTR其实就是一个WCHAR ,也就是一个指向宽字符的指针。COM中使用的是UNICODE字符串,在编写COM程序的时候经常涉及到CString、WCHAR、char等的相互转化,其实本质上就是多字节字符与宽字节字符之间的转化。我们平时在进行char 与WCHAR*之间转化的函数像WideCharToMultiByte和MultiByteToWideChar,以及W2A和A2W等。COM为了方便使用,另外也提供了一组转化函数_com_util::ConvertBSTRToString以及_com_util::ConvertStringToBSTR用在在char*与BSTR之间进行转化。需要注意的是,这组函数返回的字符串是在堆上分配出来的,使用完后需要自己释放。在BSTR类型中,定义了两个函数SysAllocString(),和SysFreeString()用来分配和释放一个BSTR的内存空间。在这总结一下他们之间的相互转化:char*----->BSTR: _com_util::ConvertStringToBSTRWCHAR*---->BSTR:可以直接用 = 进行赋值,也可以使用SysAllocStringBSTR---->WCHAR:一般是直接使用等号即可,但是在WCHAR使用完之前不能释放,所以一般都是赋值给一个CStringBSTR---->char*:_com_util::ConvertBSTRToStringConvert函数是定义在头文件atlutil.h中并且需要引用comsupp.lib文件另外COM封装了一个_bstr_t的类,使用这个类就更加方便了,它封装了与char*之间的相互转化,可以直接使用赋值符号进行相互转化,同时也不用考虑回收内存的问题,它自己会进行内存回收。VARIANT 万能类型现代编程语言一般有强类型的语言和弱类型的语言,强类型的像C/C++、Java这样的,必须在使用前定义变量类型,而弱类型像Python这样的可以直接定义变量而不用管它的类型,甚至可以写出像:i = 0 i = "hello world"这样的代码,而且不同语言中可能同一种类型的变量在内存布局上也可能不一样。解决不同语言之间变量类型的冲突,COM定义了一种万能类型——VARIANT。typedef struct tagVARIANT VARIANT; typedef struct tagVARIANT VARIANTARG; struct tagVARIANT { union { struct __tagVARIANT { VARTYPE vt; WORD wReserved1; WORD wReserved2; WORD wReserved3; union { LONGLONG llVal; LONG lVal; BYTE bVal; SHORT iVal; FLOAT fltVal; DOUBLE dblVal; VARIANT_BOOL boolVal; _VARIANT_BOOL bool; SCODE scode; CY cyVal; DATE date; BSTR bstrVal; IUnknown *punkVal; IDispatch *pdispVal; SAFEARRAY *parray; BYTE *pbVal; SHORT *piVal; LONG *plVal; LONGLONG *pllVal; FLOAT *pfltVal; DOUBLE *pdblVal; VARIANT_BOOL *pboolVal; _VARIANT_BOOL *pbool; SCODE *pscode; CY *pcyVal; DATE *pdate; BSTR *pbstrVal; IUnknown **ppunkVal; IDispatch **ppdispVal; SAFEARRAY **pparray; VARIANT *pvarVal; PVOID byref; CHAR cVal; USHORT uiVal; ULONG ulVal; ULONGLONG ullVal; INT intVal; UINT uintVal; DECIMAL *pdecVal; CHAR *pcVal; USHORT *puiVal; ULONG *pulVal; ULONGLONG *pullVal; INT *pintVal; UINT *puintVal; struct __tagBRECORD { PVOID pvRecord; IRecordInfo *pRecInfo; } __VARIANT_NAME_4; } __VARIANT_NAME_3; } __VARIANT_NAME_2; DECIMAL decVal; } __VARIANT_NAME_1; };从定义上看出,它其实是一个巨大的联合体,将所有C/C++的基本类型都包含进来,甚至包含了像BSTR, 这样的COM中使用的类型。它通过成员vt来表示它当前使用的是哪种类型的变量。vt的类型是一个枚举类型,详细的定义请参见MSDN。为了简化操作,COM中也对它进行了一个封装——_variant_t,该类型可以直接使用任何类型的数据对其进行初始化操作。但是在使用里面的值时还是得判断它的vt成员的值COM中的其他操作最后附上一张COM常用函数表以供参考:
2017年11月02日
6 阅读
0 评论
0 点赞
2017-10-30
COM学习(三)——COM的跨语言
COM是基于二进制的组件模块,从设计之初就以支持所有语言作为它的一个目标,这篇文章主要探讨COM的跨语言部分。idl文件一般COM接口的实现肯定是以某一具体语言来实现的,比如说使用VC++语言,这就造成了一个问题,不同的语言对于接口的定义,各个变量的定义各不相同,如何让使用vc++或者说Java等其他语言定义的接口能被别的语言识别?为了达到这个要求,定义了一种文件格式idl——(Interface Definition Language)接口定义语言,IDL提供一套通用的数据类型,并以这些数据类型来定义更为复杂的数 据类型。一般来说,一个文件有下面几个部分说明接口的定义组件库的定义实现类的定义而各个部分又包括他们的属性定义,以及函数成员的定义属性:属性是在接口定义的上方,使用“[]”符号包裹,一般在属性中使用下面几个关键字:object:标明该部分是一个对象(可以理解为c++中的对象,包括接口和具体的实现类)uuid:标明该部分的GUIDversion:该部分的版本接口定义接口定义采用关键字interface,接口函数定义在一对大括号中,它的定义与类的定义相似,其中函数定义需要修饰函数各个参数的作用,比如使用in 表示它作为输入参数,out表示作为输出参数,retval表示该参数作为返回值,一般在VC++定义的接口中,函数返回值为HRESULT,但是需要返回一个值供外界调用,此时就使用输出参数并加上retval表示它将在其他语言中作为函数的返回值。组件库定义库使用library关键字定义,在定义库的时候,它的属性一般定义GUID和版本信息,而在库中通常定义库中的实现类的相关信息,库中的信息也是写在一对大括号中实现类的定义接口实现类使用关键字coclass,接口类的属性一般定义一个object,一个GUID,然后一般定义实现类不需要向在C++中那样定义它的各个接口,各个数据成员,只需要告知它实现哪些接口即可,也就是说它继承自哪些接口。下面是一个具体的例子:import "unknwn.idl"; [ object, uuid(CF809C44-8306-4200-86A1-0BFD5056999E) ] interface IMyString : IUnknown { HRESULT Init([in] BSTR bstrInit); HRESULT GetLength([out, retval] ULONG *pretLength); HRESULT Find([in] BSTR bstrFind, [out, retval] BSTR* bstrSub); }; [ uuid(ADF50A71-A8DD-4A64-8CCA-FFAEE2EC7ED2), version(1.0) ] library ComDemoLib { importlib("stdole32.tlb"); [ uuid(EBD699BA-A73C-4851-B721-B384411C99F4) ] coclass CMyString { interface IMyString; }; };上面的例子中定义了一个IMyString接口继承自IUnknown接口,函数参数列表中in表示参数为输入参数,out表示它为输出参数,retval表示该参数是函数的返回值。import导入了一个库文件类似于include。而importlib导入一个tlb文件,我们可以将其看成VC++中的#pragma comment导入一个lib库从上面不难看出一个IDL文件至少有3个ID,一个是接口ID,一个是库ID,还有一个就是实现类的ID在VC环境中通过midl命令可以对该文件进行编译,编译会生成下面几个我们在编写实现时会用到的重要文件:一个.h文件:包含各个部分的声明,以及接口的定义一个_i.c文件:包含各个部分的定义,主要是各个GUI的定义需要实现的导出函数一般我们需要在dll文件中导出下面几个全局的导出函数:STDAPI DllRegisterServer(void); STDAPI DllUnregisterServer(void); STDAPI DllGetClassObject(const CLSID & rclsid, const IID & riid, void ** ppv); STDAPI DllCanUnloadNow(void);其中DllRegisterServer用来向注册表中注册模块的相关信息,主要注测在HKEY_CLASSES_ROOT中,主要定义下面几项内容:字符串名称项,该项中包含一个默认值,一般给组件的字符串名称;CLSID子健,一般给实现类的GUID;CurVer子健一般是子健的版本以版本字符串为键的注册表项,该项中主要保存:默认值,当前版本的项目名称;CLSID当前版本库的实现类的GUID在HKEY_CLASSES_ROOT/CLSID子健中注册以实现类GUID字符串为键的注册表项,里面主要包含:默认值,组件字符串名称;InprocServer32,组件所在模块的全路径;ProgID组件名称;TypeLib组件类型库的ID,也就是在定义IDL文件时,定义的实现库的GUID。下面是具体的定义:const TCHAR *g_RegTable[][3] = { { _T("CLSID\\{EBD699BA-A73C-4851-B721-B384411C99F4}"), 0, _T("FirstComLib.MyString")}, //组件ID { _T("CLSID\\{EBD699BA-A73C-4851-B721-B384411C99F4}\\InprocServer32"), 0, (const TCHAR*)-1 }, //组建路径 { _T("CLSID\\{EBD699BA-A73C-4851-B721-B384411C99F4}\\ProgID"), 0, _T("FirstComLib.MyString")}, //组件名称 { _T("CLSID\\{EBD699BA-A73C-4851-B721-B384411C99F4}\\TypeLib"), 0, _T("{ADF50A71-A8DD-4A64-8CCA-FFAEE2EC7ED2}") }, //类型库ID { _T("FirstComLib.MyString"), 0, _T("FirstComLib.MyString") }, //组件的字符串名称 { _T("FirstComLib.MyString\\CLSID"), 0, _T("{EBD699BA-A73C-4851-B721-B384411C99F4}")}, //组件的CLSID { _T("FirstComLib.MyString\\CurVer"), 0, _T("FirstComLib.MyString.1.0") }, //组件版本 { _T("FirstComLib.MyString.1.0"), 0, _T("FirstComLib.MyString") }, //当前版本的项目名称 { _T("FirstComLib.MyString.1.0\\CLSID"), 0, _T("{EBD699BA-A73C-4851-B721-B384411C99F4}")} //当前版本的CLSID };使用上一篇博文的代码,来循环注册这些项即可DllGetClassObject:该函数用来生成对应的工厂类,而工厂类负责产生对应接口的实现类。DllCanUnloadNow:函数用来询问是否可以卸载对应的dll,一般在COM中有两个全局的引用计数,用来记录当前内存中有多少个模块中的类,以及当前有多少个线程在使用它,如果当前没有线程使用或者存在的对象数为0,则可以卸载实现类的定义实现部分的整体结构图如下:由于所有类都派生自IUnknown,所在在这里就不显示这个基类了。每个实现类都对应了一个它具体的类工厂,而项目中CMyString类的类厂的定义如下:class CMyClassFactory : public IClassFactory { public: CMyClassFactory(); ~CMyClassFactory(); STDMETHOD(CreateInstance)(IUnknown *pUnkOuter, REFIID riid, void **ppvObject); STDMETHOD(LockServer)(BOOL isLock); STDMETHOD(QueryInterface)(REFIID riid, void **ppvObject); STDMETHOD_(ULONG, AddRef)(void); STDMETHOD_(ULONG, Release)(void); protected: ULONG m_refs; };STDMETHOD宏展开如下:#define STDMETHOD(method) virtual HRESULT __stdcall method所以上面的代码展开后就变成了:virtual HRESULT __stdcall CreateInstance((IUnknown *pUnkOuter, REFIID riid, void **ppvObject);另外3个派生自IUnknown接口就没什么好说的,主要说说另外两个:CreateInstance:主要用来生成对应的实现类,然后再调用实现类——CMyString的QueryInterface函数生成对应的接口LockServer:当前是否被锁住:如果传入的值为TRUE,则表示被锁住,对应的锁计数器+1, 否则 -1至于CMyString类的代码与之前的大同小异,也就没什么说的。其他语言想要调用,以该项目为例,一般会经历下面几个步骤:调用对应语言提供的产生接口的函数,该函数参数一般是传入一个组件的字符串名称。如果要引用该项目中的组件则会传入FirstComLib.MyString在注册表的HKEY_CLASSES_ROOT\组件字符串名\CLSID(比如HKEY_CLASSES_ROOT\FirstComLib.MyString\CLSID)中找到对应的CLSID值在HKEY_CLASSES_ROOT\CLSID\对应ID\InprocServer32(CLSID\{EBD699BA-A73C-4851-B721-B384411C99F4}\InprocServer32)位置处找到对应模块的路径加载该模块根据IDL文件告知其他语言里面存在的接口,由语言调用对应的创建接口的函数创建接口调用模块的导出函数DllGetClassObject将查询到的CLSID作为第一个参数,并将接口ID作为第二个参数传入,得到一个接口6.后面根据idl文件中的定义,直接调用接口中提供的函数真实ATLCOM项目的解析最后来看看一个正式的ATLCOM项目里面的内容,来复习前面的内容,首先通过VC创建一个ATLCOM的dll项目在项目上右键-->New Atl Object,输入接口名称,IDE会根据名称生成一个对应的接口,还是以MyString接口为例,完成这一步后,整个项目的类结构如下:这些全局函数的作用与之前的相同,它里面多了一个_Module的全局对象,该对象类似于MFC中的CWinApp类,它用来表示整个项目的实例,里面封装了对于引用计数的管理,以及对项目中各个接口注册信息的管理,所以看DllRegisterServer等函数就会发现它们里面其实很简单,大部分的工作都由_Module对象完成。整个IDL文件的定义如下:import "oaidl.idl"; import "ocidl.idl"; [ object, uuid(E3BD0C14-4D0C-48F2-8702-9F8DBC96E154), dual, helpstring("IMyString Interface"), pointer_default(unique) ] interface IMyString : IDispatch { }; [ uuid(A61AC54A-1B3D-4D8E-A679-00A89E2CBE93), version(1.0), helpstring("FirstAtlCom 1.0 Type Library") ] library FIRSTATLCOMLib { importlib("stdole32.tlb"); importlib("stdole2.tlb"); [ uuid(11CBC0BE-B2B7-4B5C-A186-3C30C08A7736), helpstring("MyString Class") ] coclass MyString { [default] interface IMyString; }; };里面的内容与上一次的内容相差无几,多了一个helpstring属性,该属性用于产生帮助信息,当使用者在调用接口函数时IDE会将此提示信息显示给调用者。由于有系统框架给我们做的大量的工作,我们再也不用关心像引用计数的问题,只需要将精力集中在编写接口的实现上,减少了不必要的工作量。至此从结构上说明了为了实现跨语言COM组件内部做了哪些工作,当然只有这些工作是肯定不够的,后面会继续说明它所做的另一块工作——提供的一堆通用的变量类型。
2017年10月30日
5 阅读
0 评论
0 点赞
2017-10-17
COM学习(二)——COM的注册和卸载
COM组件是跨语言的,组件被注册到注册表中,在加载时由加载函数在注册表中查找到对应模块的路径并进行相关加载。它的存储规则如下:在注册表的HKEY_CLASSES_ROOT中以模块名的方式保存着COM模块的GUID,比如HKEY_CLASSES_ROOT\ADODB.Error\CLSID键中保存着模块ADODB.Error的GUID为{00000541-0000-0010-8000-00AA006D2EA4}在HKEY_CLASSES_ROOT\CLSID中以GUID为项名保存着对应组件的详细信息,比如之前的{00000541-0000-0010-8000-00AA006D2EA4}这个GUID在注册表中的位置为HKEY_CLASSES_ROOT\CLSID\{00000541-0000-0010-8000-00AA006D2EA4}\InprocServer32\项的默认键中保存着模块所在路径为%CommonProgramFiles%\System\ado\msado15.dll一般的COM模块都是使用regsvr32程序注册到注册表中,该程序在注册时会在模块中查找DllRegisterServer函数,卸载时调用模块中提供的DllUnregisterServer,所以要实现注册的功能主要需要实现这两个函数这两个函数的原型如下:STDAPI DllRegisterServer(); STDAPI DllUnregisterServer();通过VS的F12功能查找STDAPI 的定义如下:#define STDAPI EXTERN_C HRESULT STDAPICALLTYPE在查看STDAPICALLTYPE得到如下结果:#define STDAPICALLTYPE __stdcall所以这个宏展开也就是extern "C" HRESULT __stdcall DllRegisterServer();为了实现注册功能,首先定义一个全局的变量,用来表示需要写入到注册表中的项const TCHAR *g_regTable[][3] = { {_T("SOFTWARE\\ComDemo"), 0, _T("ComDemo")}, {_T("SOFTWARE\\ComDemo\\InporcServer32"), 0, (const TCHAR*)-1}这三项分别为注册表项,注册表项中的键名和键值,当键名为0时会创建一个默认的注册表键,最后一个-1我们会在程序中判断,如果键值为-1,那么值取为模块的路径下面是注册的函数STDAPI DllRegisterServer() { HKEY hKey = NULL; TCHAR szFileName[MAX_PATH] = _T(""); GetModuleFileName(g_hDllIns, szFileName, MAX_PATH); int nCount = sizeof(g_regTable) / sizeof(*g_regTable); for (int i = 0; i < nCount; i++) { LPCTSTR pszKeyName = g_regTable[i][0]; LPCTSTR pszValueName = g_regTable[i][1]; LPCTSTR pszValue = g_regTable[i][2]; if (pszValue == (const TCHAR*)-1) { pszValue = szFileName; } long err = RegCreateKey(HKEY_LOCAL_MACHINE, pszKeyName, &hKey); if (err != ERROR_SUCCESS) { return SELFREG_E_LAST; } err = RegSetValueEx(hKey, pszValueName, 0, REG_SZ, (const BYTE*)pszValue, _tcslen(pszValue) * sizeof(TCHAR)); if (err != ERROR) { return SELFREG_E_LAST; } RegCloseKey(hKey); } return S_OK; }在程序中会循环读取上述全局变量中的值,将值保存到注册表中,在上面的代码中有一句sizeof(g_regTable) / sizeof(*g_regTable);这个是算需要循环多少次,第一个sizeof得到的是这个二维数组的总大小。在C语言中我们说二维数组可以看做是由一维数组组成的,这个二维数组可以看成是由两个一维数组——一个由3个const TCHAR 成员组成的一维数组组成。所以g_regTab自然就是这个一维数组的首地址,第二个sizeof就是这个一维数组的大小,两个相除得到的就是一维数组的个数。卸载函数如下:STDAPI DllUnregisterServer() { int nCount = sizeof(g_regTable) / sizeof(*g_regTable); for (int i = nCount - 1; i >= 0; i--) { LPCTSTR pszKeyName = g_regTable[i][0]; long err = RegDeleteKey(HKEY_LOCAL_MACHINE, pszKeyName); if (err != ERROR_SUCCESS) { return SELFREG_E_LAST; } } return S_OK; }至此已经实现注册和卸载函数。后面就可以直接使用regsvr32这个程序进行注册和卸载了.
2017年10月17日
8 阅读
0 评论
0 点赞
2017-10-12
COM学习(一)——COM基础思想
概述学习微软技术COM是绕不开的一道坎,最近做项目的时候发现有许多功能需要用到COM中的内容,虽然只是简单的使用COM中封装好的内容,但是许多代码仍然只知其然,不知其所以然,所以我决定从头开始好好学习一下COM基础的内容,因此在这记录下自己学习的内容,以便日后参考,也给其他朋友提供一点学习思路。COM的全称是Component Object Module,组件对象模型。组件就我自己的理解就是将各个功能部分编写成可重用的模块,程序就好像搭积木一样由这些可重用模块构成,这样将各个模块的耦合降到最低,以后升级修改功能只需要修改某一个模块,这样就大大降低了维护程序的难度和成本,提高程序的可扩展性。COM是微软公司提出的组件标准,同时微软也定义了组件程序之间进行交互的标准,提供了组件程序运行所需的环境。COM是基于组件化编程的思想,在COM中每一个组件成为一个模块,它可以是动态链接库或者可执行文件,一个组件程序可以包含一个或者多个组件对象,COM对象不同于OOP(面向对象)中的对象,COM对象是定义在二进制机器代码基础之上,是跨语言的。而OOP中的对象是建立在语言之上的。脱离了语言对象也就不复存在.COM是独立在编程语言之上的,是语言无关的。COM的这一特性使得不同语言开发的组件之间的互相交互成为可能。COM对象和接口COM中的对象类似于C++中的对象,对象是某个类中的实例。而类则是一组相关的数据和功能组合在一起的一个定义。使用对象的应用(或另一个对象)称为客户,有时也称为对象的用户。 接口是一组逻辑相关的函数的集合,比如一组处理URL的接口,处理HTTP请求的接口等等。在习惯上接口通常是以"I"开头。对象通过接口成员函数为客户提供各种形式的服务。一个对象可以拥有多个不同的接口,以表现不同的功能集合。 在C++语言中,一个接口就是一个虚基类,而对象就是该接口的实现类,派生自该接口并实现接口的功能。class IBook { public: virtual void NextPage() = 0; virtual void ForwardPage() = 0; } class IAppliances { public: virtual void charge() = 0; virtual void shutdown() = 0; } class CKindle: public IBook, IAppliances { public: virtual void NextPage(); virtual void ForwardPage(); virtual void charge(); virtual void shutdown(); }就像上面的例子,上面的例子中提供了一个书本的接口,书本可以翻到上一页,下一页,而电器有充电和关机的接口,最后我们利用kindle这个类来实现这两个接口。所以在使用上我们可以利用下面的伪代码来使用pInterface = CreateInterface(ID_IBOOK, ID_KINDLE); pInterface->NextPage(); if(Late()) { pInter2 = pInterface->QueryInterface(ID_APPLIANCES); pInter2->shutdown(); }在平时我们使用kindle的翻页功能来看书,因为翻页功能在接口IBook,所以首先调用一个创建接口的函数,传入对应接口以及接口实现类的标识,用来生成相应的接口,其实在内部也就是根据类ID来创建一个对应的实现类的实例。然后根据需要转化为对应基类的指针。在看书看累的时候,将接口转化为电子产品的接口,调用对应的关机功能,关闭电子书。在之后比如说kindle进行了升级,也就是重写了实现这些接口的代码,但是接口原型不变,这样使用接口的代码不用改变,也就是说即使kindle对内部进行了升级,优化某些功能,用户在使用上仍然是那样在用,不必改变使用习惯。再比如kindle出了一个新款,提供了背光功能,这个时候可能提供一个新接口:class IAppliances2 : public IAppliances { public: virtual void Light() = 0; }然后只需要稍微更新一下CKindle这个实现类,新增一个Light接口的实现,在使用上如果不用背光功能原来的代码就够用了,如果要使用背光功能,只需要将原来的接口类型改为IAppliances2 ,并且添加调用背光功能的函数,而其余的功能也不变,这与实际生活相似,某个产品提供新功能时,一般保持原始功能的使用方法不变,新功能会有新的按钮或者其他方法进行打开。再比如说我不想用kindle了改用其他的电子阅读器,只要接口不变,我的使用方法基本不变,唯一改变的可能是我以前拿着kindle,现在拿着其他品牌的阅读器,也就是说可能要改变传入CreateInterface函数中的类标识。COM基本接口COM中所有接口都派生自该接口:struct IUnknown { virtual HRESULT QueryInterface(REFIID riid,void **ppvObject) = 0; virtual ULONG AddRef( void) = 0; virtual ULONG Release( void) = 0; };所有类都应该实现上述三个方法,AddRef主要将接口的引用计数+1, 而Release则是将引用计数 -1,当对象的引用计数为0,则会调用析构函数,释放对象的存储空间。每一次接口的创建和转化都会增加引用计数,而每次不再使用调用Release,都会把引用计数 -1,当引用计数为0时会释放对象的空间。QueryInterface主要用来进行接口转化,将对象的指针转化为另外一个接口的指针,就好像上面例子中pInter2 = pInterface->QueryInterface(ID_APPLIANCES);这句代码将之前的Ibook接口转化为电子产品的接口。在C++中也就是做了一次强制类型转化。对象和接口的唯一标识在COM中,对象本身对于客户来说是不可见的,客户请求服务时,只能通过接口进行。每一个接口都由一个128位的全局唯一标识符(GUID,Global Unique Identifier)来标识。客户通过GUID来获得接口的指针,再通过接口指针,客户就可以调用其相应的成员函数。与接口类似,每个组件也用一个 128 位 GUID 来标识,称为 CLSID(class identifer,类标识符或类 ID),用 CLSID 标识对象可以保证(概率意义上)在全球范围内的唯一性。实际上,客户成功地创建对象后,它得到的是一个指向对象某个接口的指针,因为 COM 对象至少实现一个接口(没有接口的 COM 对象是没有意义的),所以客户就可以调用该接口提供的所有服务。根据 COM 规范,一个 COM 对象如果实现了多个接口,则可以从某个接口得到该对象的任意其他接口。由此可看出,客户与 COM 对象只通过接口打交道,对象对于客户来说只是一组接口。在COM中GUID的定义如下:typedef struct _GUID { unsigned long Data1; unsigned short Data2; unsigned short Data3; unsigned char Data4[ 8 ]; } GUID;一般我们在程序中只是作为一个标志来使用,并不对它进行特别的操作。生成它一般是使用VS自带的GUID生成工具。而CLSID的定义如下:typedef GUID CLSID;其实在COM中一般涉及到ID的都是GUID,只是利用typedef另外定义了一个名称而已另外COM也提供了一组函数用来对GUID进行操作:函数功能IsEqualGUID判断GUID是否相等IsEqualCLSID判断CLSID是否相等IsEqualIID判断IID是否相等CLSIDFromProgID把字符串形式的CLSID转化为CLSID结构形式(类似于将字符串的234转化为数字,也是把字面上的CLSID转化为计算机能识别的CLSID)StringFromCLSID把CLSID转化为字符串形式IIDFromString把字符串形式的IID转化为IID接口形式StringFromIID把IID结构转化为字符串StringFromGUID2把GUID形式转化为字符串形式COM接口的一般使用步骤一般使用COM中的时候首先使用CoInitialize初始化COM环境,不用的时候使用CoUninitialize卸载COM环境,在使用接口中一般需要进行下面的步骤调用CoCreateInstance函数传入对应的CLSID和对应的IID,生成对应对象并传入相应的接口指针。使用该指针进行相关操作调用接口的QueryInterface函数,转化为其他形式的接口在最后分别调用各个接口的Release函数,释放接口下面提供一个小例子,以供参考,也方便更好的理解COM//组件部分 extern "C" __declspec(dllexport) void __stdcall ComCreateObject(GUID clsID, GUID interfaceID, void** pObj); void __stdcall ComCreateObject(GUID clsID, GUID interfaceID, void** pObj) { if (clsID == CLSID_COMSTRING) { CComString *pComObject = new CComString; *pObj = pComObject->QueryInterface(interfaceID); } } class IComBase { public: virtual void* QueryInterface(GUID gInterfaceId) = 0; virtual void AddRef() = 0; virtual void Release() = 0; }; static const GUID IID_ICOMSTRING = { 0xb2fcd22c, 0x63fa, 0x4f61, { 0xbf, 0x12, 0xd3, 0xd2, 0x5a, 0x99, 0x59, 0x24 } }; class IComString : public IComBase { public: virtual void Init(LPCTSTR pStr) = 0; virtual int Find(LPCTSTR lpSubStr) = 0; virtual int GetLength() = 0; }; static const GUID CLSID_COMSTRING = { 0xf57f3489, 0xff2d, 0x4c97, { 0xb1, 0xf6, 0xc, 0x60, 0x7e, 0xf7, 0xae, 0xfc } }; class CComString : public IComString { public: virtual void* QueryInterface(GUID gInterfaceId); virtual void AddRef(); virtual void Release(); virtual void Init(LPCTSTR pStr); virtual int Find(LPCTSTR lpSubStr); virtual int GetLength(); protected: int m_nCnt = 0; CString m_csString; }; //cpp void* CComString::QueryInterface(GUID gInterfaceId) { if (gInterfaceId == IID_ICOMSTRING) { //该接口的引用计数+1 AddRef(); return dynamic_cast<IComString*>(this); } //如果它还实现了其他接口,可以再写判断,生成其他类型的接口 return NULL; } void CComString::AddRef() { m_nCnt++; } void CComString::Release() { m_nCnt--; //引用计数为0,此时没有该类的接口被使用,应该释放该类 if (m_nCnt == 0) { delete this; } } void CComString::Init(LPCTSTR pStr) { m_csString = pStr; } int CComString::Find(LPCTSTR lpSubStr) { return m_csString.Find(lpSubStr); } int CComString::GetLength() { return m_csString.GetLength(); }这些代码被封装在一个dll中,dll中导出一个函数ComCreateObject,外部在使用时调用该函数传入对应的ID,以便生成对应的接口。在这个dll里面提供一个接口的基类IComBase,这个是仿照了COM种的IUnknow基类,另外定义了一个IComString字符串的接口,同时定义了它的实现类CComString,为了简单,它的功能方法我直接使用了一个CString类实现。在函数ComCreateObject,会根据传入对应的类ID,来生成对应的类实例,然后调用实例的QueryInterface,转化成对应的接口,在实现类中实现了这个方法,实现类中的QueryInterface方法主要完成了类型转化并将引用计数+1。而Release函数在每次-1的时候会进行判断,当引用计数为0时销毁该类的实例由于类是new出来创建在堆上的,所以每次用完一定要记得调用Release释放,否则会造成内存泄露注意:在使用这里使用的是dynamic_cast进行类型转化,在进行类的强制类型转化时,特别是在有多重继承的情况下,最好使用dynamic_cast方式进行转化,当一个类拥有多个基类时,类中有多个虚函数表,为了能正常找到对应的虚函数表,就需要进行对应的偏移量的计算,C中的强制类型转化是直接将对象的首地址进行转化,这样在寻址虚函数表时可能会出错。而dynamic_cast会进行对应的计算。详细情形请参考这里在使用上void ComInitialize(); void ComUninitialize(); typedef void(__stdcall *pfnCreateInstance)(GUID, GUID, void**); pfnCreateInstance CreateInstance; HMODULE hComDll = NULL; int _tmain(int argc, _TCHAR* argv[]) { ComInitialize(); IComString *pIString = NULL; CreateInstance(CLSID_COMSTRING, IID_ICOMSTRING, (void**)&pIString); pIString->Init(_T("Hello World")); IComString* pIString2 = (IComString*)(pIString->QueryInterface(IID_ICOMSTRING)); int nLength = pIString2->GetLength(); int iPos = pIString2->Find(_T("World")); printf("%d, %d\n", nLength, iPos); pIString->Release(); pIString2->Release(); return 0; } void ComInitialize() { hComDll = LoadLibrary(_T("ComInterface.dll")); if (NULL != hComDll) { CreateInstance = (pfnCreateInstance)GetProcAddress(hComDll, "ComCreateObject"); } } void ComUninitialize() { FreeLibrary(hComDll); }给使用者使用时只需要提供对应类和接口的GUID,然后将函数ComCreateObject原型提供给调用者,以便生成对应的接口。这里为了模仿COM的使用定义了ComInitialize和ComUninitialize这两个函数,真实的初始化函数怎么写的,我也不知道,在这里只是为了模仿COM的使用。至此相信各位小伙伴应该对COM有了一个初步的了解
2017年10月12日
4 阅读
0 评论
0 点赞
2017-09-11
Windows服务框架与服务的编写
从NT内核开始,服务程序已经变为一种非常重要的系统进程,一般的驻守进程和普通的程序必须在桌面登录的情况下才能运行,而许多系统的基础程序必须在用户登录桌面之前就要运行起来,而利用服务,可以很方便的实现这种功能,而且服务程序一般不予用户进行交互,可以安静的在后台执行,合理的利用服务程序可以简化我们的系统设计,比如Windows系统的日志服务,IIS服务等等。服务程序本身是依附在某一个可执行文件之中,系统将服务安装在注册表中的HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services位置,当需要执行服务程序时,由系统的服务控制管理器在注册表中对应的位置读取服务信息,并启动对应的程序。下面从几个方面详细说明服务程序的基本框架服务程序的框架服务程序本身也是依附在exe或者dll文件中,一般一个普通的可执行文件中可以包含一个或者多个服务,但是为了代码的维护性,一般一个程序总是只包含一个服务。服务程序是由服务管理器负责调度,控制的,所以我们在编写服务程序的时候必须满足服务控制管理器的调度,必须包含:立即调用StartServiceCtrlDispatchar函数把进程的主线程连接到ServiceControlManager的主函数在进程中运行的各个服务的入口点函数ServiceMain在进程中运行的各个服务的控制处理函数HandlerServiceControlManager函数的原型如下:BOOL WINAPI StartServiceCtrlDispatcher( __in const SERVICE_TABLE_ENTRY* lpServiceTable );函数参数是一个SERVICE_TABLE_ENTRY类型的指针,这个类型的定义如下:typedef struct _SERVICE_TABLE_ENTRY { LPTSTR lpServiceName; LPSERVICE_MAIN_FUNCTION lpServiceProc; } SERVICE_TABLE_ENTRY, *LPSERVICE_TABLE_ENTRY;这个结构是一个服务名称和对应入口函数指针的映射。在传入的时候必须给一个该类型的数组,数组的每一项都代表一个服务与其入口函数指针的映射,同时这个数组的最后一组必须为NULL当启动服务的时候,系统会启动对应的进程,当进程代码执行到StartServiceCtrlDispatcher时,程序由服务控制管理器接管,服务控制管理器根据需要启动的服务名称,在传入的数组指针中,找到对应的入口函数,然后调用它,当对应的入口函数返回时结束服务,并将后续代码的控制权转交给对应主进程,由主进程接着执行后面的代码在入口函数中我们必须给服务一个控制管理程序,这个程序主要是用来处理服务程序接受到的各种控制消息,比如启动服务,暂停服务,停止服务等,这个函数有点类似于Windows 窗口程序中的窗口过程。这个函数由我们自己编写,然后调用函数RegisterServiceCtrlHandler(Ex) 将服务名称与对应的控制函数绑定,每当有一个控制事件发生时都会调用我们注册的函数进行处理,RegisterServiceCtrlHandler函数会返回一个句柄,作为服务的控制句柄。当我们要自己向服务控制管理器报告服务的当前状态时需要这个句柄。服务的启动过程已经安装的服务,被系统存储在注册表的HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services位置处,这个注册表项纪录了服务所依赖的exe或者dll文件,它的启动类型等信息,当我们尝试启动服务的时候,系统会在注册表的对应位置查找是否存在对应服务的表项,如果存在则启动对应的进程。当进程的代码执行到StartServiceCtrlDispatcher函数时,该进程将由服务控制管理器接管,服务控制管理器将会根据填入的SERVICE_TABLE_ENTRY,找到服务所对应的入口函数开启对应的服务线程并调用,在入口函数处会注册一个控制句柄,然后应该向服务控制管理程序报告当前状态为正在启动,然后执行服务的正式代码。(注意:由于服务的入口函数需要自己编写,所以这里提到的注册控制句柄,报告状态都应该是由程序员自己编写代码实现)Handler函数handler函数用来处理服务的控制请求,这个函数由RegisterServiceCtrlHandler(Ex)函数注册到系统,当服务控制请求到来时,由服务的主线程的控制分发线程来调用。综合上面的内容,可以看到一个服务程序应该是至少涉及到3个线程,进程的主线程,服务线程,控制分发线程,RegisterServiceCtrlHandler(Ex)的原型如下:SERVICE_STATUS_HANDLE WINAPI RegisterServiceCtrlHandlerEx( __in LPCTSTR lpServiceName, __in LPHANDLER_FUNCTION_EX lpHandlerProc, __in_opt LPVOID lpContext );不带Ex的版本只有前两个参数,带Ex版本的第3个参数是一个传入到对应的控制函数中的参数。对应提供的控制管理函数的原型如下:DWORD WINAPI HandlerEx( __in DWORD dwControl, __in DWORD dwEventType, __in LPVOID lpEventData, __in LPVOID lpContext );第一个参数是一个控制码,类似于GUI程序中的消息,根据这个控制码就可以知道对应的控制消息,下面列举常见的控制码:控制码含义SERVICE_CONTROL_STOP请求服务停止SERVICE_CONTROL_PAUSE请求暂停服务SERVICE_CONTROL_CONTINUE请求恢复暂停的服务SERVICE_CONTROL_INTERROGATE请求服务立即更新它的当前状态信息给服务控制管理程序SERVICE_CONTROL_SHUTDOWN请求服务执行清理任务,因为系统正在关机.由于只有非常有限的时间用来关机,所以这个控制只应由绝对需要关机的服务使用.例如:事件登录服务需要清理维护的文件中的脏字节,或服务需要关机以便当系统在关机状态时网络连接不能进行. 如果服务关键要花时间,并发出STOP_PENDING状态信息,强烈建议这些消息包括一个等待提示使得服务控制程序知道在给系统指明服务关机完成之前要等多长时间.系统给服务控制管理器有限的时间(约20秒)完成服务关机,在这个时间后无论服务关机动作是否完成都进行系统关机第二个参数是事件类型,对于有的控制码,它可能含有子控制类型来详细描述它,就好像WM_COMMAND消息中有子控件的相关消息第三个参数是事件参数,这个参数是子控制码对应的参数第四个参数是上面带Ex的函数第三个参数传进来的内容每次Handler函数被调用时,服务必须调用SetServiceStatus函数把状态报告给服务管理器程序注意:即使状态无变化也要报告服务控制管理器在服务中一般有3类对象(在这并不是指Windows系统的内核对象,这里只是为了便于理解给出的一个分类):服务程序对象:服务本身的代码,一般是服务主要完成的功能代码服务控制对象:用来控制服务,向服务发送执行服务管理对象:用来响应对应的控制码,主要是指服务的handler函数与GUI程序相类比,服务对象就好比GUI程序本身,服务控制对象就好像我们在操作GUI程序,比如点击鼠标,而服务控制对象就像窗口的窗口过程服务管理器由SCManager对象代表。SCManager对象是持有服务对象的容器对象。SCManager对象和服务对象的句柄类型是SC_HANDLE。我们可以使用函数OpenService来在服务管理器中打开对应服务获取服务对象的句柄,或者使用函数CreateService在服务管理器中创建一个新服务并返回服务的句柄后面关于服务的控制操作请参考本人之前写的一篇关于服务控制管理器的编写的博客点击这里下面通过一个封装的Service库来说明服务程序的框架。这个简单的类的详细代码请点击这里下载该项目中主要定义了三个类,其中CFSZService类是所有服务类的基类,CServiceCtrl是服务的控制类,该类用于控制服务,这个类中的所有函数都是静态函数。另外为了测试我从CFSZService类上派生了一个类——CTestService,用来编写服务的具体代码。如果以后想要使用这个项目中的代码,可以进行如下操作:FSZService类中派生一个新类,并重载基类的RunService,在这个服务中编写具体的服务代码即可在相应位置调用DECLARE_SERVICE_TABLE_ENTRY宏,用来声明一个SERVICE_TABLE_ENTRY变量,用来绑定服务和对应的入口函数在相应位置添加代码:IMPLAMENT_SERVICE_MAIN(GetSystemInfoService, CTestService) BEGIN_SERVICE_MAP() ON_SERVICE_MAP(GetSystemInfoService, CTestService) END_SERIVCE_MAP()第一个宏用来定义了一个函数,该函数是服务的入口函数,需要传入服务名称,服务的类名称。第二个宏用来将服务名和它对应的入口函数进行绑定。在主函数处调用CFSZService::RegisterService(),在该函数里面会调用StartServiceCtrlDispatcher,一遍让服务控制管理程序来接管服务代码代码的整体说明服务基类的定义如下:class CFSZService { public: typedef CAtlMap<CString, CFSZService *> CFSZServiceMap; //服务名称和对应的服务对象 CFSZService(const CString& csSrvName); ~CFSZService(void); virtual DWORD Run(DWORD dwArgc, LPTSTR* lpszArgv); virtual BOOL OnInitService(DWORD dwArgc, LPTSTR* lpszArgv); //初始化服务 virtual DWORD RunService(); //运行服务 void SetServiceStatusHandle(SERVICE_STATUS_HANDLE); static DWORD WINAPI HandlerEx(DWORD dwControl, DWORD dwEventType, LPVOID lpEventData, LPVOID lpContext); static BOOL RegisterService(); //服务命令处理函数 protected: virtual DWORD OnStop(); virtual DWORD OnUserControl(DWORD dwControl); virtual DWORD OnStart(); virtual DWORD OnContinue(); virtual DWORD OnPause(); virtual DWORD OnShutdown(); virtual DWORD OnInterrogate(); virtual DWORD OnShutDown(); protected://设备变更事件通知处理 SERVICE_CONTROL_DEVICEEVENT virtual DWORD OnDeviceArrival(PDEV_BROADCAST_HDR pDbh){return 0;} virtual DWORD OnDeviceRemoveComplete(PDEV_BROADCAST_HDR pDbh){return 0;} virtual DWORD OnDeviceQueryRemove(PDEV_BROADCAST_HDR pDbh){return 0;} virtual DWORD OnDeviceQueryRemoveFailed(PDEV_BROADCAST_HDR pDbh){return 0;} virtual DWORD OnDeviceRemovePending(PDEV_BROADCAST_HDR pDbh){return 0;} virtual DWORD OnCustomEvent(PDEV_BROADCAST_HDR pDbh){return 0;} protected://硬件配置文件发生变动 SERVICE_CONTROL_HARDWAREPROFILECHANGE virtual DWORD OnConfigChanged(){return 0;} virtual DWORD OnQueryChangeConfig(){return 0;} virtual DWORD OnConfigChangeCanceled(){return 0;} protected://设备电源事件 SERVICE_CONTROL_POWEREVENT virtual DWORD OnPowerSettingChange(PPOWERBROADCAST_SETTING pPs){return 0;} protected://session 发生变化 SERVICE_CONTROL_SESSIONCHANGE virtual DWORD OnWTSConsoleConnect(PWTSSESSION_NOTIFICATION pWn){return 0;} virtual DWORD OnWTSConsoleDisconnect(PWTSSESSION_NOTIFICATION pWns){return 0;} virtual DWORD OnWTSRemoteConnect(PWTSSESSION_NOTIFICATION pWns){return 0;} virtual DWORD OnWTSRemoteDisconnect(PWTSSESSION_NOTIFICATION pWns){return 0;} virtual DWORD OnWTSSessionLogon(PWTSSESSION_NOTIFICATION pWns){return 0;} virtual DWORD OnWTSSessionLogoff(PWTSSESSION_NOTIFICATION pWns){return 0;} virtual DWORD OnWTSSessionLock(PWTSSESSION_NOTIFICATION pWns){return 0;} virtual DWORD OnWTSSessionUnLock(PWTSSESSION_NOTIFICATION pWns){return 0;} virtual DWORD OnWTSSessionRemoteControl(PWTSSESSION_NOTIFICATION pWns){return 0;} protected: //内部的工具方法,设置服务为一个指定的状态 BOOL SetStatus(DWORD dwStatus,DWORD dwCheckPoint = 0,DWORD dwWaitHint = 0 ,DWORD dwExitCode = 0,DWORD dwAcceptStatus = SERVICE_CONTROL_INTERROGATE); BOOL SetStartPending(DWORD dwCheckPoint = 0,DWORD dwWaitHint = 0); //设为正在启动状态 BOOL SetContinuePending(DWORD dwCheckPoint = 0,DWORD dwWaitHint = 0); //设为正在继续运行状态 BOOL SetPausePending(DWORD dwCheckPoint = 0,DWORD dwWaitHint = 0); //设为正在暂停状态 BOOL SetPause(); //设为暂停状态 BOOL SetRunning(); //设为以启动状态 BOOL SetStopPending(DWORD dwCheckPoint = 0,DWORD dwWaitHint = 0); //设为正在停止状态 BOOL SetStop(DWORD dwExitCode = 0); //设为以停止状态 BOOL ReportStatus(DWORD, DWORD, DWORD);//向服务管理器报告当前服务状态 protected: CString m_csSrvName; //服务名称 DWORD m_dwCurrentStatus; //当前状态 SERVICE_STATUS_HANDLE m_hCtrl; //控制句柄 public: static CFSZServiceMap ms_SrvMap; };在这个基类中主要定义了3类函数,分别是:服务本身的代码函数:用来处理服务的业务,实现服务的功能服务控制管理函数:包括各种控制消息的响应函数和服务控制句柄的管理函数服务状态设置函数:主要用来设置服务的状态该项目使用Atl 和CString,一般在控制台程序中想要使用这二者只需要包含头文件:atlcoll.h、atlstr.h即可CFSZServiceMap 成员该成员是用来将服务名称和对应的类对象关联起来,这样以后根据服务名称就可以找到对应的服务类的对象指针,该类型定义如下:typedef CAtlMap<CString, CFSZService *> CFSZServiceMap;在每个类的构造函数中进行初始化:CFSZService::CFSZService(const CString& csSrvName) { m_csSrvName = csSrvName; ms_SrvMap.SetAt(m_csSrvName, this); }服务的入口函数服务的入口函数是利用宏定义的一个函数,每当需要添加一个服务的时候都需要调用宏IMPLAMENT_SERVICE_MAIN来定义一个对应的服务入口ServiceMain,该函数的定义如下:#define IMPLAMENT_SERVICE_MAIN(srvName, className)\ VOID WINAPI _ServiceMain_##className(DWORD dwArgc, LPTSTR* lpszArgv)\ {\ CFSZService *pThis = NULL;\ if(!CFSZService::ms_SrvMap.Lookup(_T(#srvName), pThis))\ {\ pThis = dynamic_cast<CFSZService*>( new className(_T(#srvName)) );\ }\ else\ {\ return;\ }\ assert(NULL != pThis);\ SERVICE_STATUS_HANDLE hss = RegisterServiceCtrlHandlerEx(_T(#srvName), CFSZService::HandlerEx, reinterpret_cast<LPVOID>(pThis));\ assert(NULL != hss);\ pThis->SetServiceStatusHandle(hss);\ pThis->Run(dwArgc, lpszArgv);\ delete dynamic_cast<##className*>(pThis);\ }上面的代码首先根据传入的类名动态创建了一个服务类(由于这里服务对象都是动态创建和销毁的,所以在其他地方不需要创建服务对象),然后调用RegisterServiceCtrlHandlerEx构造了一个服务控制句柄,然后调用类的SetServiceStatusHandle函数来将对应的服务控制句柄保存起来最后调用Run函数来运行服务的正式代码,最后当Run函数执行完毕后,服务的相应工作也做完了,这个时候删除了这个类。Run函数的定义如下:DWORD CFSZService::Run(DWORD dwArgc, LPTSTR* lpszArgv) { assert(NULL != this); if (OnInitService(dwArgc, lpszArgv)) { RunService(); } return 0; }这个函数中使用了OnInitService函数来进一步初始化服务相关信息,该函数提供了一个服务初始化的时机。比如调用相关函数进行socket的初始化或者对com环境进行初始化等等。然后调用RunService执行服务正式的代码。HandlerEx函数在前面的宏IMPLAMENT_SERVICE_MAIN中调用了RegisterServiceCtrlHandlerEx将函数HandlerEx作为服务控制码的处理函数,调用的时候将服务类对象的指针通过第四个参数传入,这样在静态函数中就可以使用服务的类成员函数,函数HandlerEx的部分代码如下: DWORD dwRet = ERROR_SUCCESS; if( NULL == lpContext ) { return ERROR_INVALID_PARAMETER; } CFSZService*pService = reinterpret_cast<CFSZService*>(lpContext); if( NULL == pService ) { return ERROR_INVALID_PARAMETER; } switch(dwControl) { case SERVICE_CONTROL_STOP: //0x00000001 停止服务器 { dwRet = pService -> OnStop(); } break; ... }在该函数中,将所有的控制吗都列举出来,针对不同的控制吗都调用的对应的处理函数,并且这些函数都是虚函数,所以在派生类中需要处理某个控制消息就重写某个对应的函数即可。最后再重新屡一下这个类在调用时的基本情况:在主函数中调用CFSZService::RegisterService();函数将之前我们通过一组BEGIN_SERVICE_MAP、ON_SERVICE_MAP、END_SERVICE_MAP组成的映射关系注册到系统的服务控制管理器中。这个函数单独调用了StartServiceCtrlDispatcher函数,一旦代码执行到这个地方,服务控制管理器会根据之前绑定的服务名称与入口函数的对应关系调用对应的入口函数入口函数是通过宏IMPLAMENT_SERVICE_MAIN定义的,在入口函数中首先动态创建了一个服务类,然后给这个服务注册服务控制句柄,并且服务控制函数为HandlerEx。接着,服务的入口函数调用对应服务的Run函数,在Run函数中调用OnInitService进行服务的初始化和调用RunService执行服务的正式代码,所以在重载类中可以重载这两个方法进行初始化和进行服务的相关操作当外部对服务进行控制时,服务控制管理器调用HandleEx函数进行相关的操作在HandleEx中会解析对应的控制事件,并调用对应的虚函数,所以如果想要处理某个消息,则重写对应的控制函数即可
2017年09月11日
5 阅读
0 评论
0 点赞
1
...
5
6
7
...
15