JAVA接入OPC DA2.0详细流程

超短连接转换j1z.cc(永久有效)

注意:JAVA接入OPC DA2.0引发的问题及解决方案

之前总是听说OPC协议,一直没有接触,直到最近项目需要对接OPC DA2.0,才开始了解这个协议,并且才知道这是一个有历史、有深度的坑啊!网络上零零散散有很多的资料,但是没有跑通整个流程的文章,坑更是出奇的多,这次把其中碰到的坑以及跑通整个过程的详细流程记录下来。希望能帮助更多初次接触这个协议的勇者!

准备知识

OPC协议

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

COM及DCOM

COM

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

DCOM

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

上面描述来看这玩意好像挺美好是吧?实际操作开发中才发现,这玩意简直是坑王之王,对于不熟悉的人来说充满了坑,十分折腾。

  • DCOM是windows上的服务,使用前需要启用
  • DCOM是远程连接的协议,需要以本地用户身份登录服务器,需要配置相关的权限,以及防火墙规则放行
  • 在Windows操作系统中,135端口主要用于使用PRC协议并提供DCOM(分布式组件对象模型)服务,通过RPC可以保证在一台计算机上运行的程序可以顺利地执行远程计算机上的代码。使用DCOM可以通过网络直接进行通信,能够跨包括HTTP协议在内的多种网络传输。多年来,135端口一直被人利用,属于高危端口

云平台资源,涉及到防火墙策略:

市场上的网络防火墙一般有3种模式:

  • NAT(网络地址转换)模式
  • Route(路由)模式
  • Transparent(透明)模式
OPC在这3种模式下如何配置?

如果是遇到第1种模式NAT(网络地址转换)模式,建议你选择放弃,采用OPC隧道组件类产品(如Matrikon的OPC Tunneller,Kepware的LinkMaster等)来穿透防火墙吧。

如果遇到是第2种和第三种模式,那么明确的告诉你,是完全可以通讯,与同一子网的通讯来说,你只需要在防火墙中将这两台计算机的相互访问放开即可,一般的做法是,先在防火墙中让相互通讯的两台计算机完全无限制,然后子啊根据通讯所需的协议和端口定义规则,只放行通讯所需的协议和端口,其他禁止。OPC需要TCP协议的135端口和OPC Server监听的端口,默认情况下,OPC Server的端口是动态的,因此通过Dcomcnfg去固定OPC Server的端口号,在配置面板的终结点中选择使用静态终结点并定义端口号。

JAVA库说明

OPC DA 2.0可以使用两个开源类库:

JEasyOPC Client

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

Utgard

OpenSCADA项目底下的子项目
纯Java编写,具有跨平台特性
全部基于DCOM实现(划重点)
目前只支持DA 2.0协议,且已经不再维护!

介于目前大部分生产环境使用的都是Linux系统,所以本次选择使用Utgard的方式。

准备环境

模拟OPC服务

生产环境有OPC服务,为了本地调试所以需要安装模拟器,经过查找使用比较多的是:KepServer。但是尽量是用于生产环境一致的软件版本进行测试,虽然交互基本一致,但是结构还是会有些许差别。比如我这里调试使用的是:NETx KNX OPC Server 3.5,此软件是用来对接照明的KNX 协议,所以还需要模拟KNX,使用到软件:KNX Virtual (下载需要注册),需要安装包的勇士可以留言~

安装OPC服务及环境配置(此处有坑)

先说坑,起初直接在本机安装的服务,结果使用Client连接一直报错!!:
Access is denied, please check whether the [domain-username-password] are correct. Also, if not already done please check the GETTING STARTED and FAQ sections in readme.htm. They provide information on how to correctly configure the Windows machine for DCOM access, so as to avoid such exceptions. [0x00000005]
尝试了各种方案,结果都不好使,搞了大半天,DCOM这个坑确实深。后来无奈直接安装了虚拟机使用操作系统重新来一遍,结果一遍过!不敢相信,所以此处建议安装虚拟机进行调试。

1. 安装虚拟机及操作系统
  • 虚拟机使用:VMware
  • 操作系统使用:Win Server 2016
  • OPC服务:KepServerV6 / NETx KNX OPC Server 3.5,均已调通

虚拟机及操作系统安装就不详述了,自行查询很简单。KEP的安装一直下一步即可,详细使用可查看:OPCServer:使用KEPServer,主要界面使用精简为以下三张图:
主页面介绍
client查看介绍
修改值介绍

2. 牛逼的DCOM配置来了

参考文章:OPC和DCOM配置,在虚拟机环境,我是一遍过!

  1. 创建用户并赋予权限(需要使用本地用户来连接OPC服务)
    在左下角windows菜单右键,点击计算机管理,在系统工具-本地用户和组-用户,右键新用户,输入帐号及密码,eg:
    用户名:OPCServer,密码:Abc123,选中:用户不能更改密码密码永不过期,点击创建,完成用户的创建!
  2. 继续:进入到,计算机管理-系统工具-本地用户和组-组,找到Distributed COM Users,右键点击添加到组,依次点击添加-高级-立即查找,找到OPCServer用户,点击确认完成提交。删除用户的普通组,进入到1的用户右键,选择属性,点击隶属于,删除Users
  3. 左下角搜索 高级安全 Windows 防火墙,在入站规则找到:Windows Management Instrumentation(DCOM-In),设置已启用允许链接
    高级选项卡,选中所有域、专用、公用。
    作用域 选项卡,选择任何IP地址
    确认即可。
  4. 允许程序 OPCEnum:在入站规则右键 新建规则,选择程序-此程序路径(C:\Windows\SysWOW64\OpcEnum.exe)-允许连接-勾选所有域、专用、公用-名称随便写,点击完成即可。
  5. 允许程序 KEPServer的server_runtime:在入站规则右键 新建规则,选择程序-此程序路径(C:\Program Files (x 86) \Kepware\KEPServerEX 6\server_runtime.exe)-允许连接-勾选所有域、专用、公用-名称随便写,点击完成即可。
  6. 左下角搜索 组件服务 ,在 组件服务-计算机-我的电脑,右键选择属性,在默认属性选项卡,勾选在此计算机上启用分布式 COM(E),在COM 安全选项卡,点击访问权限的编辑限制,选择OPCServer用户,并勾选允许 本地访问和远程访问,点击确定即可。
  7. 组件服务-计算机-我的电脑-DCOM 配置,找到 OpcEnum ,右键属性,然后选择安全选项卡,点击每一项(启动和激活权限、访问权限)中的编辑,选择OPCServer用户,一次允许所有选项点击确认即可。
  8. 组件服务-计算机-我的电脑-DCOM 配置,找到 KEPServerEX 6.4 ,右键属性,然后选择安全选项卡,点击每一项(启动和激活权限、访问权限)中的编辑,选择OPCServer用户,一次允许所有选项点击确认即可。
  9. 配置本地安全策略 :左下角搜索 本地安全策略,在安全设置-本地策略-安全选项,找到 网络访问:将 Everyone 权限应用于匿名用户,右键属性,设置为已启用,点击确定即可。
3. 安装IDE,java环境开始调试

启动IDE,创建项目。连接需要使用到OPC服务的CLSID,可直接通过windows查看,后面给到更简单的方式,前提需要配通DCOM。
windows查看的方法:
打开注册表,在 \HKEY_CLASSES_ROOT\Kepware.KEPServerEX.V6\CLSID ,中可查看到。

JAVA开发

1. POM引用及配置

增加引用,其中存在报错的依赖,已解决:

<dependency>
    <groupId>org.openscada.utgard</groupId>
    <artifactId>org.openscada.opc.dcom</artifactId>
    <version>1.5.0</version>
    <exclusions>
        <exclusion>
            <groupId>org.bouncycastle</groupId>
            <artifactId>bcprov-jdk15on</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.openscada.utgard</groupId>
    <artifactId>org.openscada.opc.lib</artifactId>
    <version>1.5.0</version>
    <exclusions>
        <exclusion>
            <groupId>org.bouncycastle</groupId>
            <artifactId>bcprov-jdk15on</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcprov-jdk15on</artifactId>
    <version>1.70</version>
</dependency>

配置:

opc:
  enable: ${OPC_ENABLE:true}
  host: ${OPC_HOST:192.168.65.130}
  user: ${OPC_USER:OPCUser}
  domain: ${OPC_DOMAIN:}
  password: ${OPC_PASSWORD:Abc123}
  cls-id: ${OPC_CLS_ID:aaeef077-f162-4a1f-ad88-c37f35ea4035} #7BC0CC8E-482C-47CA-ABDC-0FE7F9C6E729}
  prog-id: ${OPC_PROG_ID:NETxKNX.OPC.Server.3.5} #Kepware.KEPServerEX.V6}

说明:

  • KepServer的CLSID: 7BC0CC8E-482C-47CA-ABDC-0FE7F9C6E729,PROGID:Kepware.KEPServerEX.V6
  • NETX的CLSID: aaeef077-f162-4a1f-ad88-c37f35ea4035,PROGID:NETxKNX.OPC.Server.3.5

CLSID是指windows系统对于不同的应用程序,文件类型,OLE对象,特殊文件夹以及各种系统组件分配一个唯一表示它的ID代码,用于对其身份的标示和与其他对象进行区分。
先得说下GUID,它是Globally Unique Identifier的简称,中文翻译为“全局唯一标示符”,在Windows系统中也称之为Class ID,缩写为CLSID。
CLSID像人身份证一样,是个类的唯一标识:
ID是英文IDentity的缩写,是身份标识号码的意思,就是一个序列号,也叫帐号,是一个编码,而且是唯一的。
class是对某种类型的对象定义变量和方法的原型,是ID的样式或属性的补充。

后续介绍通过连接代码查看的方式。

2. 代码片段

2.1 罗列目标主机上的所有OPC服务

ServerList serverList = new ServerList(host, userName, password, domain);
Collection<ClassDetails> classDetails = serverList.listServersWithDetails(new Category[]{Categories.OPCDAServer20}, new Category[]{});
System.out.println("在目标主机上发现如下OPC服务器:");
for (ClassDetails details : classDetails) {
    System.out.format("\tprogId: '%s' \r\n\tclsId:'%s' \r\n\tdescription:'%s' \r\n\r\n", details.getProgId(), details.getClsId(), details.getClsId());
}

2.2 连接服务

ScheduledExecutorService threadPool = new ScheduledThreadPoolExecutor(5, r -> {
     Thread thread = new Thread(r);
     thread.setDaemon(true);
     thread.setName("客户端-任务-" + thread.getId());
     thread.setUncaughtExceptionHandler((Thread t, Throwable e) -> {
         log.error("'{}'发生异常!", t.getName(), e);
     });
     return thread;
 });
ConnectionInformation    connInfo = new ConnectionInformation();
connInfo.setHost(opcDaConfig.getHost());
if (Objects.nonNull(opcDaConfig.getDomain())) {
    connInfo.setDomain(opcDaConfig.getDomain());
}
connInfo.setUser(opcDaConfig.getUser());
connInfo.setPassword(opcDaConfig.getPassword());

connInfo.setClsid(opcDaConfig.getClsId());
if (Objects.nonNull(opcDaConfig.getProgId())) {
    connInfo.setProgId(opcDaConfig.getProgId());
}

Server server = new Server(connInfo, scheduledExecutorService);
ServerConnectionStateListener listener = connected -> {
	// 建立连接后,若断开,设置无限次10s尝试连接
    if (!connected) {
        reconnectFuture = scheduledExecutorService.scheduleAtFixedRate(() -> {
            try {
                server.connect();
            } catch (Exception e) {
                log.error("监听到连接断开,尝试重连失败!");
            }
        }, 0, 10, TimeUnit.SECONDS);
    } else {
        if (Objects.nonNull(reconnectFuture)) {
            reconnectFuture.cancel(true);
        }
    }
};
server.connect();
long connectTime = 10;

while (connectTime-- > 0) {
    if (server != null && server.getServerState() != null && server.getServerState().getServerState() != null) {
        server.addStateListener(listener);
        return server;
    } else {
        TimeUnit.SECONDS.sleep(2);
        log.warn("尝试重新连接!");
    }
}

2.3 罗列所有节点

public void dumpTree(Server server, int level) throws JIException, UnknownHostException {
    Branch branch = server.getTreeBrowser().browse();
    dumpTree(branch, level);
}

private void dumpTree(Branch branch, int level) {
    for (final Leaf leaf : branch.getLeaves()) {
        System.out.println(printTab(level) + "Leaf: " + leaf.getName() + ":"
                + leaf.getItemId());
    }
    for (final Branch subBranch : branch.getBranches()) {
        System.out.println(printTab(level) + "Branch: " + branch.getName());
        dumpTree(subBranch, level + 1);
    }
}
private String printTab(int level) {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < level; i++) {
        sb.append("\t");
    }
    return sb.toString();
}

2.4 同步读取

JiVariantUtil.java

public static DataItem parseValue(String itemId, ItemState itemState) throws Exception {
    Map<String, Object> value = getValue(itemState.getValue());
    return new DataItem(
            itemId,
            value.get("type").toString(),
            value.get("value"),
            itemState.getQuality(),
            itemState.getTimestamp().getTime(),
            DateUtil.getRecentMoment()
    );
}
public static Map<String, Object> getValue(JIVariant jiVariant) throws Exception {
 Object newValue;
    Object oldValue = jiVariant.getObject();
    String typeName = oldValue.getClass().getTypeName();
    if (typeName.startsWith(BASE_TYPE_PRDFIX) || typeName.startsWith(DATE_TYPE_PRDFIX)) {
        newValue = jiVariant.getObject();
    } else if (oldValue instanceof JIArray) {
        newValue = jiVariant.getObjectAsArray();
    } else if (oldValue instanceof IJIUnsigned) {
        newValue = jiVariant.getObjectAsUnsigned().getValue();
    } else if (oldValue instanceof IJIComObject) {
        newValue = jiVariant.getObjectAsComObject();
    } else if (oldValue instanceof JIString) {
        newValue = jiVariant.getObjectAsString().getString();
    } else if (oldValue instanceof JIVariant) {
        newValue = jiVariant.getObjectAsVariant();
    } else {
        newValue = oldValue;
        log.error("无法解析服务器的数据类型'{}'!原始数据:{}", typeName, oldValue.toString());
    }
    //newValue = getVal(jiVariant);

    HashMap<String, Object> result = new HashMap<>(2);
    result.put("type", newValue.getClass().getSimpleName());
    result.put("value", newValue);
    return result;
}

同步读:

// NETX测试例子
List<String> itemIds = List.of("\\NETxKNX\\BROADCAST\\01/0/000");

Group group = server.addGroup();
Map<String, Item> itemMap = group.addItems(itemIds.toArray(new String[0]));
List<DataItem> result = new ArrayList<>();
for (Map.Entry<String, Item> entry : itemMap.entrySet()) {
    Item item = entry.getValue();
    ItemState itemState = item.read(true);
    DataItem dataItem = JiVariantUtil.parseValue(item.getId(), itemState);
    result.add(dataItem);
}
System.out.println(result);

2.4 同步写

// KEP服务测试例子
String itemId = "通道 1.设备 1.TAG1";
Group group = server.addGroup();
Item item = group.addItem(itemId);
JIVariant jiVariant = new JIVariant(value);
item.write(jiVariant);

2.5 监听变化

// NETX测试例子
List<String> itemIds = List.of("\\NETxKNX\\BROADCAST\\01/0/000","\\NETxKNX\\BROADCAST\\07/0/000","xxx");

// 持续监听多少s,如果为0,
Integer listeningDurationSeconds = 0;

 // 启动一个同步的access用来读取地址上的值,线程池每1000ms读值一次
AccessBase access = new Async20Access(server, 1000, false);
for (String itemId : itemIds) {
    // 有变化后的回调地址
    access.addItem(itemId, new SubscribeDataCallback());
}
// 开始监听
access.bind();
if (listeningDurationSeconds == 0) {
    // countDownLatch.await();
} else {
    Thread.sleep(listeningDurationSeconds * 1000);
    // 结束监听
    access.unbind();
}

回调类:


public class SubscribeDataCallback implements DataCallback {
    @Override
    public void changed(Item item, ItemState itemState) {
       log.info("OPC-订阅数据变化,itemId:{},value:{}", item.getId(), itemState);
       // TODO::业务逻辑
    }
}

引用

KEPServer中文官方文档
OPC在网络防火墙环境下的配置
OPC通讯协议解析-OPC七问
Java OPC client开发踩坑记
JAVA对接OPC协议-Utgard
Kepware软件基本操作及使用Java Utgard实现OPC通信
github opcclient demo
github opc_client
How to Configure the Firewall to Allow DCOM Connections
HowToStartWithUtgard

  • 9
    点赞
  • 47
    收藏
    觉得还不错? 一键收藏
  • 19
    评论
评论 19
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值