首页
归档
友情链接
关于
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
菜谱
页面
归档
友情链接
关于
搜索到
84
篇与
的结果
2018-06-29
WinSock 重叠IO模型
title: WinSock 重叠IO模型tags: [WinSock 模型, 网络编程, 重叠IO模型]date: 2018-06-29 20:26:13categories: Windows 网络编程keywords: WinSock 模型, 网络编程, 重叠IO模型之前介绍的WSAAsyncSelect和WSAEvent模型解决了收发数据的时机问题,但是网卡这种设备相比于CPU和内存来说仍然是慢速设备,而调用send和recv进行数据收发操作仍然是同步的操作,即使我们能够在恰当的时机调用对应的函数进行收发操作,但是仍然需要快速的CPU等待慢速的网卡。这样仍然存在等待的问题,这篇博文介绍的重叠IO模型将解决这个等待的问题之前介绍的WSAAsyncSelect和WSAEvent模型解决了收发数据的时机问题,但是网卡这种设备相比于CPU和内存来说仍然是慢速设备,而调用send和recv进行数据收发操作仍然是同步的操作,即使我们能够在恰当的时机调用对应的函数进行收发操作,但是仍然需要快速的CPU等待慢速的网卡。这样仍然存在等待的问题,这篇博文介绍的重叠IO模型将解决这个等待的问题重叠IO简介一般接触重叠IO最早是在读写磁盘时提出的一种异步操作模型,它主要思想是CPU只管发送读写的命令,而不用等待读写完成,CPU发送命令后接着去执行自己后面的命令,至于具体的读写操作由硬件的DMA来控制,当读写完成时会向CPU发送一个终端信号,此时CPU中断当前的工作转而去进行IO完成的处理。这是在磁盘操作中的一种高效工作的方式,为什么在网络中又拿出来说呢?仔细想想,前面的模型解决了接收数据的时机问题,现在摆在面前的就是如何高效的读写数据,与磁盘操作做类比,当接收到WSAAsyncSelect对应的消息或者WSAEvent返回时就是执行读写操作的时机,下面紧接着就是调用对应的读写函数来进行读写数据了,而联想到linux中的一切皆文件的思想,我们是不是可以认为操作网卡也是在操作文件?这也是在WinSock1中,使用WriteFile和ReadFile来进行网络数据读写的原因。既然它本质上也是CPU需要等待慢速的设备,那么为了效率它必定可以支持异步操作,也就可以使用重叠IO。创建重叠IO的socket要想使用重叠IO,就不能在像之前那样使用socket函数来创建SOCKET, 这函数最多只能创建一个普通SOCKET然后设置它为非阻塞(请注意非阻塞与异步的区别)。要创建异步的SOCKET需要使用WinSock2.0函数 WSASocketSOCKET WSASocket( int af, int type, int protocol, LPWSAPROTOCOL_INFO lpProtocolInfo, GROUP g, DWORD dwFlags );该函数的前3个参数与socket的参数含义相同,第4个参数是一个协议的具体信息,配合WSAEnumProtocols 使用可以将枚举出来的网络协议信息传入,这样不通过前三个参数就可以创建一个针对具体协议的SOCKET。第5个参数目前不受支持简单的传入0即可。第6个参数是一个标志,如果要创建重叠IO的SOCKET,需要将这个参数设置为WSA_FLAG_OVERLAPPED。否则普通的SOCKET直接传入0即可使用重叠IO除了要将SOCKET设置为支持重叠IO外,还需要使用对应的支持重叠IO的函数,之前了解的巴克利套接字函数最多只能算是支持非阻塞而不支持异步。在WinSock1.0 中可以使用ReadFile和WriteFile来支持重叠IO,但是WinSock2.0 中重新设计的一套函数来支持重叠IOWSASend (send的等价函数)WSASendTo (sendto的等价函数)WSARecv (recv的等价函数)WSARecvFrom (recvfrom的等价函数)WSAIoctl (ioctlsocket的等价函数)WSARecvMsg (recv OOB版的等价函数)AcceptEx (accept 等价函数)ConnectEx (connect 等价函数)TransmitFile (专门用于高效发送文件的扩展API)TransmitPackets (专门用于高效发送大规模数据包的扩展API)DisconnectEx (扩展的断开连接的Winsock API)WSANSPIoctl (用于操作名字空间的重叠I/O版扩展控制API)那么如果使用上述函数但是传入一个非阻塞的SOCKET会怎么样呢,这些函数只看是否传入OVERLAPPED结构而不管SOCKET是否是阻塞的,一律按重叠IO的方式来运行。这也就是说,要使用重叠I/O方式来操作SOCKET,那么不一定非要一开初就创建一个重叠I/O方式的SOCKET对象(但是针对AcceptEx 来说如果传入的是普通的SOCKET,它会以阻塞的方式执行。当时测试时我传入的是使用WSASocket创建的SOCKET,我将函数的最后一个标志设置为0,发现AcceptEx只有当客户端连接时才会返回)重叠IO的通知模型与文件的重叠IO类似,重叠IO的第一种模型就是事件通知模型.利用该模型首先需要把一个event对象绑定到OVERLAPPED(WinSokc中一般是WSAOVERLAPPED)上,然后利用这个OVERLAPPED结构来进行IO操作.如:WSASend/WSARecv等判断对应IO操作的返回值,如果使用重叠IO模式,IO操作函数不会返回成功,而是会返回失败,使用WSAGetLastError得到的错误码为WSA_IO_PENDING,此时认为函数进行一种待决状态,也就是CPU将命令发送出去了,而任务没有最终完成然后CPU可以去做接下来的工作,而在需要操作结果的地方调用对应的等待函数来等待对应的事件对象。如果事件对象为有信号表示操作完成接着可以设置事件对象为无信号,然后继续投递IO操作.要等待这些事件句柄,可以调用WSAWaitForMultipleEvents函数,该函数原型如下:DWORD WSAWaitForMultipleEvents( __in DWORD cEvents, __in const WSAEVENT* lphEvents, __in BOOL fWaitAll, __in DWORD dwTimeout, __in BOOL fAlertable );第一个参数是事件对象的数目;第二个参数是事件对象的数组首地址;第三个参数是一个bool类型表示是否等待数组中所有的对象都变为有信号;第四个参数表示超时值;第五个参数是表示在等待的时候是否进入可警告状态在函数返回后我们只知道IO操作完成了,但是完成的结果是成功还是失败是不知道的,此时可以使用WSAGetOverlappedResult来确定IO操作执行的结果,该函数原型如下:BOOL WSAGetOverlappedResult( SOCKET s, LPWSAOVERLAPPED lpOverlapped, LPDWORD lpcbTransfer, BOOL fWait, LPDWORD lpdwFlags );第一个参数是对应的socket;第二个参数是对应的OVERLAPPED结构;第三个参数是一个输出参数,表示完成IO操作的字节数,通常出错的时候返回0;第四个参数指明调用者是否等待一个重叠I/O操作完成,通常在成功等待到事件句柄后,这个参数在这个模型中没有意义了;第五个参数是一个输出参数负责接收完成结果的标志。下面是一个事件通知模型的例子typedef struct _tag_CLIENTCONTENT { OVERLAPPED Overlapped; SOCKET sClient; WSABUF DataBuf; char szBuf[WSA_BUFFER_LENGHT]; WSAEVENT hEvent; }CLIENTCONTENT, *LPCLIENTCONTENT; int _tmain(int argc, TCHAR *argv[]) { WSADATA wd = {0}; WSAStartup(MAKEWORD(2, 2), &wd); CLIENTCONTENT ClientContent[WSA_MAXIMUM_WAIT_EVENTS] = {0}; WSAEVENT Event[WSA_MAXIMUM_WAIT_EVENTS] = {0}; int nTotal = 0; SOCKET skServer = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_IP, NULL, 0, WSA_FLAG_OVERLAPPED); SOCKADDR_IN ServerAddr = {0}; ServerAddr.sin_family = AF_INET; ServerAddr.sin_port = htons(SERVER_PORT); ServerAddr.sin_addr.s_addr = htonl(INADDR_ANY); bind(skServer, (SOCKADDR*)&ServerAddr, sizeof(SOCKADDR)); listen(skServer, 5); printf("开始监听...........\n"); Event[nTotal] = WSACreateEvent(); ClientContent[nTotal].hEvent = Event[nTotal]; ClientContent[nTotal].Overlapped.hEvent = Event[nTotal]; ClientContent[nTotal].DataBuf.len = WSA_BUFFER_LENGHT; ClientContent[nTotal].sClient = skServer; //针对监听套接字做特殊的处理 WSAEventSelect(skServer, Event[0], FD_ACCEPT | FD_CLOSE); nTotal++; while (TRUE) { DWORD dwTransfer = 0; DWORD dwFlags = 0; DWORD dwNumberOfBytesRecv = 0; int nIndex = WSAWaitForMultipleEvents(nTotal, Event, FALSE, WSA_INFINITE, FALSE); WSAResetEvent(Event[nIndex - WSA_WAIT_EVENT_0]); //监听socket返回 if (nIndex - WSA_WAIT_EVENT_0 == 0) { SOCKADDR_IN ClientAddr = {AF_INET}; int nClientAddrSize = sizeof(SOCKADDR); SOCKET skClient = WSAAccept(skServer, (SOCKADDR*)&ClientAddr, &nClientAddrSize, NULL, NULL); if (SOCKET_ERROR == skClient) { printf("接受客户端连接请求失败,错误码为:%08x\n", WSAGetLastError()); continue; } printf("有客户端连接进来[%s:%u]\n", inet_ntoa(ClientAddr.sin_addr), ntohs(ClientAddr.sin_port)); Event[nTotal] = WSACreateEvent(); ClientContent[nTotal].hEvent = Event[nTotal]; ClientContent[nTotal].Overlapped.hEvent = Event[nTotal]; ClientContent[nTotal].DataBuf.len = WSA_BUFFER_LENGHT; ClientContent[nTotal].DataBuf.buf = ClientContent[nTotal].szBuf; ClientContent[nTotal].sClient = skClient; //获取客户端发送数据,这是为了触发后面的等待 WSARecv(ClientContent[nTotal].sClient, &ClientContent[nTotal].DataBuf, 1, &dwNumberOfBytesRecv, &dwFlags, &ClientContent[nTotal].Overlapped, NULL); nTotal++; continue; }else { //等待发送完成 WSAGetOverlappedResult(ClientContent[nIndex - WSA_WAIT_EVENT_0].sClient, &ClientContent[nIndex - WSA_WAIT_EVENT_0].Overlapped, &dwTransfer, TRUE, &dwFlags); if (dwTransfer == 0) { printf("接受数据失败:%08x\n", WSAGetLastError()); closesocket(ClientContent[nIndex - WSA_WAIT_EVENT_0].sClient); WSACloseEvent(ClientContent[nIndex - WSA_WAIT_EVENT_0].hEvent); for (int i = nIndex - WSA_WAIT_EVENT_0; i < nTotal; i++) { ClientContent[i] = ClientContent[i]; Event[i] = Event[i]; nTotal--; } } if (strcmp("exit", ClientContent[nIndex - WSA_WAIT_EVENT_0].DataBuf.buf) == 0) { closesocket(ClientContent[nIndex - WSA_WAIT_EVENT_0].sClient); WSACloseEvent(ClientContent[nIndex - WSA_WAIT_EVENT_0].hEvent); for (int i = nIndex - WSA_WAIT_EVENT_0; i < nTotal; i++) { ClientContent[i] = ClientContent[i]; Event[i] = Event[i]; nTotal--; } continue; } send(ClientContent[nIndex - WSA_WAIT_EVENT_0].sClient, ClientContent[nIndex - WSA_WAIT_EVENT_0].DataBuf.buf, dwTransfer, 0); WSARecv(ClientContent[nIndex - WSA_WAIT_EVENT_0].sClient, &ClientContent[nIndex - WSA_WAIT_EVENT_0].DataBuf, 1, &dwNumberOfBytesRecv, &dwFlags, &ClientContent[nIndex - WSA_WAIT_EVENT_0].Overlapped, NULL); } } WSACleanup(); return 0; }上述代码中定义了一个结构,方便我们根据事件对象获取一些重要信息。在main函数中首先完成了WinSock环境的初始化然后创建监听套接字,绑定,监听。然后定义一个事件对象让他与对应的WSAOVERLAPPED绑定,然后WSAEventSelect来投递监听SOCKET以便获取到客户端的连接请求(这里没有使用AcceptEx,因为它需要特殊的加载方式)接着在循环中首先调用WSAWaitForMultipleEvents等待所有信号,当函数返回时判断当前是否为监听套接字,如果是那么调用WSAAccept函数接收连接,并准备对应的事件和WSAOVERLAPPED结构,接着调用WSARecv接收客户端传入数据如果不是监听套接字则表明客户端发送数据过来,此时调用WSAGetOverlappedResult获取重叠IO执行的结果,如果成功则判断是否为exit,如果是exit关闭当前与客户端的链接,否则调用send函数原样返回数据接着调用WSARecv再次等待客户端传送数据。完成过程模型对于重叠I/O模型来说,前面的事件通知模型在资源的消耗上有时是惊人的。这主要是因为对于每个重叠I/O操作(WSASend/WSARecv等)来说,都必须额外创建一个Event对象。对于一个I/O密集型SOCKET应用来说,这种消耗会造成资源的严重浪费。由于Event对象是一个内核对象,它在应用层表现为一个4字节的句柄值,但是在内核中它对应的是一个具体的结构,而且所有的进程共享同一块内核的内存,因此某几个进程创建大量的内核对象的话,会影响整个系统的性能。为此重叠I/O又提供了一种称之为完成过程方式的模型。该模型不需要像前面那样提供对应的事件句柄。它需要为每个I/O操作提供一个完成之后回调处理的函数。完成历程的本质是一个历程它仍然是使用当前线程的环境。它主要向系统注册一些完成函数,当对应的IO操作完成时,系统会将函数放入到线程的APC队列,当线程陷入可警告状态时,它利用线程的环境来依次执行队列中的APC函数、要使用重叠I/O完成过程模型,那么也需要为每个I/O操作提供WSAOVERLAPPED结构体,只是此时不需要Event对象了。取而代之的是提供一个完成过程的函数完成历程的原型如下:void CALLBACK CompletionROUTINE(DWORD dwError, DWORD cbTransferred,LPWSAOVERLAPPED lpOverlapped, DWORD dwFlags);要使对应的完成函数能够执行需要在恰当的时机让对应线程进入可警告状态,一般的方式是调用SleepEx函数,还有就是调用Wait家族的相关Ex函数,但是如果使用Wait函数就需要使用一个内核对象进行等待,如果使用Event对象这样就与之前的事件通知模式有相同的资源消耗大的问问题了。此时我们可以考虑使用线程的句柄来进行等待,但是等待线程句柄时必须设置一个超时值而不能直接使用INFINIT了,因为等待线程就是要等到线程结束,而如果使用INFINIT,这样Wait函数永远不会返回,线程永远不会结束,此时就造成了死锁。下面是一个使用完成过程的模型typedef struct _tag_OVERLAPPED_COMPILE { WSAOVERLAPPED overlapped; LONG lNetworks; SOCKET sClient; WSABUF pszBuf; DWORD dwTransfer; DWORD dwFlags; DWORD dwNumberOfBytesRecv; DWORD dwNumberOfBytesSend; }OVERLAPPED_COMPILE, *LPOVERLAPPED_COMPILE; void CALLBACK CompletionROUTINE(DWORD dwError, DWORD cbTransferred, LPWSAOVERLAPPED lpOverlapped, DWORD dwFlags); int _tmain(int argc, TCHAR *argv[]) { WSADATA wd = {0}; WSAStartup(MAKEWORD(2, 2), &wd); SOCKET skServer = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_IP, NULL, 0, WSA_FLAG_OVERLAPPED); SOCKADDR_IN ServerClient = {0}; ServerClient.sin_family = AF_INET; ServerClient.sin_port = htons(SERVER_PORT); ServerClient.sin_addr.s_addr = htonl(INADDR_ANY); bind(skServer, (SOCKADDR*)&ServerClient, sizeof(SOCKADDR)); listen(skServer, 0); while (TRUE) { SOCKADDR_IN AddrClient = {0}; int AddrSize = sizeof(SOCKADDR); SOCKET skClient = WSAAccept(skServer, (SOCKADDR*)&AddrClient, &AddrSize, NULL, NULL); printf("有客户端[%s:%u]连接进来....\n", inet_ntoa(AddrClient.sin_addr), ntohs(AddrClient.sin_port)); LPOVERLAPPED_COMPILE lpOc = new OVERLAPPED_COMPILE; ZeroMemory(lpOc, sizeof(OVERLAPPED_COMPILE)); lpOc->dwFlags = 0; lpOc->dwTransfer = 0; lpOc->lNetworks = FD_READ; lpOc->pszBuf.buf = new char[1024]; ZeroMemory(lpOc->pszBuf.buf, 1024); lpOc->pszBuf.len = 1024; lpOc->sClient = skClient; lpOc->dwNumberOfBytesRecv = 0; WSARecv(skClient, &(lpOc->pszBuf), 1, &(lpOc->dwNumberOfBytesRecv), &(lpOc->dwFlags), &(lpOc->overlapped), CompletionROUTINE); SleepEx(2000, TRUE); } WSACleanup(); return 0; } void CALLBACK CompletionROUTINE(DWORD dwError, DWORD cbTransferred, LPWSAOVERLAPPED lpOverlapped, DWORD dwFlags) { LPOVERLAPPED_COMPILE lpOc = (LPOVERLAPPED_COMPILE)lpOverlapped; if (0 != dwError || 0 == cbTransferred) { printf("与客户端通信发生错误,错误码为:%08x\n", WSAGetLastError()); closesocket(lpOc->sClient); delete[] lpOc->pszBuf.buf; delete lpOc; return; } if (lpOc->lNetworks == FD_READ) { if (0 == strcmp(lpOc->pszBuf.buf, "exit")) { closesocket(lpOc->sClient); delete[] lpOc->pszBuf.buf; delete lpOc; return; } send(lpOc->sClient, lpOc->pszBuf.buf, cbTransferred, 0); lpOc->dwNumberOfBytesRecv = 0; ZeroMemory(lpOc->pszBuf.buf, 1024); lpOc->dwFlags = 0; lpOc->dwTransfer = 0; lpOc->lNetworks = FD_READ; WSARecv(lpOc->sClient, &(lpOc->pszBuf), 1, &(lpOc->dwNumberOfBytesRecv), &(lpOc->dwFlags), &(lpOc->overlapped), CompletionROUTINE); } }主函数的写法与之前的例子中的写法类似。也是先初始化环境,绑定,监听等等。在循环中接收连接,当有新客户端连接进来时创建对应的客户端结构,然后调用WSARecv函数接收数据,接下来就是使用SleepEx进入可警告状态,以便让完成历程有机会执行。在完成历程中就不需要像之前那样调用WSAGetOverlappedResult了,因为调用完成历程就一定意味着重叠IO操作已经完成了。在完成历程中根据第一个参数来判断IO操作执行是否成功。如果失败则会直接断开与客户端的连接然后清理对应的结构。如果成功则直接获取获取IO操作得到的数据,如果是exit则需要关闭连接,否则原样返回并准备下一次接收数据
2018年06月29日
6 阅读
0 评论
0 点赞
2018-06-23
WinSock WSAEventSelect 模型
在前面我们说了WSAAsyncSelect 模型,它相比于select模型来说提供了这样一种机制:当发生对应的IO通知时会立即通知操作系统,并调用对应的处理函数,它解决了调用send和 recv的时机问题,但是它有一个明显的缺点,就是它必须依赖窗口。对此WinSock 提供了另一种模型 WSAEventSelect模型简介该模型主要特色在于它使用事件句柄来完成SOCKET事件的通知。与WSAAsyncSelect 模型类似,它也允许使用事件对象来完成多个socket的完成通知。该模型首先在每个socket句柄上调用WSACreateEvent来创建一个WSAEvent对象句柄(早期的WSAEvent与传统的Event句柄有一定的区别,但是从WinSock2.0 以后二者是同一个东西)。接着调用WSAEventSelect将SOCKET句柄和WSAEvent对象绑定,最终通过WSAWaitForMultiEvents来等待WSAEvent变为有信号,然后再来处理对应的socketWSAEvent有两种工作模式和工作状态工作状态有有信号和无信号两种工作模式有手工重置和人工重置,手工重置指的是每当WSAWaitForMultiEvents或者WSAWaitForSingleEvents 返回之后,WSAEvent不会自动变为无信号,需要手工调用WSAResetEvent来将WSAEvent对象设置为无信号,而自动重置表示每次等待函数返回后会自动重置为无信号;调用WSACreateEvent创建的WSAEvent对象是需要手工重置的,如果想创建自动重置的WSAEvent对象可以调用CreateEvent函数来创建(由于WinSock2.0 之后二者没有任何区别,所以只需要调用CreateEvent并将返回值强转为WSAEvent即可)WSAEventSelect函数的原型如下:int WSAEventSelect( SOCKET s, WSAEVENT hEventObject, long lNetworkEvents);其中s表示对应的SOCKET,hEventObject表示对应的WSAEvent对象,lNetworkEvents 表示我们需要处理哪些事件,它有一些对应的宏定义|网络事件| 对应的含义||:------|-----------| |FD_READ| 当前可以进行数据接收操作,此时可以调用像 recv, recvfrom, WSARecv, 或者 WSARecvFrom 这样的函数| |FD_WRITE| 此时可以发送数据,可以调用 send, sendto, WSASend, or WSASendTo| |FD_ACCEPT| 可以调用accept (Windows Sockets) 或者 WSAAccept 除非返回的错误代码是WSATRY_AGAIN. ||FD_CONNECT| 表示当前可以连接远程服务器||FD_CLOSE| 当前收到关闭的消息| 当WSAWaitForMultipleEvents返回时同时会返回一个序号,用于标识是数组中的哪个WSAEvent有信号,我们使用 index - WSA_WAIT_EVENT_0 来获取对应WSAEvent在数组中的下标,然后根据这个事件对象找到对应的SOCKET即可获得了对应的SOCKET以后,还需要获取到当前是哪个事件发生导致它变为有信号,我们可以调用WSAEnumNetworkEvents函数来获取对应发生的网络事件int WSAEnumNetworkEvents( SOCKET s, WSAEVENT hEventObject, LPWSANETWORKEVENTS lpNetworkEvents );s就是要获取其具体事件通知的SOCKET句柄hEventObject就是对应的WSAEvent句柄,可以不传入,因为SOCKET句柄已经说明了要获取那个句柄上的通知,当然如果传入了,那么这个函数会对这个WSAEvent做一次重置,置为无信号的状态,相当于WSAResetEvent调用。此时我们就不需要调用WSAResetEvent函数了最后一个参数是一个结构,结构的定义如下:typedef struct _WSANETWORKEVENTS { long lNetworkEvents; int iErrorCode[FD_MAX_EVENTS]; } WSANETWORKEVENTS, *LPWSANETWORKEVENTS;第一个数据是当前产生的网络事件。iErrorCode数组是对应每个网络事件可能发生的错误代码,对于每个事件错误代码其具体数组下标是预定义的一组FD_开头的串再加上一个_BIT结尾的宏,比如FD_READ事件对应的错误码下标是FD_READ_BIT下面的代码演示了处理接收(读取)数据的事件错误的例子代码if (NetworkEvents.lNetworkEvents & FD_READ) { if (NetworkEvents.iErrorCode[FD_READ_BIT] != 0) { printf("FD_READ failed with error %d\n", NetworkEvents.iErrorCode[FD_READ_BIT]); } }到目前为止,我们可以总结一下使用WSAEventSelect模型的步骤调用WSACreateEvent为每一个SOCKET创建一个等待对象,并与对应的SOCKET形成映射关系调用WSAEventSelect函数将SOCKET于WSAEvent对象进行绑定调用WSAWaitForMultipleEvents 函数对所有SOCKET句柄进行等待当WSAWaitForMultipleEvents 函数返回时利用返回的索引找到对应的WSAEvent对象和SOCKET对象调用WSAEnumNetworkEvents来获取对应的网络事件,根据网络事件来进行对应的收发操作重复3~5的步骤示例下面是一个简单的例子int _tmain(int argc, TCHAR *argv[]) { WSADATA wd = {0}; WSAStartup(MAKEWORD(2, 2), &wd); SOCKET skServer = socket(AF_INET, SOCK_STREAM, IPPROTO_IP); SOCKADDR_IN AddrServer = {AF_INET}; AddrServer.sin_port = htons(SERVER_PORT); AddrServer.sin_addr.s_addr = htonl(INADDR_ANY); bind(skServer, (SOCKADDR*)&AddrServer, sizeof(SOCKADDR)); listen(skServer, 5); printf("服务端正在监听...........\n"); CWSAEvent WSAEvent; WSAEvent.InsertClient(skServer, FD_ACCEPT | FD_CLOSE); WSAEvent.EventLoop(); WSACleanup(); return 0; }在代码中定义了一个类CWSAEvent,该类封装了关于该模型的相关操作和对应事件对象和SOCKET对象的操作,在主函数中首先创建监听的SOCKET,然后绑定、监听,并提交监听SOCKET到类中,以便对它进行管理,函数InsertClient的定义如下:void CWSAEvent::InsertClient(SOCKET skClient, long lNetworkEvents) { m_socketArray[m_nTotalItem] = skClient; m_EventArray[m_nTotalItem] = WSACreateEvent(); WSAEventSelect(skClient, m_EventArray[m_nTotalItem++], lNetworkEvents); }这个函数中主要向事件数组和SOCKET数组的对应位置添加了相应的成员,然后调用WSAEventSelect。而类的EventLoop函数定义了一个循环来重复前面的3~5步,函数的部分代码如下:int CWSAEvent::WaitForAllClient() { DWORD dwRet = WSAWaitForMultipleEvents(m_nTotalItem, m_EventArray, FALSE, WSA_INFINITE, FALSE); WSAResetEvent(m_EventArray[dwRet - WSA_WAIT_EVENT_0]); return dwRet - WSA_WAIT_EVENT_0; } int CWSAEvent::EventLoop() { WSANETWORKEVENTS wne = {0}; while (TRUE) { int nRet = WaitForAllClient(); WSAEnumNetworkEvents(m_socketArray[nRet], m_EventArray[nRet], &wne); if (wne.lNetworkEvents & FD_ACCEPT) { if (0 != wne.iErrorCode[FD_ACCEPT_BIT]) { OnAcceptError(nRet, m_socketArray[nRet], wne.iErrorCode[FD_ACCEPT_BIT]); }else { OnAcccept(nRet, m_socketArray[nRet]); } }else if (wne.lNetworkEvents & FD_CLOSE) { if (0 != wne.iErrorCode[FD_CLOSE_BIT]) { OnCloseError(nRet, m_socketArray[nRet], wne.iErrorCode[FD_CLOSE_BIT]); }else { OnClose(nRet, m_socketArray[nRet]); } }else if (wne.lNetworkEvents & FD_READ) { if (0 != wne.iErrorCode[FD_READ_BIT]) { OnReadError(nRet, m_socketArray[nRet], wne.iErrorCode[FD_READ_BIT]); }else { OnRead(nRet, m_socketArray[nRet]); } }else if (wne.lNetworkEvents & FD_WRITE) { if (0 != wne.iErrorCode[FD_WRITE_BIT]) { OnWriteError(nRet, m_socketArray[nRet], wne.iErrorCode[FD_WRITE_BIT]); }else { OnWrite(nRet, m_socketArray[nRet]); } } } }函数首先进行了等待,当等待函数返回时,获取对应的下标,以此来获取到socket和事件对象,然后调用WSAEnumNetworkEvents来获取对应的网络事件,最后根据事件调用不同的处理函数来处理在上面的代码中,这个循环有一个潜在的问题,我们来设想这么一个场景,当有多个客户端同时连接服务器,在第一次等待返回时,我们主要精力在进行该IO事件的处理,也就是响应这个客户端A的请求,而此时客户端A又发送了一个请求,而另外几个客户端B随后也发送了一个请求,在第一次处理完成后,等待得到的将又是客户端A,而后续客户端B的请求又被排到了后面,如果这个客户端A一直不停的发送请求,可能造成的问题是服务器一直响应A的请求,而对于B来说,它的请求迟迟得不到响应。为了避免这个问题,我们可以在函数WSAWaitForMultipleEvents 返回后,针对数组中的每个SOCKET循环调用WSAWaitForMultipleEvents将等待的数量设置为1,并将超时值设置为0,这个时候这个函数的作用就相当于查看数组中的每个SOCKET,看看是不是有待决的,当所有遍历完成后依次处理这些请求或者专门创建对应的线程来处理请求最后,整个示例代码
2018年06月23日
5 阅读
0 评论
0 点赞
2018-06-03
WSAAsyncSelect 消息模型
select 模型虽然可以管理多个socket,但是它涉及到一个时机的问题,select模型会针对所管理的数组中的每一个socket循环检测它管理是否在对应的数组中,从时间复杂度上来说它是O(n^2)的,而且还有可能发生数组中没有socket处于待决状态而导致本轮循环做无用功的情况,针对这些问题,winsock中有了新的模型——WSAAsyncSelect 消息模型消息模型的核心是基于Windows窗口消息获得网络事件的通知,Windows窗口是用来与用户交互的,而它并不知道用户什么时候会操作窗口,所以Windows窗口本身就是基于消息的异步通知,网络事件本身也是一个通知消息,将二者结合起来可以很好的使socket通知像消息那样当触发通知时调用窗口过程。这样就解决了select中的时机问题和里面两层循环的问题WSAAsyncSelect函数原型如下:int WSAAsyncSelect( __in SOCKET s, __in HWND hWnd, __in unsigned int wMsg, __in long lEvent );第一个参数是绑定的socket,第二个参数是消息所对应的窗口句柄,第三个参数是对应的消息,这个消息需要自己定义,第4个参数是我们所关心的事件,当在s这个socket发生lEvent这个事件发生时会向hWnd对应的窗口发送wMsg消息。在消息附带的两个参数wParam和lParam中,lParam的高位16位表示当前的错误码,低16位表示当前socket上发生的事件。其中事件的取值如下:FD_WRITE : 当socket上可写时触发该事件,FD_WRITE的触发与调用send没有必然的联系,FD_WRITE只是表示socket已经为发送准备好了必要的条件,其实调用时可以不必理会这个事件,只需要在想发送数据的场合调用send,一般来说FD_WRITE只在这些条件下触发:a) 调用connect函数成功连接到服务器 b) 调用accept接受连接成功后(该条件是绑定在accept返回的那个与客户端通讯的socket上) c)调用send,sendto 失败并返回WSAWOULDBLOCK(由于是异步操作,可能同时客户端也在发数据, 此时可能导致send失败)为了方便我们处理这些参数,WinSock 提供了两个宏来解析它的高16位和低16位,分别是WSAGETSELECTERROR和WSAGETSELECTEVENT而lParam则保存了当前触发事件的socket句柄如果对一个句柄调用了WSAAsyncSelect 并成功后,对应的socket会自动编程非阻塞模式。它就不像前面的select模型那样需要显示调用ioctrlsocket将socekt设置为非阻塞。另外不需要每个socket都定义一个消息ID,通常一个ID已经足够处理所有的socket事件。下面是一个具体的例子int _tmain(int argc, TCHAR *argv[]) { WSADATA wd = {0}; WSAStartup(MAKEWORD(2, 2), &wd); SOCKADDR_IN SrvAddr = {AF_INET}; SrvAddr.sin_addr.s_addr = htonl(INADDR_ANY); SrvAddr.sin_port = htons(SERVER_PORT); SOCKET skServer = socket(AF_INET, SOCK_STREAM, IPPROTO_IP); if (INVALID_SOCKET == skServer) { printf("初始化socket失败,错误码为:%08x\n", WSAGetLastError()); goto __CLEAR_UP; } if (0 != bind(skServer, (SOCKADDR*)&SrvAddr, sizeof(SOCKADDR))) { printf("绑定失败,错误码为:%08x\n", WSAGetLastError()); goto __CLEAR_UP; } if (0 != listen(skServer, 5)) { printf("监听失败,错误码为:%08x\n", WSAGetLastError()); goto __CLEAR_UP; } RegisterWindow(); CreateAndShowWnd(); g_uSockMsgID = RegisterWindowMessage(SOCKNOTIFY_MESSAGE); WSAAsyncSelect(skServer, g_hMainWnd, g_uSockMsgID, FD_ACCEPT | FD_CLOSE); MessageLoop(); __CLEAR_UP: if (INVALID_SOCKET != skServer) { closesocket(skServer); } WSACleanup(); return 0; } LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { LRESULT lRes = 0; switch (uMsg) { case WM_CLOSE: { CloseWindow(hwnd); DestroyWindow(hwnd); } break; case WM_PAINT: { PAINTSTRUCT ps = {0}; BeginPaint(hwnd, &ps); EndPaint(hwnd, &ps); } break; case WM_DESTROY: PostQuitMessage(0); break; default: if (uMsg == g_uSockMsgID) { lRes = ParseNotifyMessage(wParam, lParam); } lRes = DefWindowProc(hwnd, uMsg, wParam, lParam); } return lRes; } LRESULT ParseNotifyMessage(WPARAM wParam, LPARAM lParam) { WORD wNotify = WSAGETSELECTEVENT(lParam); WORD wError = WSAGETSELECTERROR(lParam); if (wNotify == FD_ACCEPT) { return OnAcceptMsg((SOCKET)wParam, lParam); }else if (wNotify == FD_READ) { return OnReadMsg((SOCKET)wParam, lParam); } return 1; } LRESULT OnAcceptMsg(SOCKET s, LPARAM lParam) { SOCKADDR_IN AddrClient = {0}; int nAddrSize = sizeof(SOCKADDR); SOCKET sClient = accept(s, (SOCKADDR*)&AddrClient, &nAddrSize); printf("有客户端连接进来[%s:%u]\n", inet_ntoa(AddrClient.sin_addr), ntohs(AddrClient.sin_port)); return WSAAsyncSelect(sClient, g_hMainWnd, g_uSockMsgID, FD_WRITE | FD_READ | FD_CLOSE); } LRESULT OnReadMsg(SOCKET s, LPARAM lParam) { char *pszBuf = (char*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 1024); ZeroMemory(pszBuf, 1024); int nTotalSize = 0; int i = 1; while (TRUE) { i++; int nReadSize = recv(s, pszBuf + nTotalSize, 1024, 0); if (nReadSize < 1024) { nTotalSize += nReadSize; break; } nTotalSize += nReadSize; HeapReAlloc(GetProcessHeap(), 0, pszBuf, 1024 * i); } if (strcmp(pszBuf, "exit") == 0) { shutdown(s, SD_BOTH); closesocket(s); } send(s, pszBuf, nTotalSize, 0); HeapFree(GetProcessHeap(), 0, pszBuf); return 0; } 在上面的代码中我们在main函数中创建了窗口程序,而常规的都是在WinMain中创建,其实从本质上讲控制台程序和窗口程序都是一个进程,至于以main作为入口还是以WinMain作为入口只是习惯上这样,但是并没有硬性规定。 在创建窗口之后我们将监听socket也绑定到窗口消息中,然后在对应的消息中判断FD_ACCEPT事件,如果是则调用accept进行连接。并将对生成的socket进行绑定。 在接下来的socket消息中主要处理FD_READ事件,当发生READ事件时调用read接收数据,然后调用send将数据原封不动的发送出去。 从上面的代码上看,该模型相对于select来说省去了查看socket是否在对应数组中的操作,减少了循环。而且可以很好的把握什么调用时机问题。 主要的缺点是它需要一个窗口,这样在服务程序中基本就排除掉了这个模型,它基本上只会出现在客户端程序中。 另外如果在一个窗口中需要管理成千上万个句柄时,它的性能会急剧下降,因此它的伸缩性较差。但是在客户端中基本不存在这个问题,所以如果要在客户端中想要减少编程难度,它是一个不二的选择
2018年06月03日
7 阅读
0 评论
0 点赞
2018-05-27
ATL模板库中的OLEDB与ADO
上次将OLEDB的所有内容基本上都说完了,从之前的示例上来看OLEDB中有许多变量的定义,什么结果集对象、session对象、命令对象,还有各种缓冲等等,总体上来说直接使用OLEDB写程序很麻烦,用很大的代码量带来的仅仅只是简单的功能。还要考虑各种缓冲的释放,各种对象的关闭,程序员的大量精力都浪费在无用的事情上,针对这些情况微软在OLEDB上提供了两种封装方式,一种是将其封装在ATL模板库中,一种是使用ActiveX控件来进行封装称之为ADO,这次主要写的是这两种方式ATL 模板中的OLEDB由于ATL模板是开源的,这种方式封装简洁,调试简易(毕竟源代码都给你了),各个模块相对独立,但是它的缺点很明显就是使用门槛相对较高,只有对C++中的模板十分熟悉的开发人员才能使用的得心应手。ATL中的OLEDB主要有两大模块,提供者模块和消费者模块,顾名思义,提供者模块是数据库的开发人员使用的,它主要使用这个模块实现OLEDB中的接口,对外提供相应的数据库服务;消费者模块就是使用OLEDB在程序中操作数据库。这里主要说的是消费者模块ATL主要封装的类ATL针对OLEDB封装的主要有这么几个重要的类:数据库对象CDataConnection 数据源连接类主要实现的是数据库的连接相关的功能,根据这个可以猜测出来它实际上封装的是OLEDB中的数据源对象和会话对象CDataSource:数据源对象CEnumerator: 架构结果集对象,主要用来查询数据库的相关信息,比如数据库中的表结构等信息CSession: 会话对象访问器对象:CAccessor: 常规的访问器对象CAccessorBase: 访问器对象的基类CDynamicAccessor:动态绑定的访问器CDynamicParamterAccessor:参数绑定的访问器,从之前博文的内容来看它应该是进行参数化查询等操作时使用的对象CDynamicStringAccessor:这个一般是要将查询结果显示为字符串时使用,它负责将数据库中的数据转化为字符串ALT中针对OLEDB的封装在头文件atldbcli.h中,在项目中只要包含它就行了模板的使用静态绑定针对静态绑定,VS提供了很好的向导程序帮助我们生成对应的类,方便了开发,使用的基本步骤如下:在项目上右键,选择添加类在类选择框中点击ATL并选择其中的ATL OLEDB使用者选择对应的数据源、数据库表和需要对数据库进行的操作注意如果要对数据库表进行增删改查等操作,一定要选这里的表选项点击数据源配置数据源连接的相关属性,最后点击完成。最终会在项目中生成对应的头文件这是最终生成的完整代码class Caa26Accessor { public: //value LONG m_aac031; TCHAR m_aaa146[51]; LONG m_aaa147; LONG m_aaa148; //status DBSTATUS m_dwaac031Status; DBSTATUS m_dwaaa146Status; DBSTATUS m_dwaaa147Status; DBSTATUS m_dwaaa148Status; //lenth DBLENGTH m_dwaac031Length; DBLENGTH m_dwaaa146Length; DBLENGTH m_dwaaa147Length; DBLENGTH m_dwaaa148Length; void GetRowsetProperties(CDBPropSet* pPropSet) { pPropSet->AddProperty(DBPROP_CANFETCHBACKWARDS, true, DBPROPOPTIONS_OPTIONAL); pPropSet->AddProperty(DBPROP_CANSCROLLBACKWARDS, true, DBPROPOPTIONS_OPTIONAL); pPropSet->AddProperty(DBPROP_IRowsetChange, true, DBPROPOPTIONS_OPTIONAL); pPropSet->AddProperty(DBPROP_UPDATABILITY, DBPROPVAL_UP_CHANGE | DBPROPVAL_UP_INSERT | DBPROPVAL_UP_DELETE); } HRESULT OpenDataSource() { CDataSource _db; HRESULT hr; hr = _db.OpenFromInitializationString(L"Provider=SQLOLEDB.1;Persist Security Info=False;User ID=sa; Password=XXXXXX;Initial Catalog=study;Data Source=XXXX;Use Procedure for Prepare=1;Auto Translate=True;Packet Size=4096;Workstation ID=LIU-PC;Use Encryption for Data=False;Tag with column collation when possible=False"); if (FAILED(hr)) { #ifdef _DEBUG AtlTraceErrorRecords(hr); #endif return hr; } return m_session.Open(_db); } void CloseDataSource() { m_session.Close(); } operator const CSession&() { return m_session; } CSession m_session; DEFINE_COMMAND_EX(Caa26Accessor, L" \ SELECT \ aac031, \ aaa146, \ aaa147, \ aaa148 \ FROM dbo.aa26") BEGIN_COLUMN_MAP(Caa26Accessor) COLUMN_ENTRY_LENGTH_STATUS(1, m_aac031, m_dwaac031Length, m_dwaac031Status) COLUMN_ENTRY_LENGTH_STATUS(2, m_aaa146, m_dwaaa146Length, m_dwaaa146Status) COLUMN_ENTRY_LENGTH_STATUS(3, m_aaa147, m_dwaaa147Length, m_dwaaa147Status) COLUMN_ENTRY_LENGTH_STATUS(4, m_aaa148, m_dwaaa148Length, m_dwaaa148Status) END_COLUMN_MAP() }; class Caa26 : public CCommand<CAccessor<Caa26Accessor> > { public: HRESULT OpenAll() { HRESULT hr; hr = OpenDataSource(); if (FAILED(hr)) return hr; __if_exists(GetRowsetProperties) { CDBPropSet propset(DBPROPSET_ROWSET); __if_exists(HasBookmark) { if( HasBookmark() ) propset.AddProperty(DBPROP_IRowsetLocate, true); } GetRowsetProperties(&propset); return OpenRowset(&propset); } __if_not_exists(GetRowsetProperties) { __if_exists(HasBookmark) { if( HasBookmark() ) { CDBPropSet propset(DBPROPSET_ROWSET); propset.AddProperty(DBPROP_IRowsetLocate, true); return OpenRowset(&propset); } } } return OpenRowset(); } HRESULT OpenRowset(DBPROPSET *pPropSet = NULL) { HRESULT hr = Open(m_session, NULL, pPropSet); #ifdef _DEBUG if(FAILED(hr)) AtlTraceErrorRecords(hr); #endif return hr; } void CloseAll() { Close(); ReleaseCommand(); CloseDataSource(); } };从名字上来看Caa26Accessor主要是作为一个访问器,其实它的功能也是与访问器相关的,比如创建访问器和数据绑定都在最后这个映射中。而后面的Caa26类主要是用来执行sql语句并根据上面的访问器类来解析数据,其实我们使用上主要使用后面这个类,这些代码都很简单,有之前的OLEDB基础很容易就能理解它们,这里就不再在这块进行展开了int _tmain(int argc, TCHAR *argv) { CoInitialize(NULL); Caa26 aa26; HRESULT hRes = aa26.OpenDataSource(); if (FAILED(hRes)) { aa26.CloseAll(); CoUninitialize(); return -1; } hRes = aa26.OpenRowset(); if (FAILED(hRes)) { aa26.CloseAll(); CoUninitialize(); return -1; } hRes = aa26.MoveNext(); COM_USEPRINTF(); do { COM_PRINTF(_T("|%-30u|%-30s|%-30u|%-30u|\n"), aa26.m_aac031, aa26.m_aaa146, aa26.m_aaa147, aa26.m_aaa148); hRes = aa26.MoveNext(); } while (S_OK == hRes); aa26.CloseAll(); CoUninitialize(); return 0; }动态绑定动态绑定主要是使用模板对相关的类进行拼装,所以这里需要关于模板的相关知识,只有掌握了这些才能正确的拼接出合适的类。一般需要拼接的是这样几个类结果集类,在结果集类的模板中填入对应的访问器类,表示该结果集将使用对应的访问器进行解析。访问器类可以系统预定义的,也向静态绑定那样自定义。Command类,在命令对象类的模板位置填入与命令相关的类,也就是执行命令生成的结果集、以及解析结果集所用的访问器,之后就主要使用Command类来进行数据库的相关操作了下面是一个使用的示例typedef CCommand<CDynamicAccessor, CRowset, CMultipleResults> CComRowset; typedef CTable<CDynamicAccessor, CRowset> CComTable; //将所有绑定的数据类型转化为字符串 typedef CTable<CDynamicStringAccessor, CRowset> CComTableString; int _tmain(int argc, TCHAR *argv[]) { CoInitialize(NULL); COM_USEPRINTF(); //连接数据库,创建session对象 CDataSource db; db.Open(); CSession session; session.Open(db); //打开数据库表 CComTableString table; table.Open(session, OLESTR("T_DecimalDemo")); HRESULT hRes = table.MoveFirst(); if (FAILED(hRes)) { COM_PRINTF(_T("表中没有数据,退出程序\n")); goto __CLEAN_UP; } do { //这里传入的参数是列的序号,注意一下,由于有第0列的行需要存在,所以真正的数据是从第一列开始的 COM_PRINTF(_T("|%-10s|%-20s|%-20s|%-20s|\n"), table.GetString(1), table.GetString(2), table.GetString(3), table.GetString(4)); hRes = table.MoveNext(); } while (S_OK == hRes); __CLEAN_UP: CoUninitialize(); return 0; }在上面的代码中我们定义了两个模板类,Ctable和CCommand类,没有发现需要的访问器类,查看CTable类可以发现它是继承于CAccessorRowset,而CAccessorRowset继承于TAccessor和 TRowset,也就是说它提供了访问器的相关功能而且它还可以使用OpenRowset方法不执行SQL直接打开数据表,因此在这里我们选择使用它在CTable的模板中填入CDynamicStringAccessor表示将会把得到的结果集中的数据转化为字符串。在使用上先使用CDataSource类的Open方法打开数据库连接,然后调用CTable的Open打开数据表,接着调用CTable的MoveFirst的方法将行句柄移动到首行。接着在循环中调用table的GetString方法得到各个字段的字符串值,并调用MoveNext方法移动到下一行其实在代码中并没有使用CCommand类,这是由于这里只是简单的使用直接打开数据表的方式,而并没有执行SQL语句,因此不需要它,在这里定义它只是简单的展示一下ADOATL针对OLEDB封装的确是方便了不少,但是对于像我这种将C++简单的作为带对象的C来看的人来说,它使用模板实在是太不友好了,说实话现在我现在对模板的认识实在太少,在代码中我也尽量避免使用模板。所以在我看来使用ATL还不如自己根据项目封装一套。好在微软实在太为开发者着想了,又提供了ADO这种针对ActiveX的封装方式。要使用ADO组件需要先导入,导入的语句如下:#import "C:\\Program Files\\Common Files\\System\\ado\\msado15.dll" no_namespace rename("EOF", "EndOfFile")这个路径一般是不会变化的,而EOF在C++中一般是用在文件中的,所以这里将它rename一下ADO中的主要对象和接口有:Connect :数据库的连接对象,类似于OLEDB中的数据源对象和session对象Command:命令对象,用来执行sql语句,类似于OLEDB中的Command对象Recordset: 记录集对象,执行SQL语句返回的结果,类似于OLEDB中的结果集对象Record: 数据记录对象,一般都是从Recordset中取得,就好像OLEDB中从结果集对象通过访问器获取到具体的数据一样Field:记录中的一个字段,可以简单的看做就是一个表字段的值,一般一个记录集中有多条记录,而一条记录中有个Field对象Parameter:参数对象,一般用于参数化查询或者调用存储过程Property:属性,与之前OLEDB中的属性对应-在ADO中大量使用智能指针,所谓的智能指针是它的生命周期结束后会自动析构它所指向的对象,同时也封装了一些常见指针操作,虽然它是这个对象但是它的使用上与普通的指针基本上相同。ADO中的智能指针对象一般是在类名后加上Ptr。比如Connect对象的智能指针对象是_ConnectPtr智能指针有利也有弊,有利的地方在于它能够自动管理内存,不需要程序员进行额外的释放操作,而且它在使用上就像普通的指针,相比于使用类的普通指针更为方便,不利的地方在于为了方便它的使用一般都经过了大量的重载,因此很多地方表面上看是一个普通的寻址操作,而实际上却是一个函数调用,这样就降低了性能。所以在特别强调性能的场合要避免使用智能指针。在使用上,一般经过这样几个步骤:定义数据库连接的Connect对象调用Connect对象的Open方法连接数据库,这里使用的连接字串的方式创建Command对象并调用对象Execute方法执行SQL,并获取对应的记录集。这里执行SQL语句也可以使用Recordset对象的Open方法。循环调用Recordse对象的MoveNext不断取出对应行的行记录下面是一个使用的简单例子#import "C:\\Program Files\\Common Files\\System\\ado\\msado15.dll" no_namespace rename("EOF", "EndOfFile") int _tmain(int argc, TCHAR *argv[]) { CoInitialize(NULL); _ConnectionPtr conn; _RecordsetPtr rowset; _bstr_t bstrConn = _T("Provider=SQLOLEDB.1;Persist Security Info=False;User ID=sa;Password = 123456;Initial Catalog=Study;Data Source=LIU-PC\\SQLEXPRESS;"); conn.CreateInstance(_T("ADODB.Connection")); conn->Open(bstrConn, _T("sa"), _T("123456"), adModeUnknown); if (conn->State) { COM_PRINTF(_T("连接到数据源成功\n")); }else { COM_PRINTF(_T("连接到数据源失败\n")); return 0; } rowset.CreateInstance(__uuidof(Recordset)); rowset->Open(_T("select * from aa26;"), conn.GetInterfacePtr(), adOpenStatic, adLockOptimistic, adCmdText); while (!rowset->EndOfFile) { COM_PRINTF(_T("|%-30u|%-30s|%-30u|%-30u|\n"), rowset->Fields->GetItem(_T("aac031"))->Value.intVal, rowset->Fields->GetItem(_T("aaa146"))->Value.bstrVal, rowset->Fields->GetItem(_T("aaa147"))->Value.llVal, rowset->Fields->GetItem(_T("aaa148"))->Value.llVal ); rowset->MoveNext(); } CoUninitialize(); return 0; }ADO与OLEDB混合编程ADO相比较OLEDB来说确实方便了不少,但是它也有它的问题,比如它是封装的ActiveX控件,从效率上肯定比不上OLEDB,而且ADO中记录集是一次性将结果中的所有数据加载到内存中,如果数据表比教大时这种方式很吃内存。而OLEDB是每次调用GetNextRow时加载一条记录到内存(其实根据之前的代码可以知道它加载的时机,加载的大小是可以控制的),它相对来说比教灵活。其实上述问题使用二者的混合编程就可以很好的解决,在处理结果集时使用OLEDB,而在其他操作时使用ADO这样既保留了ADO的简洁性也使用了OLEDB灵活管理结果集内存的能力。在ADO中,可以通过_Recordset查询出ADORecordsetConstruction接口,这个接口提供了将记录集转化为OLEDB中结果集,以及将结果集转化为Recordset对象的能力下面是一个简单的例子CoInitialize(NULL); try { _bstr_t bsCnct(_T("Provider=SQLOLEDB.1;Persist Security Info=False;User ID=sa;Password = 123456;Initial Catalog=Study;Data Source=LIU-PC\\SQLEXPRESS;")); _RecordsetPtr Rowset(__uuidof(Recordset)); Rowset->Open(_T("select * from aa26;") ,bsCnct,adOpenStatic,adLockOptimistic,adCmdText); //获取IRowset接口指针 ADORecordsetConstruction* padoRecordsetConstruct = NULL; Rowset->QueryInterface(__uuidof(ADORecordsetConstruction), (void **) &padoRecordsetConstruct); IRowset * pIRowset = NULL; padoRecordsetConstruct->get_Rowset((IUnknown **)&pIRowset); padoRecordsetConstruct->Release(); DisplayRowSet(pIRowset); pIRowset->Release(); GRS_PRINTF(_T("\n\n显示第二个结果集:\n")); //使用OLEDB方法打开一个结果集 IOpenRowset* pIOpenRowset = NULL; TCHAR* pszTableName = _T("T_State"); DBID TableID = {}; CreateDBSession(pIOpenRowset); TableID.eKind = DBKIND_NAME; TableID.uName.pwszName = (LPOLESTR)pszTableName; HRESULT hr = pIOpenRowset->OpenRowset(NULL,&TableID,NULL,IID_IRowset,0,NULL,(IUnknown**)&pIRowset); if(FAILED(hr)) { _com_raise_error(hr); } //创建一个新的ADO记录集对象 _RecordsetPtr Rowset2(__uuidof(Recordset)); Rowset2->QueryInterface(__uuidof(ADORecordsetConstruction), (void **) &padoRecordsetConstruct); //将OLEDB的结果集放置到ADO记录集对象中 padoRecordsetConstruct->put_Rowset(pIRowset); ULONG ulRow = 0; while(!Rowset2->EndOfFile) { COM_PRINTF(_T("|%10u|%10s|%-40s|\n") ,++ulRow ,Rowset2->Fields->GetItem("K_StateCode")->Value.bstrVal ,Rowset2->Fields->GetItem("F_StateName")->Value.bstrVal); Rowset2->MoveNext(); } } catch(_com_error & e) { COM_PRINTF(_T("发生错误:\n Source : %s \n Description : %s \n") ,(LPCTSTR)e.Source(),(LPCTSTR)e.Description()); } _tsystem(_T("PAUSE")); CoUninitialize(); return 0;这次就不再放上整体例子的链接了,有之前的基础应该很容易看懂这些,而且这次代码比较短,基本上将所有代码全粘贴了过来。
2018年05月27日
6 阅读
0 评论
0 点赞
2018-05-19
OLEDB事务
学过数据的人一般都知道事务的重要性,事务是一种对数据源的一系列更新进行分组或者批处理以便当所有更新都成功时同时提交更新,或者任意一个更新失败时进行回滚将数据库中的数据回滚到执行批处理中的所有操作之前的一种方法。使用事务保证了数据的完整性。这里不展开详细的说事务,只是谈谈OLEDB在事务上的支持ITransactionLocal接口OLEDB中支持事务的接口是ITransactionLocal接口,该接口是一个可选接口,OLEDB并不强制要求所有数据库都支持该接口,所以在使用之前需要先判断是否支持,好在现在常见的几种数据库都支持。该接口属于回话对象,因此要得到该接口只需要根据一个回话对象调用QueryInterface即可调用接口的StartTransaction方法开始一个事务该函数的原型如下HRESULT StartTransaction ( ISOLEVEL isoLevel, ULONG isoFlags, ITransactionOptions *pOtherOptions, ULONG *pulTransactionLevel); 第一个参数是事务并发的隔离级别,一般最常用的是ISOLATIONLEVEL_CURSORSTABILITY,表示只有最终提交之后才能查询对应数据库表的数据第二个参数是一个标志,目前它的值必须为0第3个参数是一个指针,它可以为空,或者是调用ITransactionLocal::GetOptionsObject函数返回的一个指针第4个参数是调用该函数创建一个事务后,该事务的并发隔离级别隔离级别是针对不同的线程或者进程的,比如有多个客户端同时在操作数据库时,如果我们设置为ISOLATIONLEVEL_CURSORSTABILITY,那么在同一事务中只有当其中一个客户端提交了事务更新后,另外一个客户端才能正常的进行查询等操作,可以简单的将这个标识视为它在数据库中上了锁,只有当它完成事务后其他客户端才可以正常使用数据库开始一个事务后正常的进行相关的数据库操作当所有步骤都正常完成后调用ITransaction::Commit方法提交事务所做的所有修改或者当其中有一步或者几步失败时调用ITransaction::Abort方法回滚所有的操作演示例子cppcpp//注意使用ISOLATIONLEVEL_CURSORSTABILITY表示最终Commint以后,才能读取这两个表的数据//注意使用ISOLATIONLEVEL_CURSORSTABILITY表示最终Commint以后,才能读取这两个表的数据hr = pITransaction->StartTransaction(ISOLATIONLEVEL_CURSORSTABILITY,0,NULL,NULL); hr = pITransaction->StartTransaction(ISOLATIONLEVEL_CURSORSTABILITY,0,NULL,NULL); //获取主表主键的最大值//获取主表主键的最大值 pRetData = pRetData = RunSqlGetValue(pIOpenRowset,_T("Select Max(PID) As PMax From T_Primary"));RunSqlGetValue(pIOpenRowset,_T("Select Max(PID) As PMax From T_Primary")); if(NULLif(NULL == pRetData)== pRetData) {{ goto CLEAR_UP;goto CLEAR_UP; }} iPID = iPID = *(int*)((BYTE*)pRetData +*(int*)((BYTE*)pRetData + sizeof(DBSTATUS)sizeof(DBSTATUS) ++ sizeof(ULONG));sizeof(ULONG)); //最大值总是加1,这样即使取得的是空值,起始值也是正常的1//最大值总是加1,这样即使取得的是空值,起始值也是正常的1 ++iPID;++iPID; TableID.eKind = DBKIND_NAME; TableID.eKind = DBKIND_NAME; TableID.uName.pwszName = TableID.uName.pwszName = (LPOLESTR)pszPrimaryTable;(LPOLESTR)pszPrimaryTable; hr = pIOpenRowset->OpenRowset(NULL,&TableID hr = pIOpenRowset->OpenRowset(NULL,&TableID ,NULL,IID_IRowsetChange,1,PropSet,(IUnknown**)&pIRowsetChange);,NULL,IID_IRowsetChange,1,PropSet,(IUnknown**)&pIRowsetChange); COM_COM_CHECK(hr,_T("打开表对象'%s'失败,错误码:0x%08X\n"),pszPrimaryTable,hr);COM_COM_CHECK(hr,_T("打开表对象'%s'失败,错误码:0x%08X\n"),pszPrimaryTable,hr); ulChangeOffset = ulChangeOffset = CreateAccessor(pIRowsetChange,pIAccessor,hChangeAccessor,pChangeBindings,ulRealCols);CreateAccessor(pIRowsetChange,pIAccessor,hChangeAccessor,pChangeBindings,ulRealCols); if(0if(0 == ulChangeOffset== ulChangeOffset |||| NULLNULL == hChangeAccessor== hChangeAccessor |||| NULLNULL == pIAccessor== pIAccessor |||| NULLNULL == pChangeBindings== pChangeBindings |||| 00 == ulRealCols)== ulRealCols) {{ goto CLEAR_UP;goto CLEAR_UP; }} //分配一个新行数据 设置数据后 插入//分配一个新行数据 设置数据后 插入 pbNewData = pbNewData = (BYTE*)COM_CALLOC(ulChangeOffset);(BYTE*)COM_CALLOC(ulChangeOffset); //设置第一个字段 K_PID//设置第一个字段 K_PID *(DBLENGTH *)((BYTE *)pbNewData + pChangeBindings[0].obLength)*(DBLENGTH *)((BYTE *)pbNewData + pChangeBindings[0].obLength) == sizeof(int);sizeof(int); *(int*)*(int*) (pbNewData + pChangeBindings[0].obValue)(pbNewData + pChangeBindings[0].obValue) = iPID;= iPID; //设置第二个字段 F_MValue//设置第二个字段 F_MValue *(DBLENGTH *)((BYTE *)pbNewData + pChangeBindings[1].obLength)*(DBLENGTH *)((BYTE *)pbNewData + pChangeBindings[1].obLength) == 8;8; StringCchCopy((WCHAR*)StringCchCopy((WCHAR*) (pbNewData + pChangeBindings[1].obValue)(pbNewData + pChangeBindings[1].obValue) ,pChangeBindings[1].cbMaxLen/sizeof(WCHAR),_T("主表数据"));,pChangeBindings[1].cbMaxLen/sizeof(WCHAR),_T("主表数据")); //插入新数据//插入新数据 hr = pIRowsetChange->InsertRow(NULL,hChangeAccessor,pbNewData,NULL); hr = pIRowsetChange->InsertRow(NULL,hChangeAccessor,pbNewData,NULL); COM_COM_CHECK(hr,_T("调用InsertRow插入新行失败,错误码:0x%08X\n"),hr);COM_COM_CHECK(hr,_T("调用InsertRow插入新行失败,错误码:0x%08X\n"),hr); hr = pIRowsetChange->QueryInterface(IID_IRowsetUpdate,(void**)&pIRowsetUpdate); hr = pIRowsetChange->QueryInterface(IID_IRowsetUpdate,(void**)&pIRowsetUpdate); COM_COM_CHECK(hr,_T("获取IRowsetUpdate接口失败,错误码:0x%08X\n"),hr);COM_COM_CHECK(hr,_T("获取IRowsetUpdate接口失败,错误码:0x%08X\n"),hr); hr = pIRowsetUpdate->Update(NULL,0,NULL,NULL,NULL,NULL); hr = pIRowsetUpdate->Update(NULL,0,NULL,NULL,NULL,NULL); COM_COM_CHECK(hr,_T("调用Update提交更新失败,错误码:0x%08X\n"),hr);COM_COM_CHECK(hr,_T("调用Update提交更新失败,错误码:0x%08X\n"),hr); COM_SAFEFREE(pChangeBindings);COM_SAFEFREE(pChangeBindings); COM_SAFEFREE(pRetData);COM_SAFEFREE(pRetData); COM_SAFEFREE(pbNewData);COM_SAFEFREE(pbNewData); if(NULLif(NULL != hChangeAccessor &&!= hChangeAccessor && NULLNULL != pIAccessor)!= pIAccessor) {{ pIAccessor->ReleaseAccessor(hChangeAccessor,NULL); pIAccessor->ReleaseAccessor(hChangeAccessor,NULL); hChangeAccessor = hChangeAccessor = NULL;NULL; }} COM_SAFERELEASE(pIAccessor);COM_SAFERELEASE(pIAccessor); COM_SAFERELEASE(pIRowsetChange);COM_SAFERELEASE(pIRowsetChange); COM_SAFERELEASE(pIRowsetUpdate);COM_SAFERELEASE(pIRowsetUpdate); //插入第二个也就是从表的数据//插入第二个也就是从表的数据 TableID.eKind = DBKIND_NAME; TableID.eKind = DBKIND_NAME; TableID.uName.pwszName = TableID.uName.pwszName = (LPOLESTR)pszMinorTable;(LPOLESTR)pszMinorTable; hr = pIOpenRowset->OpenRowset(NULL,&TableID hr = pIOpenRowset->OpenRowset(NULL,&TableID ,NULL,IID_IRowsetChange,1,PropSet,(IUnknown**)&pIRowsetChange);,NULL,IID_IRowsetChange,1,PropSet,(IUnknown**)&pIRowsetChange); COM_COM_CHECK(hr,_T("打开表对象'%s'失败,错误码:0x%08X\n"),pszMinorTable,hr);COM_COM_CHECK(hr,_T("打开表对象'%s'失败,错误码:0x%08X\n"),pszMinorTable,hr); ulChangeOffset = ulChangeOffset = CreateAccessor(pIRowsetChange,pIAccessor,hChangeAccessor,pChangeBindings,ulRealCols);CreateAccessor(pIRowsetChange,pIAccessor,hChangeAccessor,pChangeBindings,ulRealCols); if(0if(0 == ulChangeOffset== ulChangeOffset |||| NULLNULL == hChangeAccessor== hChangeAccessor |||| NULLNULL == pIAccessor== pIAccessor |||| NULLNULL == pChangeBindings== pChangeBindings |||| 00 == ulRealCols)== ulRealCols) {{ goto CLEAR_UP;goto CLEAR_UP; }} //分配一个新行数据 设置数据后 插入//分配一个新行数据 设置数据后 插入 pbNewData = pbNewData = (BYTE*)COM_CALLOC(ulChangeOffset);(BYTE*)COM_CALLOC(ulChangeOffset); //设置第一个字段 K_MID//设置第一个字段 K_MID *(DBLENGTH *)((BYTE *)pbNewData + pChangeBindings[0].obLength)*(DBLENGTH *)((BYTE *)pbNewData + pChangeBindings[0].obLength) == sizeof(int);sizeof(int); //设置第二个字段 K_PID//设置第二个字段 K_PID *(DBLENGTH *)((BYTE *)pbNewData + pChangeBindings[1].obLength)*(DBLENGTH *)((BYTE *)pbNewData + pChangeBindings[1].obLength) == sizeof(int);sizeof(int); *(int*)*(int*) (pbNewData + pChangeBindings[1].obValue)(pbNewData + pChangeBindings[1].obValue) = iPID;= iPID; //设置第二个字段//设置第二个字段 *(DBLENGTH *)((BYTE *)pbNewData + pChangeBindings[2].obLength)*(DBLENGTH *)((BYTE *)pbNewData + pChangeBindings[2].obLength) == 8;8; StringCchCopy((WCHAR*)StringCchCopy((WCHAR*) (pbNewData + pChangeBindings[2].obValue)(pbNewData + pChangeBindings[2].obValue) ,pChangeBindings[2].cbMaxLen/sizeof(WCHAR),_T("从表数据"));,pChangeBindings[2].cbMaxLen/sizeof(WCHAR),_T("从表数据")); for(int i = iMIDS; i <= iMIDMax; i++)for(int i = iMIDS; i <= iMIDMax; i++) {//循环插入新数据{//循环插入新数据 //设置第一个字段 K_MID//设置第一个字段 K_MID *(int*)*(int*) (pbNewData + pChangeBindings[0].obValue)(pbNewData + pChangeBindings[0].obValue) = i;= i; hr = pIRowsetChange->InsertRow(NULL,hChangeAccessor,pbNewData,NULL); hr = pIRowsetChange->InsertRow(NULL,hChangeAccessor,pbNewData,NULL); COM_COM_CHECK(hr,_T("调用InsertRow插入新行失败,错误码:0x%08X\n"),hr);COM_COM_CHECK(hr,_T("调用InsertRow插入新行失败,错误码:0x%08X\n"),hr); }} hr = pIRowsetChange->QueryInterface(IID_IRowsetUpdate,(void**)&pIRowsetUpdate); hr = pIRowsetChange->QueryInterface(IID_IRowsetUpdate,(void**)&pIRowsetUpdate); COM_COM_CHECK(hr,_T("获取IRowsetUpdate接口失败,错误码:0x%08X\n"),hr);COM_COM_CHECK(hr,_T("获取IRowsetUpdate接口失败,错误码:0x%08X\n"),hr); hr = pIRowsetUpdate->Update(NULL,0,NULL,NULL,NULL,NULL); hr = pIRowsetUpdate->Update(NULL,0,NULL,NULL,NULL,NULL); COM_COM_CHECK(hr,_T("调用Update提交更新失败,错误码:0x%08X\n"),hr);COM_COM_CHECK(hr,_T("调用Update提交更新失败,错误码:0x%08X\n"),hr); //所有操作都成功了,提交事务释放资源//所有操作都成功了,提交事务释放资源 hr = pITransaction->Commit(FALSE, XACTTC_SYNC, hr = pITransaction->Commit(FALSE, XACTTC_SYNC, 0);0); COM_COM_CHECK(hr,_T("事务提交失败,错误码:0x%08X\n"),hr);COM_COM_CHECK(hr,_T("事务提交失败,错误码:0x%08X\n"),hr); CLEAR_UP:CLEAR_UP://操作失败,回滚事务先,然后释放资源//操作失败,回滚事务先,然后释放资源 hr = pITransaction->Abort(NULL, FALSE, FALSE); hr = pITransaction->Abort(NULL, FALSE, FALSE);在上述代码中首先创建一个事务对象,然后在进行相关的数据库操作,这里主要是在更新和插入新数据,当所有操作成功后调用commit函数提交,当其中有错误时会跳转到CLEAR_UP标签下,调用Abort进行回滚 最后实例的完整代码: [Trancation](https://gitee.com/masimaro/codes/tcesnrul0g2yi76bam5dj19#Trancation) <!-- more -->
2018年05月19日
1 阅读
0 评论
0 点赞
2018-05-12
OLEDB 简单数据查找定位和错误处理
在数据库查询中,我们主要使用的SQL语句,但是之前也说过,SQL语句需要经历解释执行的步骤,这样就会拖慢程序的运行速度,针对一些具体的简单查询,比如根据用户ID从用户表中查询用户具体信息,像这样的简单查询OLEDB提供了专门的查询接口。使用该接口可以很大程度上提升程序性能。另外在之前的代码中,只是简单的通过HRESULT这个返回值来判断是否成功,针对错误没有具体的处理,但是OLEDB提供了自己的处理机制,这篇博文主要来介绍这两种情况下的处理方式简单数据查询和定位它的使用方法与之前的简单读取结果集类似,主要经历如下几部绑定需要在查询中做条件的几列(绑定的方式与之前的相同)分配一段内存,给定对应的条件值循环调用IRowsetFind接口的FindNextRow方法,传入对应的结果集、条件、条件值的缓冲,接收函数返回的新的结果集指针使用常规方法访问结果集FindNextRow函数的定义如下:HRESULT FindNextRow ( HCHAPTER hChapter, HACCESSOR hAccessor, //绑定查询条件的访问器,用于OLEDB组件访问用户传进来的条件 void *pFindValue, //之前的内存缓冲 DBCOMPAREOP CompareOp, // 查询条件,主要有:DBCOMPAREOPS_EQ、DBCOMPAREOPS_NE、DBCOMPAREOPS_LT等等,具体的请参看MSDN DBBKMARK cbBookmark, const BYTE *pBookmark, DBROWOFFSET lRowsOffset, DBROWCOUNT cRows, //一次返回的行数 DBCOUNTITEM *pcRowsObtained, //真实返回的行 HROW **prghRows); //返回的行访问器的句柄数组下面是一个具体的例子:HRESULT hRes = pIRowset->QueryInterface(IID_IRowsetFind, (void**)&pIRowsetFind); COM_SUCCESS(hRes, _T("查询接口IRowsetFind失败,错误码为:%08x\n"), hRes); hRes = pIRowset->QueryInterface(IID_IColumnsInfo, (void**)&pIColumnsInfo); COM_SUCCESS(hRes, _T("查询接口IColumnInfo失败,错误码为:%08x\n"), hRes); hRes = pIRowset->QueryInterface(IID_IAccessor, (void**)&pIAccessor); COM_SUCCESS(hRes, _T("查询接口IAccessor失败,错误码为:%08x\n"), hRes); hRes = pIColumnsInfo->GetColumnInfo(&cColumns, &rgColumnsInfo, &lpColumnsName); COM_SUCCESS(hRes, _T("查询列信息失败,错误码为:%08x\n"), hRes); rgQueryBinding[0].bPrecision = rgColumnsInfo[1].bPrecision; rgQueryBinding[0].bScale = rgColumnsInfo[1].bScale; rgQueryBinding[0].cbMaxLen = sizeof(ULONG); rgQueryBinding[0].dwMemOwner = DBMEMOWNER_CLIENTOWNED; rgQueryBinding[0].dwPart = DBPART_STATUS | DBPART_LENGTH | DBPART_VALUE; rgQueryBinding[0].eParamIO = DBPARAMIO_NOTPARAM; rgQueryBinding[0].iOrdinal = 4; //绑定第4列,也就是表中的所属行政区编号列 rgQueryBinding[0].obStatus = 0; rgQueryBinding[0].obLength = sizeof(DBSTATUS); rgQueryBinding[0].obValue = sizeof(DBSTATUS) + sizeof(ULONG); rgQueryBinding[0].wType = DBTYPE_I4; hRes = pIAccessor->CreateAccessor(DBACCESSOR_ROWDATA, 1, rgQueryBinding, 0, &hQueryAccessor, NULL); COM_SUCCESS(hRes, _T("创建访问器失败,错误码为:%08x\n"), hRes); pQueryBuff = COM_ALLOC(void, rgQueryBinding[0].obValue + sizeof(ULONG)); *(DBSTATUS*)((LPBYTE)pQueryBuff + rgQueryBinding[0].obValue) = DBSTATUS_S_OK; *(ULONG*)((LPBYTE)pQueryBuff + rgQueryBinding[0].obLength) = sizeof(ULONG); *(ULONG*)((LPBYTE)pQueryBuff + rgQueryBinding[0].obValue) = uId; hRes = pIRowsetFind->FindNextRow(DB_NULL_HCHAPTER, hQueryAccessor, pQueryBuff, DBCOMPAREOPS_EQ, 0, NULL, 0, 10, &cRowsObtained, &rgShowRows); COM_SUCCESS(hRes, _T("查询结果集失败,错误码为:%08x\n"), hRes); bRet = ReadRows(cColumns, rgColumnsInfo, cRowsObtained, rgShowRows, pIRowset);这段代码首先获取到对应列的列信息,然后根据这个列信息进行动态绑定,在这里我们绑定第4列,也就是之前行政区表的所属行政区编号列,接着针对这个绑定创建访问器,并分配缓冲存储对应的条件值,最后调用FindNextRow返回查询到的新的结果集,并调用对应的函数读取返回的结果集上面的代码并不复杂,从FindNextRow的第4个参数的值来看,它只能支持简单的大于小于等于等等操作,像sql语句中的模糊查询,多表查询,联合查询等等它是不能胜任的,因此说它只是一个简单查询,它在某些简单场合下可以节省性能,但是对于复杂的业务逻辑中SQL语句仍然是不二的选择错误处理在windows中定义了丰富的错误处理代码和错误处理方式,几乎每种类型的程序都有自己的一套处理方式,比如Win32 API中的GetLastError,WinSock中的WSAGetLastError, 其实在OLEDB中有它自己的处理方式。COM中可以使用GetErrorInfo函数得到一个错误的信息的接口,IErrorInfo,进一步可以根据该接口的对应函数可以得到具体的错误信息。IErrorInfo接口IErrorInfo 有时候自身包含一些出错信息,可以直接读取。IErrorInfo有时候只有一条错误信息,有时候是一个树形结构的错误信息通过调用QueryInterface函数查询错误对象的IErrorRecords接口来判定错误信息是否还有详细的子记录。如果有子记录。如果能得到IErrorRecords接口,就调用IErrorRecords::GetRecordCount获得错误信息记录个数,接着循环调用IErrorRecords::GetErrorInfo又取得子记录的IErrorInfo接口,并获取错误信息若没有子错误记录,那么直接调用IErrorInfo::GetDescription得到错误描述信息,调用IErrorInfo::GetSource得到错误来源信息以上所述IErrorInfo接口是COM定义的标准接口,IErrorRecords是OLEDB专门定义的错误信息记录接口。IErrorRecords接口其实IErrorRecords接口除了能获取子记录的IErrorInfo接口外还有一个重要的功能。调用接口的GetBasicErrorInfo方法可以得到一个指定索引错误记录的基本错误信息结构体ERRORINFO。该结构的定义如下:typedef struct tagERRORINFO { HRESULT hrError; DWORD dwMinor; CLSID clsid; IID iid; DISPID dispid; } ERRORINFO;根据这个结构可以得到指定错误的具体信息,有点类似于FormatMessage格式化错误码,得到错误码对应的错误提示信息。另外可以调用接口的GetCustomErrorObject给定一个错误码,得到一个具体的错误对象,一般在OLEDB中这个对象是ISQLErrorInfo接口这两个函数的第一个参数是一个编号,这个编号一般是第几个IErrorRecords中错误信息的编号。下面是一个具体的例子LPOLESTR lpSQL = OLESTR("select * where aa26;select * from aa26 where, aac031;"); HRESULT hRes =pIOpenRowset->QueryInterface(IID_IDBCreateCommand, (void**)&pIDBCreateCommand); COM_SUCCESS(hRes, _T("查询接口IDBCreateCommand失败,错误码为:%08x\n"), hRes); LPOLESTR lpErrorInfo = (LPOLESTR)CoTaskMemAlloc(1024 * sizeof(WCHAR)); hRes = pIDBCreateCommand->CreateCommand(NULL, IID_ICommandText, (IUnknown**)&pICommandText); COM_SUCCESS(hRes, _T("创建接口ICommandText失败,错误码为:%08x\n"), hRes); pICommandText->SetCommandText(DBGUID_DEFAULT, lpSQL); hRes = pICommandText->Execute(NULL, IID_IRowset, NULL, NULL, (IUnknown**)&pIRowset); if (FAILED(hRes)) { GetErrorInfo(0, &pIErrorInfo); HRESULT hr = pIErrorInfo->QueryInterface(IID_IErrorRecords, (void**)&pIErrorRecords); if (SUCCEEDED(hr)) { ULONG uRecordes = 0; hr = pIErrorRecords->GetRecordCount(&uRecordes); COM_SUCCESS(hr, _T("获取错误集个数失败,错误码为:%08x\n"), hr); for (int i = 0; i < uRecordes; i++) { ReadErrorRecords(lpErrorInfo, 1024 * sizeof(WCHAR), hRes, i, pIErrorRecords, lpSQL); COM_PRINTF(_T("%s"), lpErrorInfo); } }else { ReadErrorInfo(lpErrorInfo, 1024 * sizeof(WCHAR), hRes, pIErrorInfo); COM_PRINTF(_T("%s"), lpErrorInfo); } }在上述例子中,我们故意传入一个错误的SQL语句,让其出错,然后通过GetErrorInfo函数获取一个错误的IErrorInfo接口,尝试查询IErrorRecords,如果有那么在循环中遍历它的子集,并且得到每个子集的详细错误信息。否则直接调用函数ReadErrorInfo获取错误的具体信息BOOL ReadErrorRecords(LPOLESTR &lpErrorInfo, DWORD dwSize, HRESULT hErrRes, ULONG uRecordIndex, IErrorRecords *pIErrorRecords, LPWSTR lpSql) { ERRORINFO ErrorInfo = {0}; static LCID lcid = GetUserDefaultLCID(); HRESULT hRes = pIErrorRecords->GetBasicErrorInfo(uRecordIndex, &ErrorInfo); COM_CHECK_HR(hRes); hRes = pIErrorRecords->GetErrorInfo(uRecordIndex, lcid, &pIErrorInfo); COM_CHECK_HR(hRes); hRes = pIErrorInfo->GetDescription(&pstrDescription); COM_CHECK_HR(hRes); hRes = pIErrorInfo->GetSource(&pstrSource); COM_CHECK_HR(hRes); if (ReadSQLError(&bstrSQLErrorInfo, pIErrorRecords, uRecordIndex)) { StringCchPrintf(lpErrorInfo, dwSize, _T("\n(%s)\n错误信息: HRESULT=0x%08X\n描述: %s\nSQL错误信息: %s\n来源: %s"), lpSql, ErrorInfo.hrError, pstrDescription, bstrSQLErrorInfo, pstrSource); }else { StringCchPrintf(lpErrorInfo, dwSize, _T("\n(%s)\n错误信息: HRESULT=0x%08X\n描述: %s\n来源: %s"), lpSql, ErrorInfo.hrError, pstrDescription, pstrSource); } }该函数用于显示错误子集的信息,在函数中首先调用IErrorRecords接口的GetBasicErrorInfo函数传入子集的编号,获取子集的基本信息,然后再调用IErrorRecords接口的GetErrorInfo方法获取子集的IErrorInfo接口,接着调用IErrorInfo接口的相应函数获取错误的详细信息,在这个里面我们调用了另外一个自定义函数ReadSQLError,尝试获取在执行SQL语句时的错误,然后进行相关的输出。函数的部分代码如下:BOOL ReadSQLError(BSTR *pbstrSQL, IErrorRecords *pIErrorRecords, DWORD dwRecordIndex) { HRESULT hRes = pIErrorRecords->GetCustomErrorObject(dwRecordIndex, IID_ISQLErrorInfo, (IUnknown**)&pISQLErrorInfo); hRes = pISQLErrorInfo->GetSQLInfo(pbstrSQL, &lNativeErrror); }这个函数就简单的调用了IErrorRecords接口的GetCustomErrorObject方法传入子集的编号,获取到ISQLErrorInfo接口,最后调用ISQLErrorInfo接口的GetSQLInfo方法获取执行SQL语句时的错误。至于函数ReadErrorInfo,它的代码十分简单,就只是ReadErrorRecords函数中关于IErrorInfo处理的部分代码而已,在这就不在说明
2018年05月12日
6 阅读
0 评论
0 点赞
2018-05-04
OLEDB 数据变更通知
除了之前介绍的接口,OLEDB还定义了其他一些支持回调的接口,可以异步操作OLEDB对象或者得到一些重要的事件通知,从而使应用程序有机会进行一些必要的处理。其中较有用的就是结果集对象的变更通知接口。通过这个接口可以及时得到结果集被增删改数据变化的情况,并有机会进行必要的数据合法性审核。除了之前介绍的接口,OLEDB还定义了其他一些支持回调的接口,可以异步操作OLEDB对象或者得到一些重要的事件通知,从而使应用程序有机会进行一些必要的处理。其中较有用的就是结果集对象的变更通知接口。通过这个接口可以及时得到结果集被增删改数据变化的情况,并有机会进行必要的数据合法性审核。数据变更通知的接口是IRowsetNotify,数据源对象要求的异步通知事件接口是IDBAsynchNotify。标准COM的回调方式为了更好的理解OLEDB的回调,先回忆一下标准COM的回调方式。COM组件除了提供函数供应用程序主动调用这种方式外,还提供了回调这种方式,这种方式由用户实现相应的接口,然后由COM组件来调用,这样我们就可以知道COM组件的运行状态,同时能针对一些情况进行处理,比如处理内存耗尽,获取用户输入等等。要支持事件回调的COM组件必须提供IConnectionPointContainer接口,调用者调用IConnectionPointContainer接口的FindConnectPoint接口,通过回调事件的IID找到特定的事件挂载点,然后调用接口的Advise方法将挂载点与对应的回调函数关联起来(一个事件可以对应多个回调函数)这样当事件发生时就可以调用对应的回调函数。这个机制有点类似于QT中的信号和槽函数机制,QT中的事件是实现定义好的,可以直接使用而这里是需要通过事件ID找到具体事件,拥有事件后,与QT步骤类似,都是需要将事件与对应的回调函数绑定。IRowsetNotify接口对于OLEDB结果集来说,最重要的事件接口是IRowsetNotify,该接口提供三个重要的通知函数:OnFieldChange:列数据发生变更OnRowChange: 行发生变化,尤其是删除或者插入行OnRowsetChange:修改数据被提交通过这些事件函数具体实现时设置不同的返回值可以控制结果集对象对修改做出的响应,比如:返回S_OK表示接受这个修改,返回S_FALSE明确拒绝接受这个修改。这样就给了一个最终反悔的机制。这些函数有两个重要的参数:DBREASON: 发生变化的原因DBEVENTPHASE:事件被触发的阶段通过对这两个参数组合的判定,可以准确的判断出结果集中数据变化的动态追踪及情况DBREASON 参数的相关值DBREASON_ROW_ASYNCHINSERT:异步插入DBREASON_ROWSET_FETCHPOSITIONCHANGE:结果集的行指针发生变化,当调用类似 IRowset::GetNextRows or IRowset::RestartPosition时触发DBREASON_ROWSET_RELEASE:当结果集被释放的时候触发DBREASON_ROWSET_CHANGED:数据库中某些元数据发生变化时触发,这里是指描述数据库表字段的一些信息发生变化,比如表字段的大小,类型这些数据,要修改这些数据需要用户具有一定的权限,一般情况下不会触发这个原因DBREASON_COLUMN_SET:当行数据被设置时触发(这里只是已存在的行数据被设置,不包括新增行),一般调用SetData时会触发DBREASON_COLUMN_RECALCULATED:当列的值发生变更时触发,一般是调用SetDataDBREASON_ROW_ACTIVATE:当用户修改行指针导致行的状态由未激活变为激活时触发DBREASON_ROW_RELEASE:当调用ReleaseRows释放某些行句柄的时候触发DBREASON_ROW_DELETE:当行被删除时触发DBREASON_ROW_FIRSTCHANGE:当某些行的某列被设置新值后又改变了当前行指针的指向时,它会被第一时间触发,并且它的触发会早于DBREASON_COLUMN_SET,这个事件只会在使用延迟更新的时候才会产生。DBREASON_ROW_INSERT:在插入新行的时候触发DBREASON_ROW_UNDOCHANGE:当调用Undo放弃修改的时候触发DBREASON_ROW_UNDOINSERT:当调用Undo放弃插入新行的时候触发DBREASON_ROW_UNDODELETE:当调用Undo放弃删除的时候触发DBREASON_ROW_UPDATE:当调用Update进行更新的时候触发DBEVENTPHASE这个参数表示当前执行的状态,一般操作数据结果集有5个状态,分别对应这样的5个值:DBEVENTPHASE_OKTODO:准备好了去做,当应用程序需要操作结果集的时候会发送一个DBEVENTPHASE_OKTODO到监听程序(在这暂时就理解为OLEDB的数据源),监听程序收到后不会立马去执行该动作,而是会返回S_OK表示它知道了这个请求,或者返回S_FALSE拒绝这个请求DBEVENTPHASE_ABOUTTODO:当数据源针对 DBEVENTPHASE_OKTODO返回S_OK时,应用程序会给一个信号,告知数据源可以进行执行动作之前最后的准备工作,这部完成之后,数据源会异步的执行相关请求操作DBEVENTPHASE_DIDEVENT:当数据源执行完这次的请求之后会到这个状态,此时数据库表的数据已经更新DBEVENTPHASE_FAILEDTODO:当之前的某一步发生错误时会进入这个状态,此时会产生回滚,将数据还原到最开始的状态。下面是数据状态迁移图,这个图很形象的展示了在某个操作执行过程中的各种状态变化结果集对象事件通知接口的使用方法定义一个派生自IRowsetNotify接口的类,并实现其接口中的所有方法设置结果集对象属性集DBPROPSET_ROWSET中的DBPROP_IConnectionPointContainer属性为VARIANT_TRUE获得结果集对象调用IRowset::QueryInterface方法得到IConnectionPointContainer接口指针调用IConnectionPointContainer::FindConnectionPoint方法得到IRowsetNotify接口对应的IConnectionPoint接口指针实例化一个第一步中创建的类调用IConnectionPoint::Advise并传递该对象指针对结果集对象进行操作,此时如果事件条件成立,结果集对象会调用该对象的相应方法通知调用者触发了什么事件详细的内容可以参考MSDN IRowsetNotify例子最后来看使用的具体例子class CCOMRowsetNotify: public IRowsetNotify { public: CCOMRowsetNotify(void); virtual ~CCOMRowsetNotify(void); protected: virtual HRESULT FindConnectionPointContainer(IUnknown *pIUnknown, REFIID rrid, IConnectionPoint* &pIcp); public: virtual HRESULT Addvise(IUnknown *pIUnknown, REFIID rrid); virtual HRESULT UnAddvise(IUnknown *pIUnknown, REFIID rrid); public: virtual STDMETHODIMP_(ULONG) AddRef(void); virtual STDMETHODIMP_(ULONG) Release(void); virtual STDMETHODIMP QueryInterface(REFIID riid, void **ppvObject); virtual STDMETHODIMP OnFieldChange (IRowset *pRowset, HROW hRow, DBORDINAL cColumns, DBORDINAL rgColumns[], DBREASON eReason, DBEVENTPHASE ePhase,BOOL fCantDeny); virtual STDMETHODIMP OnRowChange (IRowset *pRowset, DBCOUNTITEM cRows,const HROW rghRows[], DBREASON eReason, DBEVENTPHASE ePhase, BOOL fCantDeny); virtual STDMETHODIMP OnRowsetChange (IRowset *pRowset, DBREASON eReason, DBEVENTPHASE ePhase, BOOL fCantDeny); protected: ULONG m_uRef; DWORD m_dwCookie; };使用时首先定义一个派生自IRowsetNotify的类,并实现所有的接口方法if (!OpenTable(pIOpenRowset, pIRowsetChange)) { COM_PRINTF(_T("打开表失败\n")); goto __CLEAN_UP; } RowsetNotify.Addvise(pIRowsetChange, IID_IRowsetNotify); HRESULT CCOMRowsetNotify::FindConnectionPointContainer(IUnknown *pIUnknown, REFIID rrid, IConnectionPoint* &pIcp) { IConnectionPointContainer* pICpc = NULL; HRESULT hr = pIUnknown->QueryInterface(IID_IConnectionPointContainer,(void**)&pICpc); if(FAILED(hr)) { COM_PRINTF(_T("通过IRowset接口获取IConnectionPointContainer接口失败,错误码:0x%08X\n"),hr); return hr; } hr = pICpc->FindConnectionPoint(rrid,&pIcp); if(FAILED(hr)) { COM_PRINTF(_T("获取IConnectionPoint接口失败,错误码:0x%08X\n"),hr); COM_SAFE_RELEASE(pIcp); return hr; } return hr; } HRESULT CCOMRowsetNotify::Addvise(IUnknown *pIUnknown, REFIID rrid) { IConnectionPoint *pIcp = NULL; HRESULT hRes = FindConnectionPointContainer(pIUnknown, rrid, pIcp); if (S_OK != hRes) { return hRes; } hRes = pIcp->Advise(dynamic_cast<IRowsetNotify*>(this), &m_dwCookie); COM_SAFE_RELEASE(pIcp); return hRes; } 上述代码先打开数据结果集,然后调用类对象的Addvise方法传入IID_IRowsetNotify接口指针,在方法Addvise中做的主要操作是首先使用传入的接口指针查找到接口IConnectionPointContainer,然后利用IConnectionPointContainer接口的FindConnectionPoint方法找到对应的挂载点,最后调用IConnectionPointContainer的Advise方法将对应的类对象挂载到挂载点上,这样在后面操作结果集时就会调用对应的On函数,完成对应事件的处理
2018年05月04日
6 阅读
0 评论
0 点赞
2018-04-27
OLEDB 静态绑定和数据转化接口
title: OLEDB 静态绑定和数据转化接口tags: [OLEDB, 数据库编程, VC++, 数据库, 静态绑定, 数据转化对象接口]date: 2018-04-27 20:13:54categories: windows 数据库编程keywords: OLEDB, 数据库编程, VC++, 数据库, 静态绑定, 数据类型转化OLEDB 提供了静态绑定和动态绑定两种方式,相比动态绑定来说,静态绑定在使用上更加简单,而在灵活性上不如动态绑定,动态绑定在前面已经介绍过了,本文主要介绍OLEDB中的静态,以及常用的数据类型转化接口。静态绑定之前的例子都是根据返回的COLUMNINFO结构来知晓数据表中各项的具体信息,然后进行绑定操作,这个操作由于可以动态的针对不同的数据类型绑定为不同的类型,因此称之为动态绑定。动态绑定是建立在我们对数据库中表结构一无所知,而又需要对数据库进行编程,但是一般在实际的项目中开发人员都是知道数据库的具体结构的,而且一旦数据库设计好了后续更改的可能性也不太大,因此可以采取静态绑定的方式来减少编程的复杂度。在进行静态绑定时,一般针对每个数据库表结构定义一个结构体用来描述表的各项数据,然后利用结构体的偏移来绑定到数据库中。数据关系对应表一般静态绑定需要将数据库表的各项数据与结构体中的成员一一对应,这个时候就涉及到数据库数据类型到C/C++中数据类型的转化,下表列举了常见的数据库类型到C/C++数据类型的转化关系数据库类型OLEDB 类型C/C++类型binaryDBTYPE_BYTES/DBTYPE_IUNKNOWNBYTE[length]/BLOBvarbinaryDBTYPE_BYTES/DBTYPE_IUNKNOWNBLOBbitDBTYPE_BOOLVARIANT_BOOLcharDBTYPE_STRchar[length]varcharDBTYPE_STRchar[length]nvarcharDBTYPE_WSTRwchar_t[length]ncharDBTYPE_WSTRwchar_t[length]textDBTYPE_STR/DBTYPE_IUNKNOWNBLOBimageDBTYPE_BYTES/DBTYPE_IUNKNOWNBLOBntextDBTYPE_WSTR/DBTYPE_IUNKNOWNBLOBtinyintDBTYPE_UI1BYTEsmallintDBTYPE_I2SHORTintDBTYPE_I4LONGbigintDBTYPE_I8LARGE_INTEGERrealDBTYPE_R4floatfloatDBTYPE_R8doublemoneyDBTYPE_CYLARGE_INTEGERnumericDBTYPE_NUMERICtypedef struct tagDB_NUMERIC {<br/> BYTE precision;<br/> BYTE scale;<br/> BYTE sign;<br/> BYTE val[16];} DB_NUMERIC;decimalDBTYPE_NUMERICtypedef struct tagDB_NUMERIC {<br/> BYTE precision;<br/> BYTE scale;<br/> BYTE sign;<br/> BYTE val[16];<br/>} DB_NUMERIC;sysnameDBTYPE_WSTRwchar_t[length]datetimeDBTYPE_DBTIMESTAMPtypedef struct tagDBTIMESTAMP {<br/> SHORT year;<br/> USHORT month;<br/> USHORT day;<br/> USHORT hour;<br/> USHORT minute;<br/> USHORT second;<br/> ULONG fraction;<br/>}DBTIMESTAMP;timestampDBTYPE_BYTESBYTE[length]uniqueidentifierDBTYPE_GUIDGUID实例下面是一个静态绑定的例子//静态绑定的结构 typedef struct _tag_DBSTRUCT { DBSTATUS dbCodeStatus; ULONG uCodeLength; int nCode; DBSTATUS dbNameStatus; ULONG uNameLength; WCHAR szName[NAME_LENGTH]; }DBSTRUCT, *LPDBSTRUCT; dbBindings[0].bPrecision = 0; dbBindings[0].bScale = 0; dbBindings[0].cbMaxLen = sizeof(int); dbBindings[0].dwMemOwner = DBMEMOWNER_CLIENTOWNED; dbBindings[0].dwPart = DBPART_STATUS | DBPART_LENGTH | DBPART_VALUE; dbBindings[0].iOrdinal = pdbColumnInfo[0].iOrdinal; dbBindings[0].obStatus = offsetof(DBSTRUCT, dbCodeStatus); dbBindings[0].obLength = offsetof(DBSTRUCT, uCodeLength); dbBindings[0].obValue = offsetof(DBSTRUCT, nCode); dbBindings[0].wType = DBTYPE_I4; dbBindings[1].bPrecision = 0; dbBindings[1].bScale = 0; dbBindings[1].cbMaxLen = sizeof(WCHAR) * NAME_LENGTH; dbBindings[1].dwMemOwner = DBMEMOWNER_CLIENTOWNED; dbBindings[1].dwPart = DBPART_STATUS | DBPART_LENGTH | DBPART_VALUE; dbBindings[1].iOrdinal = pdbColumnInfo[1].iOrdinal; dbBindings[1].obStatus = offsetof(DBSTRUCT, dbNameStatus); dbBindings[1].obLength = offsetof(DBSTRUCT, uNameLength); dbBindings[1].obValue = offsetof(DBSTRUCT, szName); dbBindings[1].wType = DBTYPE_WSTR; hRes = pIAccessor->CreateAccessor(DBACCESSOR_ROWDATA, 2, dbBindings, 0, &hAccessor, NULL); pdbStruct = (DBSTRUCT*)COM_ALLOC(sizeof(DBSTRUCT) * cRows); while (TRUE) { hRes = pIRowset->GetNextRows(DB_NULL_HCHAPTER, 0, cRows, &cRowsObtained, &prghRows); if (hRes != S_OK && cRowsObtained == 0) { break; } ZeroMemory(pdbStruct, sizeof(DBSTRUCT) * cRows); for (int i = 0; i < cRowsObtained; i++) { hRes = pIRowset->GetData(prghRows[i], hAccessor, &pdbStruct[i]); if (!FAILED(hRes)) { COM_PRINTF(_T("%012d\t%s\n"), pdbStruct[i].nCode, pdbStruct[i].szName); } } pIRowset->ReleaseRows(cRowsObtained, prghRows, NULL, NULL, NULL); }我们针对之前的行政区表来进行演示,在这个表中我们只查询其中的两列数据,与之前的例子相似,针对每列定义3项数据,分别是状态,长度和真实的数据,在绑定的时候就不需要计算总体需要内存的大小,行内存大小就是结构体的大小,在绑定的时候我们结构体成员在结构体中的偏移作为返回数据时各项在缓冲中的偏移。而在访问数据时就需要自己计算偏移,直接使用结构体中的成员即可。从上面的例子,我总结了静态绑定和动态绑定之间的差别:其实从本质上将动态绑定和静态绑定没有区别,都是分配一段缓冲作为行集的缓冲,然后在使用的时候进行偏移的计算静态绑定是利用我们提前知道数据库的结构,实现通过结构体来安排各项在缓冲中的偏移所占内存的大小。动态绑定中所有成员的分配和所占内存大小都是根据COLUMNINFO结构事后动态分配的,需要自己计算偏移。相比于动态绑定来说,静态绑定不需要获取数据库的各项的属性信息,不需要自己计算各项的偏移,相对比较简单,适用于事先知道数据库的表结构,使用相对固定,一旦数据库结构改变就需要改变代码动态绑定可以适用于几乎任何情形,可扩展性强,几乎不需要考虑数据库表结构变更问题。代码灵活,但是需要自己计算偏移,自己分配管理内存,相对来说对程序员的功力要求更高一些。数据类型转化数据库中数据类型繁多,而对应到具体的编程语言上有不同的展示方式,具体的语言中对同一种数据库类型有不同的数据类型对应,甚至有的可能并没有什么类型可以直接对应,这就涉及到一个从数据库数据类型到具体编程语言数据类型之间进行转换的问题,针对这一问题OLEDB提供了一个接口——IDataConvert一般情况下任何数据类型都可以转化为相应格式的字符串,而对应的字符串又可以反过来转化为数据库中相应的数据类型。当然一些特殊转换也是允许的,比如:整型数据间的转换,浮点数间的转换等。这也是使用这个数据转化接口的主要原则。数据转换接口的使用使用COM标准的方式创建IDataConver接口(调用CreateInstance函数传入CLSID_OLEDB_CONVERSIONLIBRARY创建一个IID_IDataConvert接口)接着调用该接口的DataConvert方法可以进行数据转化调用接口的CanConvert可以知道两种数据类型之间能否进行转化。调用GetConversionSize可以知道源数据类型转化为指定类型时需要的缓冲大小。实例这个例子相对比较简单,就简单的在之前打印数据库数据中加了一句转化为字符串的操作,然后打印,就不在文中展示了,具体的例子见我放到码云中的代码片段例子代码:静态绑定数据类型转化
2018年04月27日
7 阅读
0 评论
0 点赞
2018-04-20
OLEDB存取BLOB型数据
现代数据库系统除了支持一些标准的通用数据类型以外,大多数还支持一种称之为BLOB型的数据。BLOB全称为big large object bytes, 大二进制对象类型,这种类型的数据通常用于存储文档、图片、音频等文件,这些文件一般体积较大,保存这些文件可以很方便的管理和检索这类信息。在MS SQLSERVER中常见的BLOB数据类型有text、ntext(n表示unicode)、image、nvarchar、varchar、varbinary等。其中image基本可以用来保存一切二进制文件,比如word、Excel、音频、视频等等类型。针对BLOB型数据,OLEDB也提供了对它的支持使用BLOB型数据的利弊一般数据库对BLOB型数据有特殊的处理方式,比如压缩等等,在数据库中存储BLOB数据可以方便的进行检索,展示,备份等操作。但是由于BLOB型数据本身比较大,存储量太大时数据量太大容易拖慢数据库性能,所以一般的说法都是尽量不要在数据库中存储这类信息。特别是图片,音视频。针对这类文件一般的做法是将其保存在系统的某个路径钟中,而在数据库中存储对应的路径操作BLOB型数据的一般方法一般针对BLOB不能像普通数据那样操作,而需要一些特殊的操作,在OLEDB中通过设置绑定结构中的一些特殊值最终指定获取BLOB型数据的一个ISequentialStream接口指针,最终会通过这个接口来进行BLOB型数据的读写操作判断一个列是否是BLOB型数据判断某个列是否是BLOB型数据一般通过如下两个条件:pColumnInfo[i].wType == DBTYPE_IUNKNOW : 包含当列信息的DBCOLUMNSINFO 结构体对象的wType值为DBTYPE_IUNKNOW,该列的类型为DBTYPE_IUNKNOW,该条件也被称为列类型判定pColumnInfo[i].dwFlags & DBCOLUMNFLAGS_ISLONG :当列信息中的dwFlag值为DBCOLUMNFLAGS_ISLONG,也就是说该列的标识中包含DBCOLUMNFLAGS_ISLONG属性,该判定条件也被称之为列标识判定当这两个条件之一成立之时,我们就可以断定这列为BLOB型数据BLOG型数据的绑定在进行BLOB型数据的绑定也有特殊要求,主要体现在下面几点:绑定结构的cbMaxLength 需要设置为0绑定结构的wType设置为DBTYPE_IUNKNOW为结构的pObject指针分配内存,大小等于DBOBJECT结构的大小指定pObject的成员 pObject->iid = IID_ISequentialStream pObject->dwFlags = STGM_READ为行缓冲长度加上一个IStream指针的长度,此时数据源不再提供查询到的数据而提供一个接口指针,后续对BLOB数据的操作都使用该指针进行最后使用完后记得释放pObject所指向的内存空间读取BLOB数据根据前面所说的创建绑定结构,并为绑定结构赋值,最终可以从结果集中获取到一个ISequentialStream接口指针。调用接口的Read方法可以读取到BLOB列中的数据,而BLOB数据的长度存储在绑定时指定的数据长度内存偏移处,这与普通列的长度存放返回方式是一样的,一般BLOB数据都比较长,这个时候就需要分段读取。在使用ISequentialStream接口操作BLOB型数据时需要注意的一个问题是,有的数据库不支持在一个访问器中访问多个BLOB数据列。一般BLOB数据列及其的消耗资源,并且数据库鼓励我们在设计数据库表结构的时候做到一行只有一列BLOB数据,因此很多数据库并不支持在一个访问器中读取多个BLOB数据。要判断数据库是否支持在一个访问器中读取多个BLOB数据,可以获取DBPROP_MULTIPLESTORAGEOBJECTS属性,该属性属于属性集DBPROPSET_ROWSET,它是一个只读属性,如果该属性的值为TRUE表示支持,为FALSE表示不支持。下面是一个读取BLOB型数据的例子,数据库中的表结构为:id(int)、text(image)、png(image)、jpg(image)void ReadBLOB(IRowset *pIRowset) { COM_DECLARE_INTERFACE(IColumnsInfo); COM_DECLARE_INTERFACE(IAccessor); DBORDINAL cColumns = 0; DBCOLUMNINFO* rgColumnsInfo = NULL; LPOLESTR lpszColumnsName = NULL; DBBINDING* rgBindings = NULL; DBBINDING** ppBindings = NULL; //绑定结构数组 DWORD *puDataLen = NULL; //当前访问器所需内存大小 DWORD *pulColCnt = NULL; //当前访问器中包含的项 ULONG ulBindCnt = 0; //访问器的数量 ULONG uBlob = 0; //当前有多少blob数据 HACCESSOR* phAccessor = NULL; HROW* hRow = NULL; DBCOUNTITEM ulGetRows = 0; ULONG uCols = 0; PVOID pData1 = NULL; //第1个访问器中数据的缓冲 PVOID pData2 = NULL; //第2个访问器中数据的缓冲 PVOID pData3 = NULL; //第3个访问器中数据的缓冲 HRESULT hRes = pIRowset->QueryInterface(IID_IColumnsInfo, (void**)&pIColumnsInfo); COM_SUCCESS(hRes, _T("查询接口pIColumnsInfo失败,错误码为:%08x\n"), hRes); hRes = pIColumnsInfo->GetColumnInfo(&cColumns, &rgColumnsInfo, &lpszColumnsName); COM_SUCCESS(hRes, _T("获取结果集列信息失败,错误码为:%08x\n"), hRes); ppBindings = (DBBINDING**)COM_ALLOC(sizeof(DBBINDING*)); rgBindings = (DBBINDING*)COM_ALLOC(sizeof(DBBINDING) * cColumns); pulColCnt = (DWORD*)COM_ALLOC(sizeof(DWORD)); puDataLen = (DWORD*)COM_ALLOC(sizeof(DWORD)); for (int i = 0; i < cColumns; i++) { //如果当前访问器对应的绑定结构的数组的首地址为空,将当前绑定结构指针作为绑定结构数组的首地址 if (NULL == ppBindings[ulBindCnt]) { ppBindings[ulBindCnt] = &rgBindings[i]; } ++pulColCnt[ulBindCnt]; rgBindings[i].bPrecision = rgColumnsInfo[i].bPrecision; rgBindings[i].bScale = rgBindings[i].bScale; rgBindings[i].cbMaxLen = 10 * sizeof(WCHAR); rgBindings[i].dwMemOwner = DBMEMOWNER_CLIENTOWNED; rgBindings[i].dwPart = DBPART_LENGTH | DBPART_STATUS | DBPART_VALUE; rgBindings[i].eParamIO = DBPARAMIO_NOTPARAM; rgBindings[i].iOrdinal = rgColumnsInfo[i].iOrdinal; rgBindings[i].obStatus = puDataLen[ulBindCnt]; rgBindings[i].obLength = puDataLen[ulBindCnt] + sizeof(DBSTATUS); rgBindings[i].obValue = rgBindings[i].obLength + sizeof(ULONG); rgBindings[i].wType = DBTYPE_WSTR; if (rgColumnsInfo[i].wType == DBTYPE_IUNKNOWN || rgColumnsInfo[i].dwFlags & DBCOLUMNFLAGS_ISLONG) { rgBindings[i].cbMaxLen = 0; rgBindings[i].wType = DBTYPE_IUNKNOWN; rgBindings[i].pObject = (DBOBJECT*)COM_ALLOC(sizeof(DBOBJECT)); rgBindings[i].pObject->iid = IID_ISequentialStream; rgBindings[i].pObject->dwFlags = STGM_READ; uBlob++; } //记录下每个访问器所需内存的大小 puDataLen[ulBindCnt] = rgBindings[i].obValue + rgBindings[i].cbMaxLen; if (rgBindings[i].wType == DBTYPE_IUNKNOWN) { puDataLen[ulBindCnt] = rgBindings[i].obValue + sizeof(ISequentialStream*); } puDataLen[ulBindCnt] = UPGROUND(puDataLen[ulBindCnt]); //判断当前是否需要创建单独的访问器 if ((uBlob || rgBindings[i].iOrdinal == 0)) { ulBindCnt++; ppBindings = (DBBINDING**)COM_REALLOC(ppBindings, sizeof(DBBINDING*) * (ulBindCnt + 1)); puDataLen = (DWORD*)COM_REALLOC(puDataLen, sizeof(DWORD) * (ulBindCnt + 1)); pulColCnt = (DWORD*)COM_REALLOC(pulColCnt, sizeof(DWORD) * (ulBindCnt + 1)); } } //创建访问器 phAccessor = (HACCESSOR*)COM_ALLOC( (ulBindCnt + 1) * sizeof(HACCESSOR)); hRes = pIRowset->QueryInterface(IID_IAccessor, (void**)&pIAccessor); COM_SUCCESS(hRes, _T("查询IAccessor接口失败,错误码为:%08x\n"), hRes); for (int i = 0; i < ulBindCnt; i++) { hRes = pIAccessor->CreateAccessor(DBACCESSOR_ROWDATA, pulColCnt[i], ppBindings[i], 0, &phAccessor[i], NULL); COM_SUCCESS(hRes, _T("创建访问器失败,错误码为:%08x\n"), hRes); } //读取其中的一行数据 hRes = pIRowset->GetNextRows(DB_NULL_HCHAPTER, 0, 1, &ulGetRows, &hRow); COM_SUCCESS(hRes, _T("读取行数据失败,错误码为:%08x\n"), hRes); //读取第一个绑定结构中的信息 pData1 = COM_ALLOC(puDataLen[0]); hRes = pIRowset->GetData(hRow[0], phAccessor[0], pData1); for(int i = 0; i < pulColCnt[0]; i++) { if (ppBindings[0][i].wType == DBTYPE_IUNKNOWN) { DBSTATUS dbStatus = *(DBSTATUS*)((BYTE*)pData1 + ppBindings[0][i].obStatus); if (dbStatus == DBSTATUS_S_OK) { ULONG uFileLen = *(ULONG*)((BYTE*)pData1 + ppBindings[0][i].obLength); if (uFileLen > 0) { DWORD dwReaded = 0; PVOID pFileData = COM_ALLOC(uFileLen); ZeroMemory(pFileData, uFileLen); ISequentialStream *pSeqStream = *(ISequentialStream**)((BYTE*)pData1 + ppBindings[0][i].obValue); pSeqStream->Read(pFileData, uFileLen, &dwReaded); WriteFileData(_T("1.txt"), pFileData, dwReaded); } } } } //后续的部分就不再写出来了,写法与上面的代码类似 pIRowset->ReleaseRows(1, hRow, NULL, NULL, NULL); __CLEAR_UP: //后面是清理的代码由于我们事先知道数据表的结构,它有3个BLOB型数据,所以这里直接定义了3个缓冲用来接收3个BLOB型数据。为了方便检测,我们另外写了一个的函数,将读取出来的BLOB数据写入到文件中,事后以文件显示是否正确来测试这段代码首先还是与以前一样,获取数据表的结构,然后进行绑定,注意这里由于使用的是SQL Server,它不支持一个访问器中访问多个BLOB,所以这里没有判断直接绑定不同的访问器。在绑定的时候使用ulBindCnt作为当前访问器的数量,在循环里面有一个判断当(uBlob || rgBindings[i].iOrdinal == 0) && (ulBindCnt != cColumns - 1)条件成立时将访问器的数量加1,该条件表示之前已经有blob型数据(之前SQL不支持一个访问器访问多个BLOB,如果之前已经有BLOB数据了,就需要另外创建访问器)或者当前是第0行(因为第0行只允许读,所以将其作为与BLOB型数据一样处理),当这些条件成立时会新增一个访问器,而随着访问器的增加,需要改变ppBindings数组中的元素,该数组存储的是访问器对应的绑定结构开始的指针。数组puDataLen表示的是当前访问器所需内存的大小,pulColCnt表示当前访问器中共有多少列,针对这个表最终这些结构的内容大致如下图:绑定完成之后,后面就是根据数组中的内容创建对应的访问器,然后绑定、读取数据,针对BLOB数据,我们还是一样从对应缓冲的obValue偏移处得到接口指针,然后调用接口的Read方法读取,最后写入文件BLOB数据的写入:要写入BLOB型数据也需要使用ISequentialStream接口,但是它不像之前可以直接使用接口的Write方法,写入的对象必须要自己从ISequentialStream接口派生,并指定一段内存作为缓冲,以便供OLEDB组件调用写方法时作为数据缓冲。这段缓冲必须要保证分配在COM堆上,也就是要使用CoTaskMemory分配内存。这里涉及到的对象主要有IStream、ISequentialStream、IStorage、ILockBytes,同样,并不是所有数据源都支持这4类对象,具体支持哪些可以查询DBPROPSET_DATASOURCEINFO属性集中的DBPROP_STRUCTUREDSTORAGE属性来判定,目前SQL Server中支持ISequentialStream接口。虽然我们可以使用这种方式来实现读写BLOB,但是每种数据源支持的程度不同,而且有的数据源甚至不支持这种方式,为了查询对读写BLOB数据支持到何种程度,可以查询DBPROPSET_DATASOURCEINFO属性集合的DBPROP_OLEOBJECTS属性来判定通常有以下几种支持方式(DBPROP_OLEOBJECTS属性的值,按位设置):DBPROPVAL_OO_BLOB: 就是之前介绍的接口方式,使用接口的方式来读写BLOB数据DBPROPVAL_OO_DIRECTBIND: 可以直接绑定在行中,通过行访问器像普通列一样访问,也就是说它不需要获取专门的指针来操作,他可以就像操作普通数据那样,分配对应内存就可以访问,但是要注意分配内存的大小,每行中对应列中BLOB的数据长度差别可能会很明显,比如有的可能是一部长达2小时的电影文件,而有的可能是一部短视频,它们之间的差距可能会达到上G,而按照最小的来可能会发生截断,按最大的分配可能会发生多达好几个G的内存浪费DBPROPVAL_OO_IPERSIST:通过IPersistStream, IPersistStreamInit, or IPersistStorage三个接口的Persist对象访问DBPROPVAL_OO_ROWOBJECT: 支持整行作为一个对象来访问,通过结果集对象的IGetRow接口来获得行对象,但是这种模式会破坏第三范式,所以一般数据库都不支持DBPROPVAL_OO_SCOPED: 通过IScopedOperations接口来暴露行对象,通过这个接口可以暴露一个树形的结果集对象DBPROPVAL_OO_SINGLETON: 直接通过ICommand::Execute和IOpenRowset::OpenRowset来打开行对象下面是插入BLOB数据的一个实例//自定义一个 class CSeqStream : public ISequentialStream { public: // Constructors CSeqStream(); virtual ~CSeqStream(); public: virtual BOOL Seek(ULONG iPos); //将当前内存指针偏移到指定位置 virtual BOOL CompareData(void* pBuffer); //比较两段内存中的值 virtual ULONG Length() { return m_cBufSize; }; virtual operator void* const() { return m_pBuffer; }; public: STDMETHODIMP_(ULONG) AddRef(void); STDMETHODIMP_(ULONG) Release(void); STDMETHODIMP QueryInterface(REFIID riid, LPVOID *ppv); //读写内存的操作,这些是必须实现的函数 STDMETHODIMP Read( /* [out] */ void __RPC_FAR *pv, /* [in] */ ULONG cb, /* [out] */ ULONG __RPC_FAR *pcbRead); STDMETHODIMP Write( /* [in] */ const void __RPC_FAR *pv, /* [in] */ ULONG cb, /* [out]*/ ULONG __RPC_FAR *pcbWritten); private: ULONG m_cRef; // reference count void* m_pBuffer; // buffer ULONG m_cBufSize; // buffer size ULONG m_iPos; // current index position in the buffer };//插入数据第一列BLOB数据 //这里由于已经事先知道每列的数据结构,因此采用偷懒的方法,一行行的插入 pData1 = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, pdwDataLen[nCol]); for(int i = 0; i < pulColCnt[nCol]; i++) { if (DBTYPE_IUNKNOWN == ppBindings[nCol][i].wType) { *(DBSTATUS*)((BYTE*)pData1 + ppBindings[nCol][i].obStatus) = DBSTATUS_S_OK; CSeqStream *pSeqStream = new CSeqStream(); GetFileData(_T("test.txt"), dwFileLen, pFileData); pSeqStream->Write(pFileData, dwFileLen, &dwWritten); pSeqStream->Seek(0); //写这个操作将缓存的指针偏移到了最后,需要调整一下,以便OLEDB组件在插入BLOB数据时从缓存中读取 HeapFree(GetProcessHeap(), 0, pFileData); *(ULONG*)((BYTE*)pData1 + ppBindings[nCol][i].obLength) = dwFileLen; *(ISequentialStream**)((BYTE*)pData1 + ppBindings[nCol][i].obValue) = pSeqStream; //此处不用release pSeqStream,COM组件会自动释放 }else { //根据数据库定义,此处应该为ID *(ULONG*)((BYTE*)pData1 + ppBindings[nCol][i].obLength) = 10; if (DBTYPE_WSTR == ppBindings[nCol][i].wType) { StringCchCopy((LPOLESTR)((BYTE*)pData1 + ppBindings[nCol][i].obValue), 10, SysAllocString(OLESTR("1"))); } } } hRes = pIRowsetChange->InsertRow(DB_NULL_HCHAPTER, phAccessor[nCol], pData1, &hNewRow); COM_SUCCESS(hRes, _T("插入第1列BLOB数据失败,错误码为:%08x\n"), hRes);在上面的代码中首先定义一个派生类,用来进行BLOB数据的读写,然后在后面的代码中演示了如何使用它在后面的一段代码中,基本步骤和之前一样,经过连接数据源、创建回话对象,打开表,然后绑定,获取行访问器,这里由于代码基本不变,为了节约篇幅所以省略它们,只贴出最重要的部分。在插入的代码中,首先查找访问器中的各个列的属性,如果是BLOB数据就采用BLOB数据的插入办法,否则用一般数据的插入办法。插入BLOB数据时,首先创建一个派生类的对象,注意此处由于后续要交给OLEDB组件调用,所以不能用栈内存。我们先调用类的Write方法将内存写入对应的缓冲中,然后调用Seek函数将内存指针偏移到缓冲的首地址,这个指针的作用就相当于文件的文件指针,COM组件在调用对应函数将它插入数据库时会采用这个内存的指针,所以必须将其置到首地址处。让后将对象的指针放入到对应的obvalues偏移中,设置对应的数据大小为BLOB数据的大小,最后只要像普通数据类型那样调用对应的更新方法即可实现BLOB数据的插入最后贴上两个例子的详细代码地址示例1:BLOB数据的读取示例2:BLOB数据的插入
2018年04月20日
6 阅读
0 评论
0 点赞
2018-04-14
OLEDB不使用SQL语句直接打开数据表
一般来说获取数据库表的方法是采用类似select * from table_name这样的sql语句。SQL语句必然伴随着数据库的解释执行,一般来说效率比较低下,而且使用SQL语句时需要数据库支持ICommandText对象,但是在OLEDB中它是一个可选接口,也就是有的数据库可能不支持,这个时候OLEDB给我们提供了一种方法让我们能够在不使用SQL的情况下操作数据库表对象。直接打开表对象需要使用IOpenRowset接口。该接口属于Session对象。打开数据库表的一般步骤声明一个DBID结构对象为结构对象的ekind(对象种类)字段赋值DBKIND_NAME值为结构对象的uName.pwszName字段赋值为表名调用IOpenRowset接口的OpenRowset方法,将DBID结构的指针传入,并让函数返回结果集对象IOpenRowset接口属于Session,可以在使用CreateSession时让其直接打开这个接口,而且该接口是必须实现的接口,因此不用担心获取不到的情况,得到这个接口后就可以直接使用接口的OpenRowset方法。OpenRowset函数原型如下:HRESULT OpenRowset( IUnknown *pUnkOuter, DBID *pTableID, //打开表时使用该结构 DBID *pIndexID, //打开索引时使用这个参数 REFIID riid, //返回对象的GUID ULONG cPropertySets, //给对应返回对象设置的属性集的个数 DBPROPSET rgPropertySets[], //给对应对象设置的属性集 IUnknown **ppRowset); // 返回的接口从函数定义上来,这种方式还可以用来打开索引使用实例BOOL OpenTable(IOpenRowset *pIOpenRowset, IRowset* &pIRowset) { DBID dbId = {0}; dbId.eKind = DBKIND_NAME; dbId.uName.pwszName = OLESTR("aa26"); DBPROP dbRowsetProp[4] = {0}; DBPROPSET dbRowsetPropset[1] = {0}; //运行直接使用对应接口函数对数据库进行增删改操作 dbRowsetProp[0].colid = DB_NULLID; dbRowsetProp[0].dwOptions = DBPROPOPTIONS_REQUIRED; dbRowsetProp[0].dwPropertyID = DBPROP_UPDATABILITY; dbRowsetProp[0].vValue.vt = VT_I4; dbRowsetProp[0].vValue.intVal = DBPROPVAL_UP_CHANGE | DBPROPVAL_UP_DELETE | DBPROPVAL_UP_DELETE; //运行在删改的同时插入数据 dbRowsetProp[1].colid = DB_NULLID; dbRowsetProp[1].dwOptions = DBPROPOPTIONS_REQUIRED; dbRowsetProp[1].dwPropertyID = DBPROP_CANHOLDROWS; dbRowsetProp[1].vValue.vt = VT_BOOL; dbRowsetProp[1].vValue.boolVal = VARIANT_TRUE; //打开IRowsetUpdate接口,实现延迟更新 dbRowsetProp[2].colid = DB_NULLID; dbRowsetProp[2].dwOptions = DBPROPOPTIONS_REQUIRED; dbRowsetProp[2].dwPropertyID = DBPROP_IRowsetUpdate; dbRowsetProp[2].vValue.vt = VT_BOOL; dbRowsetProp[2].vValue.boolVal = VARIANT_TRUE; dbRowsetPropset[0].cProperties = 3; dbRowsetPropset[0].guidPropertySet = DBPROPSET_ROWSET; dbRowsetPropset[0].rgProperties = dbRowsetProp; HRESULT hRes = pIOpenRowset->OpenRowset(NULL, &dbId, NULL, IID_IRowset, 1, dbRowsetPropset, (IUnknown**)&pIRowset); return SUCCEEDED(hRes); }详细的代码请参考: 完整代码
2018年04月14日
8 阅读
0 评论
0 点赞
1
2
3
...
9