先交代一下背景:我之前接过一个项目,现场几十台设备的数据都汇总到一台KepServerEX上,业务系统那边要我做一个Java服务,把这些实时数据拿过来再转发给MES和数据库。当时搜了一圈资料,发现讲OPC UA客户端连接KepServerEX的教程很零碎,很多是抄官方示例,没法直接落地。文章标题写的是"Java实现OPC UA客户端推送数据到KepServerEX",估计有人是想把数据写进去,也有人其实是想把KepServerEX的数据读出来推给下游——这两个方向我在下面都会讲到,你按自己需求选着看就行。
这篇教程默认你已经有Java基础、Maven会基本使用,对KepServerEX有粗浅认识但没配置过OPC UA。我会从KepServerEX端怎么开服务开始,到Java代码连接、订阅、写入,再到常见坑的排查,尽量把你可能遇到的问题一次说透。
1. 先把"推送数据"这件事理解清楚
1.1 最常见的场景:Java订阅KepServerEX,把数据往下游推
多数情况下,Java程序跑在应用服务器上,想拿到车间设备的实时数据。我们可以用OPC UA协议连上KepServerEX,把它当成OPC UA服务器来读数据、订阅变化,拿到数据后再推给MES、数据库、消息队列。
这里KepServerEX的角色是数据源,也是OPC UA服务端。Java程序作为OPC UA客户端,发起连接,订阅指定节点(Tag)的变化,数据一变就收到回调,然后由我们自己决定怎么处理这份数据——比如封装成JSON发给后端接口,或者写进Kafka、RabbitMQ。
1.2 另一种理解:向KepServerEX写入数据
也有小伙伴遇到的是反向需求,比如外部系统把计算结果写进KepServerEX的标签,让PLC逻辑动作。这时KepServerEX还是OPC UA服务器,Java还是客户端,只不过调用的是Write服务而不是订阅。这在工业场景里一般叫"下发指令"或"回写参数"。
所以标题里的"推送数据到KepServerEX",从技术实现上看其实是两种完全不同的操作。我在下文会分别把读取订阅和写入的代码都给你,便于你按需改造。
2. KepServerEX端准备工作:不配好服务端,代码写得再好也没用
2.1 开启OPC UA服务并确认端口
KepServerEX安装好后,默认并不一定启用了OPC UA服务。你需要打开KepServerEX Configuration,在左侧树里找到"OPC UA配置"这一项,通常在报警与事件、数据记录、OPC UA服务这几个选项卡中。
具体操作路径会因版本有差异,但核心就两件事:
- 勾选"启用OPC UA服务"。
- 确认TCP端口,我这边用的版本默认是49320,部分版本会显示为
opc.tcp://本机IP:49320。
端口务必记好,后面Java代码连接时用的就是它。如果机器上开了防火墙,记得放行这个端口,否则客户端连不上,还容易误以为是代码写错了。
注意:KepServerEX 6.x和KepServerEX 5.x的配置入口略有不同。如果你用的是旧版本,找不到OPC UA配置,就去查一下对应版本文档的"OPC UA Server"章节,原理是一样的。
2.2 添加模拟设备和标签
很多入门者卡在没有真实PLC。其实KepServerEX自带了Simulator驱动,可以模拟数据变化,适合用来调试OPC UA连接和订阅逻辑。
在KepServerEX里新建一个通道(Channel),驱动选择Simulator;然后建一个设备(Device),在设备下面建标签(Tag)。比如我这边建的完整路径就是:
Channel1.Device1.Tag1标签数据类型可以选浮点、整数或布尔。为了让订阅效果明显,你可以把标签的扫描间隔设短一点,或者直接让模拟器按固定步长变值,这样客户端订阅后很快就能看到数据在跳。
2.3 拿到NodeId:这是新手最容易卡住的地方
OPC UA中每个标签都有唯一的节点标识,称为NodeId。Milo客户端代码里要用NodeId.parse(...)来定位标签。KepServerEX里最常用的格式是:
ns=2;s=Channel1.Device1.Tag1其中ns=2是命名空间索引,s=后面跟标签路径。实际运行环境里ns值不一定是2,可能是3、4或者别的数字,取决于KepServerEX的配置。你可以在客户端工具里确认节点详情,也可以先写代码读取服务器地址空间来探测——如果连接建立了,但节点解析失败,优先检查ns号。
调试工具方面,我习惯用UAExpert。它可以直接浏览KepServerEX地址空间,树形展示所有Channel、Device、Tag,点一下就能看到NodeId、数据类型、当前值。写代码之前先用UAExpert连一次,能帮你排除很多"怎么连不上"的问题。
3. Java客户端开发:依赖引入与基础连接
3.1 库的选择:为什么是Eclipse Milo
Java世界里做OPC UA客户端的库不少,最主流的还是Eclipse Milo。它是纯Java实现,不依赖本地DLL,Windows和Linux都能跑,Maven拉依赖就行。相比Prosys的SDK,Milo免费开源,社区活跃,遇到问题也好搜答案。
用Milo之前有件事要清楚:它封装的API在不同小版本间有变化。我下文代码基于0.6.x系列,如果你拉的是0.7或更新的版本,个别方法名可能有调整,但整体思路一致。建议你固定版本,比如我用的0.6.8,这样代码能稳定编译。
3.2 Maven依赖最小配置
<dependency> <groupId>org.eclipse.milo</groupId> <artifactId>sdk-client</artifactId> <version>0.6.8</version> </dependency>另外我一般还会加一个日志依赖,方便看调试信息。如果你用Spring Boot,日志就交给SLF4J,Milo内部也用SLF4J输出,不用额外配置。
3.3 编写连接代码
连接KepServerEX,核心就三步:创建客户端、设置应用描述、发起connect。下面是一个可以直接跑的示例:
import org.eclipse.milo.opcua.sdk.client.OpcUaClient; import org.eclipse.milo.opcua.stack.core.UaException; import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy; import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText; import org.eclipse.milo.opcua.stack.core.types.builtin.UInteger; public class KepClientConnector { public static void main(String[] args) throws Exception { String endpointUrl = "opc.tcp://192.168.1.101:49320"; OpcUaClient client = OpcUaClient.create( endpointUrl, endpoints -> endpoints.stream() .filter(e -> e.getSecurityPolicyUri() .equals(SecurityPolicy.None.getUri())) .findFirst(), config -> config .setApplicationName(LocalizedText.english("KepServerEX Data Publisher")) .setApplicationUri("urn:mycompany:java:opcua:publisher") .setRequestTimeout(UInteger.valueOf(5000)) ); client.connect().get(); System.out.println("连接成功: " + client.getEndpoint().getEndpointUrl()); // 这里先演示读一个标签,下一节再讲订阅 client.disconnect().get(); } }代码里有几个细节值得说明:
SecurityPolicy.None表示不加密不签名。内网调试阶段图省事可以用它;生产环境建议换成Basic256Sha256,否则数据明文传输,有安全风险。endpoints -> ...findFirst()是从服务器返回的所有端点里挑一个。如果你指定了安全策略,这里要改成对应的过滤条件。setApplicationUri是客户端的应用URI,自定义一个URN即可,不要跟其他应用冲突。
如果连接时报:
Exception: UaException: StatusCode{name=Bad_SecurityChecksFailed, value=...}或者提示证书问题,多半是安全策略不匹配或证书信任没配置。先用SecurityPolicy.None跑通,再逐步加安全策略。
4. 读取与订阅推送:核心实现
4.1 单次读取:先验证节点和权限
连接成功后,先用Read操作验证NodeId是否正确。这一步骤很有必要,我调试时经常用它确认ns=2;s=Channel1.Device1.Tag1这个路径没写错。
import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId; import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue; import org.eclipse.milo.opcua.stack.core.types.enumerated.AttributeId; import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn; NodeId nodeId = NodeId.parse("ns=2;s=Channel1.Device1.Tag1"); DataValue value = client.readValue( TimestampsToReturn.Both, nodeId ).get(); System.out.println("当前值: " + value.getValue().getValue());如果能正确打印出数值,说明连接、节点、权限都没问题。接下来就可以上订阅了。
4.2 订阅监控:数据一变就推给你
订阅是OPC UA最常用的能力,相当于给服务器说"这个标签你盯着,一变就通知我"。服务器端会按采样周期检查值,如果变化程度超过设定死区,就主动把新值发给客户端,不用客户端反复轮询。
下面是基于Milo 0.6.8的订阅代码:
import org.eclipse.milo.opcua.sdk.client.api.subscriptions.UaMonitoredItem; import org.eclipse.milo.opcua.sdk.client.api.subscriptions.UaSubscription; import org.eclipse.milo.opcua.stack.core.types.builtin.QualifiedName; import org.eclipse.milo.opcua.stack.core.types.enumerated.MonitoringMode; import org.eclipse.milo.opcua.stack.core.types.structured.MonitoredItemCreateRequest; import org.eclipse.milo.opcua.stack.core.types.structured.MonitoredItemCreateResult; import org.eclipse.milo.opcua.stack.core.types.structured.ReadValueId; import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue; import org.eclipse.milo.opcua.stack.core.types.builtin.Variant; import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger; // 创建订阅,500ms为发布周期 UaSubscription subscription = client.getSubscriptionManager() .createSubscription(500.0) .get(); // 构造监控项请求 ReadValueId readValueId = new ReadValueId( NodeId.parse("ns=2;s=Channel1.Device1.Tag1"), AttributeId.Value.uid(), null, QualifiedName.NULL_VALUE ); MonitoredItemCreateRequest request = new MonitoredItemCreateRequest( readValueId, MonitoringMode.Reporting, new MonitoringParameters( UInteger.valueOf(1), // 客户端句柄 Double.NaN, // 采样间隔,使用服务器默认 null, // 不设置过滤器 UInteger.valueOf(1000),// 队列长度 true // 丢弃最旧的数据 ) ); // 创建监控项并订阅数据回调 List<MonitoredItemCreateResult> results = subscription .createMonitoredItems( List.of(request), (item, value) -> { Object v = value.getValue().getValue(); String nodeIdStr = item.getReadValueId().getNodeId().toParseableString(); System.out.println("节点 " + nodeIdStr + " 新值: " + v); // 这里就是推送入口,见4.3 } ) .get();注意两点:
createMonitoredItems的返回结果是CompletableFuture<List<MonitoredItemCreateResult>>,所以回调里能拿到MonitoredItem和DataValue,一定要用item.getReadValueId().getNodeId()去对应哪一个节点,因为一个订阅下可以挂多个监控项。- 回调是在Milo的IO线程里执行的,不要在回调里做耗时操作,比如直接同步写数据库、发HTTP请求。正确做法是把消息丢进队列,再用单独的消费线程去推送,稍后我会展开讲。
4.3 把收到的新值推给下游系统
拿到订阅回调里的值之后,推送目标常见有三种:REST接口、消息队列、数据库。不管哪种,都建议在回调里只做"投递",别做"处理"。
我比较倾向的做法是引入一个BlockingQueue,回调里把(nodeId, value, timestamp)封装成对象塞进队列,后端线程池消费。这样既能削峰,又能保证推送操作不会反压到OPC UA的IO线程。
import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; BlockingQueue<TagData> queue = new LinkedBlockingQueue<>(5000); // 回调里这样写: (item, value) -> queue.offer(new TagData( item.getReadValueId().getNodeId().toParseableString(), value.getValue().getValue(), value.getServerTime() )); // 消费线程: while (true) { TagData data = queue.take(); // 推送逻辑:可以是HTTP、WebSocket、MQTT、JDBC pushToMqtt(data); }TagData就是一个简单的POJO,字段包含节点名、数值、服务器时间戳。如果没有消息中间件,直接HTTP POST给业务接口也行。核心原则只有一个:不要在回调里同步做重活,宁可让队列积压,也不要阻塞OPC UA的推送通道。
5. 反向写数据:把数据推送到KepServerEX
5.1 写入单个标签的基本写法
如果你的需求是把Java侧的数据写进KepServerEX标签,用到的是OPC UA的Write服务。马思路跟Read很接近:构造WriteValue,然后调用client.write(...)。
import org.eclipse.milo.opcua.stack.core.types.structured.WriteValue; // 要写入的值,这里以Double为例 DataValue newValue = new DataValue(new Variant(123.45)); WriteValue writeValue = new WriteValue( NodeId.parse("ns=2;s=Channel1.Device1.Tag1"), AttributeId.Value.uid(), null, newValue ); // 执行写入 var response = client.write(List.of(writeValue)).get(); System.out.println("写入结果: " + response.getResults()[0]);client.write返回的StatusCode需要逐个检查。Good表示成功,其他值需要到OPC UA状态码表里查对应含义。常见的失败原因有两个:一是标签只读,不允许外部写入;二是写入的数据类型与标签类型不匹配,比如标签是Int16你写了个字符串。
5.2 批量写入与权限问题
实际项目里往往要一次写多个标签,比如下发一组工艺参数。你可以构造一个List<WriteValue>一次性提交:
List<WriteValue> batch = new ArrayList<>(); batch.add(new WriteValue(nodeA, AttributeId.Value.uid(), null, new DataValue(new Variant(100))); batch.add(new WriteValue(nodeB, AttributeId.Value.uid(), null, new DataValue(new Variant(200))); var batchResp = client.write(batch).get();批量写入时,OPC UA协议本身支持原子性要求,但KepServerEX的处理按标签配置来,不一定保证全部成功或全部失败。业务上有强一致要求的话,写完还需要回读校验一遍。
权限提醒:KepServerEX的OPC UA用户或匿名会话默认可能有只读权限,如果需要写入,必须在KepServerEX的"OPC UA安全"配置里给对应用户授予Write权限。这个坑我踩过,代码明明没问题,写入结果一直是Bad_NotWritable,最后发现是用户权限不够。
6. 关键参数调优与常见问题排查
6.1 订阅周期、采样间隔、队列大小怎么定
这几个参数解决了,你就能控制推送的实时性。
- 发布周期(PublishingInterval):服务器每隔这个时间向客户端发一次通知,单位毫秒。我一般设500ms,对大多数MES场景够用。如果追求高实时性,可以压到100ms,但要评估服务器和网络压力。
- 采样间隔(SamplingInterval):服务器检查标签值变化的频率。设为
Double.NaN表示用服务器最小值,通常效果就是"一变就报"。 - 队列长度(QueueSize):服务器为这个监控项缓存的未处理消息数量。设为1000比较稳妥,如果客户端消费慢,队列填满了又有新数据,会根据
discardOldest参数决定丢新还是丢旧。我习惯discardOldest=true,因为工业实时数据看重"最新值",老值丢了无妨。
参数之间是相关的。最理想的组合是:采样间隔取服务器最小值,发布周期按业务实时性定,队列长度500到2000之间。别把发布周期设得太短而队列又满,否则CPU和网络很容易被打满。
6.2 连接失败:从端口到安全策略逐个查
连接失败是最高频的问题。我建议按下面顺序排查:
- 看KepServerEX是否真的启用了OPC UA服务,端口是否监听。在KepServerEX所在机器上执行
netstat -an | findstr 49320,没有监听说明服务没起来。 - 看防火墙有没有放行端口。先临时关防火墙测一次,通了再精确配置规则。
- 看端点URL写没写对,IP地址是否可达。注意服务器重启后IP可能变化。
- 看安全策略匹配。如果KepServerEX只配了Basic256Sha256,你却用SecurityPolicy.None去过滤端点,filter返回空自然连不上。调试时先用UAExpert对比能连的策略。
6.3 证书信任问题
Milo客户端第一次连接加密端点时,会收到服务器证书。默认情况下客户端不信任未知证书,会抛证书校验异常。
解决方式有两个:
- 临时方式:写代码时把KepServerEX的证书导入到Java的信任库,或者直接信任所有证书(仅限测试环境,别在生产这么干)。
- 规范方式:在Milo配置里使用
KeyStoreLoader加载客户端私钥和证书,并与KepServerEX端做双向证书信任。这需要做一轮证书交换。
如果只是内网测试,我一般先用SecurityPolicy.None跑通业务流程,再回头补证书。这样能最快定位问题范围。
6.4 订阅已创建但收不到数据
这种情况也常见,代码逻辑没错,就是没数据推过来。我把它归结为三类原因:
- 标签值其实没变化。OPC UA订阅默认只在值变化时推送(超过死区),如果标签本身就是常量,自然没通知。解决方法是先在KepServerEX的模拟器里让值持续变化。
- 队列被消费端拖垮了。回调里做了耗时操作,导致IO线程阻塞,后面数据全部积压。改成队列+独立线程消费后立刻缓解。
- 发布周期设得太长。服务器还没到发布节点,客户端自然收不到。把周期从1000ms调到200ms左右试试,能直观感受到差别。
6.5 代码层面的几个易错点
最后一个坑,差点让项目延期的那种:很多人写Milo订阅的时候用了旧版API,把createMonitoredItems的返回值直接当列表用。在我用的0.6.8版本里,这个方法是异步的,返回值是CompletableFuture,必须调get()阻塞或者用回调方式处理。如果你在网上搜到的是老代码,很可能编都编不过。
另外Milo要求Java 8及以上,如果你项目还在用Java 7,那就没有官方支持了,建议先升JDK再折腾。
7. 这段经历给我的几点心得
整个项目做下来,我最深的体会是:OPC UA这块真正的难点不在Java代码,而在工业场景里的环境差异——安全策略、证书、端口、命名空间,每一样都决定了你几十行代码能不能跑起来。所以我的调试顺序永远固定:先用UAExpert手动连一次KepServerEX,确认端点、用户、节点都没问题,再写Java代码。这个习惯帮我省下了大量排查时间。
另外如果你是要做长期运行的服务,千万别忽略断线重连和日志。OPC UA连接在设备断电、网络抖动时会断开,你不可能一直盯着。我这里简单提一下,可以在主线程外配一个定时任务,定期检查client的连接状态,断开就重新connect,并按订阅参数重建所有监控项。这个逻辑不复杂,但很关键,不加的话服务跑几天就静默完蛋了。
最后再分享一个小技巧:KepServerEX的标签名和NodeId之间并不总是严格对应,跨版本或者有人重命名过标签,旧的ns=2;s=...路径就可能失效。稳妥的做法是在程序启动时用client.getAddressSpace().browse(...)遍历一遍地址空间,动态匹配标签名,找到NodeId再订阅。这样即使工程师改了标签路径,程序也能自动跟上,不用每次改代码重启。
希望这篇教程对你能有帮助。上面这些代码和排查思路,都是我在真实项目里一条条验证过的,你照着搭应该一天之内就能把数据从KepServerEX推到你的业务系统里。有问题欢迎在评论区交流。