高性能Web Service数据库编程
启用ATL Server的Data Source Cache支持
简介:
ATL Server是性能很高的Web Application和Web Service的开发类库。到目前为止,我认为如果应用程序要和数据库交互,性能最高的办法是使用OLE DB,同时要启用ATL Server的Data Source Cache。
启用ATL Server的Data Source Cache支持
先来创建一个名为HighPerformance的工程,该工程将访问SQL Server数据库。在服务器选项中,选择“数据源缓存”。向导为我们创建了两个工程,一个是HighPerformance,一个是HighPerformanceIsapi。HighPerformance是我们的应用程序逻辑所在之处,我们所有的Web Service的可公开函数都在这里。HighPerformanceIsapi是我们的ISAP扩展服务工程,IIS将在第一次Web Service被调用期间加载它,并且一直缓存,直到很长一段时间没有人使用我们的Web Service才释放。因此我们可以利用这个特性将需要缓存的全局变量保存到HighPerformanceIsapi工程中,然后每个应用程序线程(HighPerformance工程)可以通过调用QueryService来获得缓存的全局变量,并且调用全局变量的成员函数。
HighPerformanceIsapi工程内部维护了一个线程池,每个线程对象内部都拥有自己的数据源缓存对象,请看代码:
// 逐线程数据源缓存
typedef CDataSourceCache<> ds_cache_type;
CComObjectGlobal<ds_cache_type> m_dsCache;
CComObjectGlobal用于保存在m_dsCache对象的生存期内,HighPerformanceIsapi服务器不会被卸载掉。CDataSourceCache类实现了IDataSourceCache接口。
当HighPerformance工程中的应用程序逻辑通过QueryService请求全局对象时,只要传递
__uuidof(IDataSourceCache)即可。QueryService的实现代码如下:
HRESULT STDMETHODCALLTYPE QueryService(REFGUID guidService,
REFIID riid, void** ppvObject)
{
if (InlineIsEqualGUID(guidService, __uuidof(IDataSourceCache)))
{
CIsapiWorker *pWorker = GetThreadWorker();
if (pWorker)
{
CDataSourceCache<> *pCache = NULL;
if (pWorker->GetWorkerData(_DATASOURCE_CACHE, (void **)&pCache))
{
*ppvObject = static_cast<IDataSourceCache *>(pCache);
return S_OK;
}
}
}
return baseISAPI::QueryService(guidService, riid, ppvObject);
}
实现数据库的交互
创建ATL OLEDB使用者类
我们使用向导连接SQL SERVER (服务器名为GAO、用户名和密码为sa、数据库名为测试数据库、数据表为学生表),并支持属性化,生成支持写操作的OLE DB命令类。具体向导使用这里不叙述,请参考MSDN。
我们的OLE DB类名为CDataRowset。属性db_source是代表了连接信息,后面我将通过udl文件替代之。db_command属性中的SQL语句代表了查询表的操作,我们也可以使用Update或者insert语句代替之。
注意,我们的CDataRowset类创建在HighPerformance工程中,HighPerformanceIsapi工程中应该保存我们的全局数据源对象。
提供插入记录的能力
我们需要修改db_command属性中绑定的SQL语句,以便我们插入一条语句。db_command(L"INSERT INTO [学生表] ([姓名],[年龄],[性别],[地址]) VALUES (?,?,?,?)")
将db_column属性修改为db_param属性,顺序请对应INSERT语句的四个?号。
为了使我们的Web Service有更好的性能,我在工程中设置了使用UNICODE编码。
[
db_command(L"INSERT INTO [学生表] ([姓名],[年龄],[性别],[地址]) VALUES (?,?,?,?)")
]
class CDataRowset
{
public:
[ db_param(1,DBPARAMIO_INPUT)] TCHAR m_column0[20];
[ db_param(2,DBPARAMIO_INPUT)] TCHAR m_column1[20];
[ db_param(3,DBPARAMIO_INPUT)] TCHAR m_column2[20];
[ db_param(4,DBPARAMIO_INPUT)] TCHAR m_column3[20];
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);
}
};
编写CAddRecord类,该类从CDataRowset类派生,这里只能使用继承而不是直接修改CDataRowset类,是因为db_command属性导致了CDataRowset类引入了一些隐藏类,这些隐藏类会导致一些意外行为。
CAddRecord类将提供设置成员变量和清除成员变量的方法。代码如下:
void CAddRecord::Clear(void)
{
//set member to zero
m_column0[0]=0;
m_column1[0]=0;
m_column2[0]=0;
m_column3[0]=0;
}
void CAddRecord::SetStudentInfo(BSTR Name,BSTR Age,BSTR Gender,BSTR Address)
{
wcscpy(m_column0,Name);
wcscpy(m_column1,Age);
wcscpy(m_column2,Gender);
wcscpy(m_column3,Address);
}
创建数据源连接对象
删除CDataRowset类的db_source属性,该属性帮助我们创建CDataSource对象,我们这里不需要。
定义连接字符串宏
#define CONN_STRINGW L"Provider=SQLOLEDB.1;Password=sa;Persist Security Info=True;User ID=sa;Initial Catalog=/x6d4b/x8bd5/x6570/x636e/x5e93;Data Source=GAO;Use Procedure for Prepare=1;Auto Translate=True;Packet Size=4096;Workstation ID=FREEBIRD;Use Encryption for Data=False;Tag with column collation when possible=False"
修改CHighPerformance类的代码:
HTTP_CODE InitializeHandler(AtlServerRequest *pRequestInfo, IServiceProvider *pProvider)
{
if (HTTP_SUCCESS != CSoapHandler<CHighPerformanceService>::InitializeHandler(pRequestInfo, pProvider))
return HTTP_FAIL;
if (S_OK != GetDataSource( pProvider,
CONN_STRINGW,
CONN_STRINGW,
&m_dc ))
return HTTP_FAIL;
return HTTP_SUCCESS;
}
m_dc是该类的私有成员变量,类型为CDataConnection。
[ soap_method ]
HRESULT AddNewStudent(BSTR Name,BSTR Age,BSTR Gender,BSTR Address)
{
m_rec.SetStudentInfo(Name,Age,Gender,Address);
HRESULT hr=m_rec.OpenRowset(m_dc);
return hr;
}
m_rec定义为CAddRecord m_rec;
使用UDL文件代替连接字符串
从UDL文件中获取连接字符串,需要创建MSDAINITIALIZE对象,并调用IDataInitialize接口的LoadStringFromStorage方法才行。这种动作我并不想放到InitializeHandler函数中反复做。唯一的办法是放到ISAPI扩展DLL中。
我们应该在类CHighPerformanceExtension中维护一个成员变量,当我们在应用程序DLL中通过QueryService查询某个接口(这里名叫IUdl)时,我们可以获得该变量的实现的IUdl接口,通过IUdl的方法GetConnectString获取连接字符串。这样我们就可以将该连接字符串作为参数传递给GetDataSource函数,从而取代宏。
实现IUdl接口的对象将在创建时读取c:/gao.udl文件,并将连接字符串保存,IUdl::GetConnectString方法将返回连接字符串。
这里涉及到几个知识点,如何在ATL SERVER中开发一个COM-LIKE 接口,并且将接口暴露为服务;如通过OLE DB组件读取UDL文件。源代码如下,请参考:
#pragma once
//interface IUdl
__interface __declspec(uuid("F4FA35D6-D26D-4b5a-9544-A69F66E927B8"))
IUdl:public IUnknown
{
HRESULT GetConnectString(BSTR* pConString);
};
#pragma once
//#include <stdafx.h>
#include "../CommonServiceDefinition.h"
#include <fstream>
using namespace std;
class CUdl :
public IUdl
{
public:
CUdl()
{
CComPtr<IDataInitialize> spDataInitialize;
HRESULT hr = spDataInitialize.CoCreateInstance( __uuidof(MSDAINITIALIZE));
if (SUCCEEDED(hr))
hr=spDataInitialize->LoadStringFromStorage(CComBSTR("c://gao.udl"), &m_ConnString);
}
ULONG STDMETHODCALLTYPE AddRef() {return 1;}
ULONG STDMETHODCALLTYPE Release() {return 1;}
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppvObject)
{
if( !ppvObject )
return E_POINTER;
if( IsEqualIID( riid, IID_IUnknown))
*ppvObject = static_cast<IUnknown*>(this);
else if( IsEqualIID( riid, __uuidof(IUdl)))
*ppvObject = static_cast<IUdl*>(this);
else
return E_NOINTERFACE;
AddRef();
return S_OK;
}
HRESULT GetConnectString(BSTR* pConString)
{
if( pConString==NULL)
{
return E_INVALIDARG;
}
CComBSTR bstr(m_ConnString);
*pConString=bstr.Detach();
return S_OK;
}
private:
CComHeapPtr<OLECHAR> m_ConnString;
};
类CHighPerformanceExtension中声明变量CUdl m_udl;并且改写QueryService方法
HRESULT STDMETHODCALLTYPE QueryService(REFGUID guidService,
REFIID riid, void** ppvObject)
{
if (InlineIsEqualGUID(guidService, __uuidof(IDataSourceCache)))
{
CIsapiWorker *pWorker = GetThreadWorker();
if (pWorker)
{
CDataSourceCache<> *pCache = NULL;
if (pWorker->GetWorkerData(_DATASOURCE_CACHE, (void **)&pCache))
{
*ppvObject = static_cast<IDataSourceCache *>(pCache);
return S_OK;
}
}
}
if(InlineIsEqualGUID(guidService,__uuidof(IUdl)) &&
InlineIsEqualGUID(riid,__uuidof(IUdl)) )
{
return m_udl.QueryInterface(riid,ppvObject);
}
return baseISAPI::QueryService(guidService, riid, ppvObject);
}
我们的应用程序dll中的代码如下:
// HighPerformance.h : 定义 ATL Server 请求处理程序类
//
#pragma once
#include "addrecord.h"
#include "../CommonServiceDefinition.h"
namespace HighPerformanceService
{
// webservice 的所有 struct、enum 和 typedefs 应进入命名空间
// IHighPerformanceService - Web 服务接口声明
//
[
uuid("EECB2E3E-9CA8-4E07-8DE9-81A21E99E707"),
object
]
__interface IHighPerformanceService
{
[id(1)] HRESULT AddNewStudent([in]BSTR Name,[in]BSTR Age,[in]BSTR Gender,[in]BSTR Address);
};
// HighPerformanceService - Web 服务实现
//
[
request_handler(name="Default", sdl="GenHighPerformanceWSDL"),
soap_handler(
name="HighPerformanceService",
namespace="urn:HighPerformanceService",
protocol="soap"
)
]
class CHighPerformanceService :
public IHighPerformanceService
{
public:
HTTP_CODE InitializeHandler(AtlServerRequest *pRequestInfo, IServiceProvider *pProvider)
{
if (HTTP_SUCCESS != CSoapHandler<CHighPerformanceService>::InitializeHandler(pRequestInfo, pProvider))
return HTTP_FAIL;
CComPtr<IUdl> spUdl;
HRESULT hr=pProvider->QueryService(__uuidof(IUdl),&spUdl);
if(hr!=S_OK)
return HTTP_FAIL;
CComBSTR connectstring;
hr=spUdl->GetConnectString(&connectstring);
if(hr!=S_OK)
{
return HTTP_FAIL;
}
if (S_OK != GetDataSource( pProvider,
connectstring,
connectstring,
&m_dc ))
{
return HTTP_FAIL;
}
return HTTP_SUCCESS;
}
[ soap_method ]
HRESULT AddNewStudent(BSTR Name,BSTR Age,BSTR Gender,BSTR Address)
{
m_rec.SetStudentInfo(Name,Age,Gender,Address);
HRESULT hr=m_rec.OpenRowset(m_dc);
return hr;
}
private:
CDataConnection m_dc;
CAddRecord m_rec;
// TODO: 在此添加其他 Web 服务方法
}; // 类 CHighPerformanceService
} // 命名空间 HighPerformanceService
Web Service调试
MSDN提供一个例子---SOAPDebugApp 示例:在客户端内存空间中调试 XML Web services
地址:ms-help://MS.MSDNQTR.2003FEB.2052/vcsample/html/vcsamsoapdebugappsample.htm
这种方式不同真正的WEB调用,而是通过直接调用应用程序DLL,这种方式很好,但是有局限性,由于我们的InitializeHandler要在每次调用WEB方法之前执行,并且每次都要从ISAPI扩展DLL线程池中打交道,而这种模拟调用不会导致IIS加载ISAPI扩展DLL。所以,InitializeHandler会失败,我们就没有办法对WEB 方法进行断点跟踪。解决之道有两种,一是用文件输出的方式,而是先不用扩展DLL缓存,当确信应用程序DLL已经没有问题后,再进行修改。但这两种方法都比较麻烦,不知道还有没有跟好的办法?
另外,调试的时候IIS容易出现不正常,请用iisreset命令重启。
性能评测
我编写客户端调用AddNewSutdent方法,一切成功。三个客户端同时各自写入2万笔纪录,总数6万,好极了。性能感觉非常好。