从回调函数谈OPC SDK的代码质量

上篇谈了服务器的安全性问题,这次结合客户端和服务端来谈谈回调函数。什么是回调函数?也叫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,大家要关注哦!?

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在Python中,可以使用opcua库来实现对变量变化时的回调函数。具体实现步骤如下: 1. 导入opcua库。 2. 创建一个客户端实例,并连接到OPC UA服务器。 3. 获取要订阅的变量节点对象。 4. 创建一个名为SubHandler的自定义类,作为订阅事件的处理程序。其中,datachange_notification方法是在节点的值发生变化时被触发的回调函数。在这个函数中,可以实现变量值变化时要执行的操作。 5. 创建一个订阅,订阅周期为0毫秒,并将handler传递给subscribe方法。 6. 使用subscribe.subscribe_data_change方法将变量节点与handler订阅关联起来,实现对变量值变化的订阅。 下面是一个示例代码,用于实现对变量值变化时的回调函数: ```python from opcua import Client # 创建一个客户端实例,并连接到OPC UA服务器 client = Client("opc.tcp://localhost:4840/freeopcua/server/") client.connect() # 获取要订阅的变量节点对象 var_node = client.get_node("ns=2;i=2") # 创建一个名为SubHandler的自定义类,作为订阅事件的处理程序 class SubHandler(object): def datachange_notification(self, node, val, data): # 在变量值变化时执行的操作 print("Variable value changed: ", val) # 创建一个订阅,订阅周期为0毫秒,并将handler传递给subscribe方法 handler = SubHandler() subscription = client.create_subscription(0, handler) # 使用subscribe.subscribe_data_change方法将变量节点与handler订阅关联起来,实现对变量值变化的订阅 sub = subscription.subscribe_data_change(var_node) ``` 在上述代码中,我们创建了一个SubHandler类作为订阅事件的处理程序,其中的datachange_notification方法在变量值变化时被触发。在这个方法中,我们通过print函数打印出变量值,可以根据实际需要自定义实现变量值变化时要执行的操作。最后,我们将变量节点与handler订阅关联起来,实现对变量值变化的订阅。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值