上篇谈了服务器的安全性问题,这次结合客户端和服务端来谈谈回调函数。什么是回调函数?也叫Callbak,它是一类由客户端发起的,在客户端执行的COM实例。客户端会把该实例的接口IID通过指定的函数(Advise())发给服务端,服务端登记下该Callbak的IID和相应接口代理地址,在合适的时候比如有了新数据时调用该Callback,这样客户端的Callback函数获得执行。简单地说,就是此时客户端变成了服务端,原来的服务端变成了客户端。
OPC中有几种回调函数?相信大家的回答是一种,即IOPCDataCallback。其实还有另一种不太为大家熟知,绝大多数厂商也忽略没有执行的回调函数——IOPCShutdown。在SDK中搜一下RegisterInterface,结果如下。可见IOPCShutdown是属于server类的接口,在server类中执行;而IOPCDataCallback是属于group类,在group中执行的。
今天我们专门讲一下IOPCShutdown。这个接口主要就是接收服务端传过来的DA服务器关机的信息,这样客户端可以做好相应的工作,比如要不要尝试再度连接服务器,或者干脆进行客户端的体面退出等等。
为了测试此接口,在客户端加上如下程序,
IConnectionPoint* ipCPShutdown = NULL;
hResult = ipCPCShutdown->FindConnectionPoint(IID_IOPCShutdown, &ipCPShutdown);
if (FAILED(hResult))
{
_tprintf(_T("FindConnectionPoint failed for Shutdown Callback.\r\n"));
return hResult;
}
ShutdownCallback* ipShutdownCallback = new ShutdownCallback();
DWORD dwAdviseShutdown;
hResult = ipCPShutdown->Advise(ipShutdownCallback, &dwAdviseShutdown);
if (FAILED(hResult))
{
_tprintf(_T("Advise failed for Shutdown Callback.\r\n"));
}
ipCPShutdown->Release();
ipCPCShutdown->Release();
其中的ShutdownCallback类如下,最主要的就是ShutdownRequest(LPCWSTR szReason)函数,因为它给出了服务器关机的原因(szReason)
class ShutdownCallback :public IOPCShutdown
{
public:
ShutdownCallback()
{
m_ulRefs = 1;
}
STDMETHODIMP ShutdownRequest(LPCWSTR szReason) {
_tprintf("\nShutdownRequest: %S ", szReason);
return S_OK;
}
STDMETHODIMP QueryInterface(REFIID iid, LPVOID* ppInterface)
{
if (ppInterface == NULL)
{
return E_INVALIDARG;
}
if (iid == IID_IUnknown)
{
*ppInterface = dynamic_cast<IUnknown*>(this);
AddRef();
return S_OK;
}
if (iid == IID_IOPCShutdown)
{
*ppInterface = dynamic_cast<IOPCShutdown*>(this);
AddRef();
return S_OK;
}
return E_NOINTERFACE;
}
STDMETHODIMP_(ULONG) AddRef()
{
return InterlockedIncrement((LONG*)&m_ulRefs);
}
STDMETHODIMP_(ULONG) Release()
{
ULONG ulRefs = InterlockedDecrement((LONG*)&m_ulRefs);
if (ulRefs == 0)
{
delete this;
return 0;
}
return ulRefs;
}
private:
ULONG m_ulRefs;
};
测试时,我开始使用了Matrikon的模拟服务器软件,但它不提供关机选项,只能从服务中直接停止,客户端根本接收不到任何关机通知。 好在我手边还有业界领先的一款实时数据库解决方案,用它的服务器软件试试。该服务器软件的控制界面有一个关机按钮,当我按下后在服务端关掉该实时数据库时,客户端的ShutdownRequest(LPCWSTR szReason)并没有被调用,什么都没有发生,这让我非常困惑。下面开启了我的debug过程,我先把断点设在在客户端的如下位置,
在服务端,相应地进入到SDK中的COpcConnectionPoint::Advise()中,
在这里注意到没有,不管该OnAdvise()调用成功与否,接下返回给Advise()客户端的总是S_OK。这明显就是一种错误,导致客户端Advise()永远获得S_OK,代码质量由此可见一斑!
继续往下走,进到了OnAdvise()里面,返回类型是void而不是HRESULT,
注意下面要调用OpcConnect() , 注意这里的参数(IOPCShutdown*)this, 因为该COpcConnectionPoint是在COpcDaServerWrapper的类里获得实例的,因此this是指的COpcDaServerWrapper的类实例, 希望从该类实例中获得IOPCShutdown的接口入口地址。再往下看OpcConnect(),
OPCUTILS_API HRESULT OpcConnect(
IUnknown* ipSource,
IUnknown* ipSink,
REFIID riid,
DWORD* pdwConnection)
{
HRESULT hResult = S_OK;
IConnectionPoint* pCP = NULL;
IConnectionPointContainer* ipCPC = NULL;
注意到 IUnknown* ipSink 就是上面提到的 (IOPCShutdown*)this,但是我们得到了指向IOPCShutdown接口的指针了吗?
答案是否定的,也就是说(IOPCShutdown*)this找到的是指向COpcComObject的入口指针,而不是我们要的指向IOPCShutdown的入口指针。这里多说二句,第一我们看到this确实是指向COpcDaServeWrapper的实例,第二要明白对于类的编译首先是要建立一个虚拟表,里面存放的都是该类执行的所有接口相应的指针地址,再往下看你就会明白为什么会返回COpcComObject的入口指针。由于没有获得想要的IOPCShutdown接口地址,这里的pCP->Advise()返回给hResult必然是S_FAIL,接下进入Throw()和CATCH_FINALLY()中,返回S_FAIL给上面的OnAdvise(),而OnAdvise()没有检查返回值,因为它的返回类型是void。至此,我们可以看到宝贵的S_FAIL就这样生生地给浪费了,这代码质量你觉得如何?再多说一点,因为是COM对象编程,一般不需要用try/catch,有错误用HResult来反映,然后检查HRsult返回值,这也是COM的特点。
回过头来就要探究为什么我要的IOPCShutdown的接口没有得到,显然COpcDaServerWrapper里有什么不对劲的地方。这促使我去进一步了解它的定义如下,
// CLASS: COpcDaServerWrapper
// PURPOSE: A class that implements the IOPCServer interface.
class COpcDaServerWrapper :
public COpcComObject,
public COpcCPContainer,
public IOPCCommon,
public IOPCBrowseServerAddressSpace,
public IOPCItemProperties,
public IOPCServer,
public IOPCBrowse,
public IOPCItemIO,
public COpcSynchObject
{
OPC_CLASS_NEW_DELETE()
它第一个继承的是COpcComObject类,因为它是COpcDaServeWrapper继承的第一个地址,在找不到IOPCShutdown的情况下指针是指向原来的类起始地址的。既然没有继承IOPCShutdown的接口,这就不奇怪为什么不能获得指向该接口的指针了。这么明显的大BUG,就一直在那呆着,最新的SDK也一样,让人无语了,这品质,看来IOPCShutdown真没有人使用。
下面就简单了,加上IOPCShutdown后再走一遍程序,
class COpcDaServerWrapper :
public COpcComObject,
public COpcCPContainer,
public IOPCCommon,
public IOPCBrowseServerAddressSpace,
public IOPCItemProperties,
public IOPCServer,
public IOPCBrowse,
public IOPCItemIO,
public COpcSynchObject,
public IOPCShutdown
{
OPC_CLASS_NEW_DELETE()
OPC_BEGIN_INTERFACE_TABLE(COpcDaServerWrapper)
OPC_INTERFACE_ENTRY(IOPCCommon)
OPC_INTERFACE_ENTRY(IConnectionPointContainer)
OPC_INTERFACE_ENTRY(IOPCServer)
OPC_INTERFACE_ENTRY(IOPCBrowseServerAddressSpace)
OPC_INTERFACE_ENTRY(IOPCItemProperties)
OPC_INTERFACE_ENTRY(IOPCBrowse)
OPC_INTERFACE_ENTRY(IOPCItemIO)
OPC_INTERFACE_ENTRY(IOPCShutdown)
OPC_END_INTERFACE_TABLE()
这次如愿以偿,得到我想要的IOPCShutdown接口的指针。进一步的测试显示,当实时数据库关机时,客户端的ShutdownRequest(LPCWSTR szReason)得到了调用,里面的szReason显示出了和服务端软件显示的相同关机原因。
这里我给大家描述了我在客户端执行IOPCShutdown时发现的SDK的bugs,当然还有其它的SDK bug。下篇再给大家谈谈在客户端清除服务端资源时发现的另外一个bug,大家要关注哦!?