美文网首页DTeam团队日志
Java OPC client开发踩坑记

Java OPC client开发踩坑记

作者: 冯宇Ops | 来源:发表于2017-07-05 01:59 被阅读637次

    最近一个项目中需要用到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
    信息:
    

    总算跑起来了。

    相关文章

      网友评论

      • 36029f3a88a5:老师你好:org.jinterop.dcom.common.JIException: Not enough storage is available to complete this operation. [0x8007000E],存储空间不足这个如何解决?
        冯宇Ops:https://support.microsoft.com/en-us/help/890425/you-receive-a-not-enough-storage-is-available-to-complete-this-operati
      • 明明找灵气:可以试试 python的 OpenOPC ,基于DA的,它做了个插件在服务器端,这样无论你的客户端在什么系统下 都可以访问(通过那个插件访问的)而且。。很快。
        DA什么的 这些概念 其实我根本不了解 也不关心:cold_sweat:
      • 58c88d7d98a2:WinCC 的OPC 这个 ci.setDomain()和 ci.setClsid()不知道 怎么找,一直没调试通。老师有什么高见。
        58c88d7d98a2:老师,还是没搞定。恳切你加下QQ,远程协助下。
        冯宇Ops:找个client连一下就能看到了。比如MatrikonOPC Explorer就能看到这些属性。class id和program id二选一,program id直接从服务端就能看出来,class id通常需要查注册表,当然你也能从DCOM配置中看到注册的class id。domain那个是域环境才需要的配置,如果你的机器没有处于域环境下,是不需要配置domain的。
      • 58c88d7d98a2:老师:你遇到这个问题没,我的程序是用Eclipse 调试,而且程序是部署在 OPC Server本机的,应该不用配置DCOM吧!!! They provide information on how to correctly configure the Windows machine for DCOM access, so as to avoid such exceptions. [0x00000005]
        冯宇Ops:@boy51job utgard支持的是opc da 2.0协议,只要你的服务端用的是这个协议,理论上应该都能支持
        58c88d7d98a2:老师,我想问下,winCC opc server 可以用你的这个案例来实现吗?
        冯宇Ops:utgard是基于DCOM实现的,即使你是在本地部署,也需要配置DCOM连接参数。JEasyOPC那个可以用OPC本地COM连接,不需要DCOM认证。
      • 58c88d7d98a2:(item, state) -在程序里面报名是什么原因。我用的是jdk1.7
        冯宇Ops:这个是lambda表达式,是java8的新特性,你用java7就不支持。这里本来接收的参数是一个匿名函数,可以参考utgard官方范例: https://openscada.atlassian.net/wiki/spaces/OP/pages/6094892/HowToStartWithUtgard
      • 朱建小心肝:very helpful

      本文标题:Java OPC client开发踩坑记

      本文链接:https://www.haomeiwen.com/subject/oaithxtx.html