Java OPC client开发踩坑记

最近一个项目中需要用到OPC client,从OPC Server中获取数据。主要的编程语言使用Java实现。实际开发中遇到了各种坑,其实也和自己没有这方面的经验有关,现在写一篇文章分享下整个项目中遇到的一些问题。

准备知识

开发OPC Client之前需要一些准备知识,需要一些知识储备,否则根本搞不清楚里面的门道。现在对一些预先准备的知识点做一概述。OPC是什么就不说了。

OPC Server端的协议

OPC Server端目前常见的有以下几种协议:

  • OPC DA: Data Access协议,是最基本的OPC协议。OPC DA服务器本身不存储数据,只负责显示数据收集点的当前值。客户端可以设置一个refresh interval,定期刷新这个值。目前常见的协议版本号为2.0和3.0,两个协议不完全兼容。也就是用OPC DA 2.0协议的客户端连不上OPC DA 3.0的Server
  • OPC HDA: Historical Data Access协议。前面说过DA只显示当前状态值,不存储数据。而HDA协议是由数据库提供,提供了历史数据访问的能力。比如价格昂贵的Historian数据库,就是提供HDA协议接口访问OPC的历史数据。HDA的Java客户端目前我没找到免费的。
  • OPC UA: Unified Architecture统一架构协议。诞生于2008年,摒弃了前面老的OPC协议繁杂,互不兼容等劣势,并且不再需要COM口访问,大大简化了编程的难度。基于OPC UA的开源客户端非常多。不过由于诞生时间较晚,目前在国内工业上未大规模应用,并且这个协议本身就跟旧的DA协议不兼容,客户端没法通用。

我们的目标环境绝大多数是OPC DA 2.0的Server,极个别可能有OPC DA 3.0。当时找到的很多类库实现的都是OPC UA的。

第一坑: 基于JAVA开发的OPC Client非常少,大部分是商业的,售价不菲。现场环境又是OPC DA的Server,开源client只有两个可选,找工具和评估就花了不少时间。

OPC存储格式

OPC存储和传统的关系型数据库存储格式有很大的不同,不同于关系型数据库的表存储,OPC存储格式是树形结构,Server端的存储格式如下:

host
`-- OPC Server Name
    `-- tag1: value, type, timestamp, ...,
    `-- tag2: value, type, timestamp, ...,
    `-- tag3: ...
    ...

每个主机上可能存在多个OPC Server,每个Server下面有若干个tag,就是各个数据收集点当前的值,会定期更新。每个tag包含的内容大致有当前值,值类型,时间戳等等数据。是一种树形结构。所以客户端连接的时候需要指明服务器的ip或主机名,需要连接的OPC服务名,以及监听哪些tag的数据。

Client端存储的格式如下:

Group1
`-- tag1
`-- tag2
`-- tag3
Group2
`-- tag4
`-- tag5
...

这个就比较有意思了,Client是可以自己维护一个存储层级Group。也就是服务端存储的都是一个个tag,客户端可以自己维护一个个Group,分类存放这些tag。所以OPC的Client就和传统的关系型数据库有很大的不同。客户端除了指明上述Server端的信息之外,还需要创建一个个Group,将Server端的tag一个个放到这些Group中,然后对应的tag才能持续的获得数据。

第二坑: 这种存储格式在其他数据库十分罕见,当时这里就迷茫了好一阵子,通过了解协议的人讲解,才明白原来客户端还可以维护一套存储结构。当时没理清楚Group和tag的关系,从服务端看不到Group,客户端却要填一个Group,不知道这个Group从哪来。后来才搞清楚。

COM

Component Object Model对象组件模型,是微软定义的一套软件的二进制接口,可以实现跨编程语言的进程间通信,进而实现复用。

DCOM

Microsoft Distributed Component Object Model,坑最多的一个玩意。字面意思看起来是分布式的COM,简单理解就是可以利用网络传输数据的COM协议,客户端也可以通过互联网分布在各个角落,不再限制在同一台主机上了。

上面描述来看这玩意好像挺美好是吧?实际操作开发中才发现,这玩意简直是坑王之王,对于不熟悉的人来说充满了坑,十分折腾。配置过程可以参考一些文章

  • DCOM是windows上的服务,使用前需要启用
  • DCOM是远程连接的协议,需要配置相关的权限,以及防火墙规则放行
  • 特别注意这一点,前两项配置在网上都能找到,这一条是我在经历无数次痛之后才意识到的。DCOM远程连接和http不同,是通过本地用户认证的,需要以本地用户身份登录服务器,拿到相应的权限,才能使用DCOM。有点绕是吧?你可以类比Windows的远程桌面登录,需要拿到服务器的用户名密码才能登录并操作系统,权限受到登录用户的权限所限制。而DCOM就是用的这种方式。关于各种错误网上能找出一大堆解决方案,可能还没一个能解决你的问题的。甚至可能progID无论无何也通不了,始终报错,不得不改用CLSID这种方法,十分坑。

神坑: DCOM。从配置开始就充满了陷阱和坑。不但配置繁琐复杂,还会受到各种权限以及防火墙规则的影响。最恶心的是这玩意随时可能报各种奇葩的错误,由于缺乏足够的错误信息,很难解决,基本凭借经验解决DCOM的故障。

开发过程

收集到足够的准备知识后,就可以开工了。OPC Server是DA 2.0的,因此找到了以下两个开源类库。
JEasyOPC Client

  • 底层依赖JNI,只能跑在windows环境,不能跨平台
  • 整个类库比较古老,使用的dll是32位的,整个项目只能使用32位的JRE运行
  • 同时支持DA 2.0与3.0协议,算是亮点

Utgard

  • OpenSCADA项目底下的子项目
  • 纯Java编写,具有跨平台特性
  • 全部基于DCOM实现(划重点)
  • 目前只支持DA 2.0协议,3.0协议的支持还在开发中

这两个类库都试过,JEasyOPC底层用了JNI,调用代码量倒不是很大,使用也足够简单,坑也遇到了点,就是64位的JRE运行会报错,说dll是ia32架构的,不能运行于AMD64平台下,换了32位版本的JRE之后运行起来了,但是一直报错Unknown Error,从JNI报出来的,不明所以,实在无力解决,只能放弃。

只剩下Utgard一种选择了,也庆幸目标Server是DA 2.0的,用这个类库完全够用。这个类库全部使用DCOM协议连接OPC Server,所以对于本地连接OPC Server,理论上不需要COM口,但是这个类库全部使用DCOM协议连接,所以依旧需要配置主机名,以及登录的用户名密码。使用之前必须先配置DCOM,其中痛苦不足为外人道也,在上面准备知识部分已经写道了。

经过一番折腾,总算将项目跑起来了,最终参考的工程代码如下(项目实用Gradle构建,代码使用Utgard官方的tutorial范例):
build.gradle:

apply plugin: 'java'
apply plugin: 'application'

repositories {
    maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
    jcenter()
    maven { url 'http://neutronium.openscada.org/maven/' }
}

dependencies {
    compile 'org.openscada.utgard:org.openscada.opc.lib:1.3.0-SNAPSHOT'
    compile 'org.openscada.utgard:org.openscada.opc.dcom:1.2.0-SNAPSHOT'
    compile 'org.jinterop:j-interop:2.0.4'
    compile 'ch.qos.logback:logback-core:1.2.3'
    compile 'org.slf4j:slf4j-api:1.7.25'
}

mainClassName = 'UtgardTutorial1'

src/main/java/UtgardTutorial1.java:

import org.jinterop.dcom.common.JIException;
import org.openscada.opc.lib.common.ConnectionInformation;
import org.openscada.opc.lib.da.AccessBase;
import org.openscada.opc.lib.da.Server;
import org.openscada.opc.lib.da.SyncAccess;

import java.util.concurrent.Executors;

public class UtgardTutorial1 {

    public static void main(String[] args) throws Exception {
        // create connection information
        final ConnectionInformation ci = new ConnectionInformation();
        ci.setHost("localhost");
        ci.setUser("Administrator");
        ci.setPassword("mypassword");
        ci.setProgId("TLSvrRDK.OPCTOOLKIT.DEMO");
//        ci.setClsid("08a3cc25-5953-47c1-9f81-efe3046f2d8c"); // if ProgId is not working, try it using the Clsid instead
        final String itemId = "tag1";
        // create a new server
        final Server server = new Server(ci, Executors.newSingleThreadScheduledExecutor());

        try {
            // connect to server
            server.connect();
            // add sync access, poll every 500 ms
            final AccessBase access = new SyncAccess(server, 500);
            access.addItem(itemId, (item, state) ->
                    System.out.println("Resut: " + state.toString()));
            // start reading
            access.bind();
            // wait a little bit
            Thread.sleep(10 * 1000);
            // stop reading
            access.unbind();
        } catch (final JIException e) {
            System.out.println(String.format("%08X: %s", e.getErrorCode(), server.getErrorMessage(e.getErrorCode())));
            e.printStackTrace();
        }
    }
}

最终项目运行输出如下:

 Recieved RESPONSE
Resut: Value: [[]]], Timestamp: 星期三 七月 05 00:32:29 CST 2017, Quality: 192, ErrorCode: 00000000
七月 05, 2017 12:32:27 上午 rpc.DefaultConnection processOutgoing
信息:
 Sending REQUEST
七月 05, 2017 12:32:27 上午 rpc.DefaultConnection processIncoming
信息:
 Recieved RESPONSE
Resut: Value: [[]]], Timestamp: 星期三 七月 05 00:32:29 CST 2017, Quality: 192, ErrorCode: 00000000
七月 05, 2017 12:32:28 上午 rpc.DefaultConnection processOutgoing
信息:
 Sending REQUEST
七月 05, 2017 12:32:28 上午 rpc.DefaultConnection processIncoming
信息:
 Recieved RESPONSE
Resut: Value: [[U]], Timestamp: 星期三 七月 05 00:32:30 CST 2017, Quality: 192, ErrorCode: 00000000
七月 05, 2017 12:32:28 上午 rpc.DefaultConnection processOutgoing
信息:
 Sending REQUEST
七月 05, 2017 12:32:28 上午 rpc.DefaultConnection processIncoming
信息:
 Recieved RESPONSE
Resut: Value: [[U]], Timestamp: 星期三 七月 05 00:32:30 CST 2017, Quality: 192, ErrorCode: 00000000
七月 05, 2017 12:32:29 上午 rpc.DefaultConnection processOutgoing
信息:

总算跑起来了。

你可能感兴趣的:(Java OPC client开发踩坑记)