前言
之前写过一篇关于MQTT的方式进行物理访问的文章:SpringBoot集成MQTT,WebSocket返回前端信息_springboot mqtt websocket-CSDN博客
最近又接触到OPCUA协议,想通过java试试看能不能实现。
软件
在使用java实现之前,想着有没有什么模拟器作为服务器端能够进行发送opcua数据,网上搜到好多都是使用KEPServerEX6,下载了之后,发现学习成本好大,这个软件都不会玩,最后终于找到了Prosys OPC UA Simulation Server,相对来说,这个软件的学习成本很低。但是也有一个弊端,只能进行本地模拟。
下载地址:Prosys OPC - OPC UA Simulation Server Downloads
下载安装完成之后,打开页面就可以看到,软件生成的opcua测试地址
为了方便操作,把所有的菜单全部暴露出来,点击Options下的Switch to Basic Mode
如果需要修改这个默认的连接地址,可通过 Endpoints
菜单进行设置(我这里用的是默认的地址)。也可以在这个菜单下修改连接方式和加密方式。
也可以在Users下添加用户名和密码
Objects上自带了一些函数能够帮助我们快速进行测试,也可以自己创建(我使用的是自带的)
接下来就是代码
代码
引入依赖
<dependency>
<groupId>org.eclipse.milo</groupId>
<artifactId>sdk-client</artifactId>
<version>0.6.9</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.70</version>
</dependency>
<dependency>
<groupId>org.eclipse.milo</groupId>
<artifactId>sdk-server</artifactId>
<version>0.6.9</version>
</dependency>
目前实现了两种方式:匿名方式、用户名加证书方式,还有仅用户名方式后续继续研究
匿名方式:
import org.eclipse.milo.opcua.sdk.client.OpcUaClient;
import org.eclipse.milo.opcua.sdk.client.api.identity.AnonymousProvider;
import org.eclipse.milo.opcua.sdk.client.api.identity.UsernameProvider;
import org.eclipse.milo.opcua.sdk.server.Session;
import org.eclipse.milo.opcua.stack.core.AttributeId;
import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy;
import org.eclipse.milo.opcua.stack.core.types.builtin.*;
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger;
import org.eclipse.milo.opcua.stack.core.types.enumerated.MonitoringMode;
import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn;
import org.eclipse.milo.opcua.stack.core.types.structured.MonitoredItemCreateRequest;
import org.eclipse.milo.opcua.stack.core.types.structured.MonitoringParameters;
import org.eclipse.milo.opcua.stack.core.types.structured.ReadValueId;
import org.eclipse.milo.opcua.stack.core.types.structured.UserNameIdentityToken;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
/**
* 无密码无证书无安全认证模式
* @Author: majinzhong
* @Data:2024/8/30
*/
public class OpcUaTest {
//opc ua服务端地址
private final static String endPointUrl = "opc.tcp://Administrator:53530/OPCUA/SimulationServer";
// private final static String endPointUrl = "opc.tcp://192.168.24.13:4840";
public static void main(String[] args) {
try {
//创建OPC UA客户端
OpcUaClient opcUaClient = createClient();
//开启连接
opcUaClient.connect().get();
// 订阅消息
subscribe(opcUaClient);
// 写入
// writeValue(opcUaClient);
// 读取
// readValue(opcUaClient);
// 关闭连接
opcUaClient.disconnect().get();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 创建OPC UA客户端
*
* @return
* @throws Exception
*/
private static OpcUaClient createClient() throws Exception {
Path securityTempDir = Paths.get(System.getProperty("java.io.tmpdir"), "security");
Files.createDirectories(securityTempDir);
if (!Files.exists(securityTempDir)) {
throw new Exception("unable to create security dir: " + securityTempDir);
}
return OpcUaClient.create(endPointUrl,
endpoints ->
endpoints.stream()
.filter(e -> e.getSecurityPolicyUri().equals(SecurityPolicy.None.getUri()))
.findFirst(),
configBuilder ->
configBuilder
.setApplicationName(LocalizedText.english("OPC UA test")) // huazh-01
.setApplicationUri("urn:eclipse:milo:client") // ns=2:s=huazh-01.device1.data-huazh
//访问方式 new AnonymousProvider()
.setIdentityProvider(new AnonymousProvider())
.setRequestTimeout(UInteger.valueOf(5000))
.build()
);
}
private static void subscribe(OpcUaClient client) throws Exception {
//创建发布间隔1000ms的订阅对象
client.getSubscriptionManager()
.createSubscription(1000.0)
.thenAccept(t -> {
//节点ns=2;s=test.device2.test2
// NodeId nodeId = new NodeId(4, 322);
NodeId nodeId = new NodeId(3, 1003);
ReadValueId readValueId = new ReadValueId(nodeId, AttributeId.Value.uid(), null, null);
//创建监控的参数
MonitoringParameters parameters = new MonitoringParameters(UInteger.valueOf(1), 1000.0, null, UInteger.valueOf(10), true);
//创建监控项请求
//该请求最后用于创建订阅。
MonitoredItemCreateRequest request = new MonitoredItemCreateRequest(readValueId, MonitoringMode.Reporting, parameters);
List<MonitoredItemCreateRequest> requests = new ArrayList<>();
requests.add(request);
//创建监控项,并且注册变量值改变时候的回调函数。
t.createMonitoredItems(
TimestampsToReturn.Both,
requests,
(item, id) -> item.setValueConsumer((it, val) -> {
System.out.println("=====订阅nodeid====== :" + it.getReadValueId().getNodeId());
System.out.println("=====订阅value===== :" + val.getValue().getValue());
})
);
}).get();
//持续订阅
Thread.sleep(Long.MAX_VALUE);
}
public static void readValue(OpcUaClient client) {
try {
NodeId nodeId = new NodeId(3, 1002);
DataValue value = client.readValue(0.0, TimestampsToReturn.Both, nodeId).get();
System.out.println("=====读取ua1====:" + value.getValue().getValue());
} catch (Exception e) {
e.printStackTrace();
}
}
public static void writeValue(OpcUaClient client) {
try {
//创建变量节点 test.device2.test2
NodeId nodeId = new NodeId(2, "test.device2.test2");
//uda3 boolean
Short value = 11;
//创建Variant对象和DataValue对象
Variant v = new Variant(value);
DataValue dataValue = new DataValue(v, null, null);
StatusCode statusCode = client.writeValue(nodeId, dataValue).get();
System.out.println(statusCode);
System.out.println("=====写入ua1====:" + statusCode.isGood());
} catch (Exception e) {
e.printStackTrace();
}
}
}
用户名加正式认证方式:
import org.eclipse.milo.opcua.sdk.client.OpcUaClient;
import org.eclipse.milo.opcua.sdk.client.api.identity.UsernameProvider;
import org.eclipse.milo.opcua.stack.core.AttributeId;
import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy;
import org.eclipse.milo.opcua.stack.core.types.builtin.*;
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger;
import org.eclipse.milo.opcua.stack.core.types.enumerated.MonitoringMode;
import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn;
import org.eclipse.milo.opcua.stack.core.types.structured.MonitoredItemCreateRequest;
import org.eclipse.milo.opcua.stack.core.types.structured.MonitoringParameters;
import org.eclipse.milo.opcua.stack.core.types.structured.ReadValueId;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
/**
* 有密码有证书有安全认证模式
* @Author: majinzhong
* @Data:2024/8/30
*/
public class OpcUaTest2 {
//opc ua服务端地址
private final static String endPointUrl = "opc.tcp://Administrator:53530/OPCUA/SimulationServer";
// private final static String endPointUrl = "opc.tcp://192.168.24.13:4840";
public static void main(String[] args) {
try {
//创建OPC UA客户端
OpcUaClient opcUaClient = createClient();
//开启连接
opcUaClient.connect().get();
// 订阅消息
subscribe(opcUaClient);
// 写入
// writeValue(opcUaClient);
// 读取
// readValue(opcUaClient);
// 关闭连接
opcUaClient.disconnect().get();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 创建OPC UA客户端
*
* @return
* @throws Exception
*/
private static OpcUaClient createClient() throws Exception {
Path securityTempDir = Paths.get(System.getProperty("java.io.tmpdir"), "security");
Files.createDirectories(securityTempDir);
if (!Files.exists(securityTempDir)) {
throw new Exception("unable to create security dir: " + securityTempDir);
}
KeyStoreLoader loader = new KeyStoreLoader().load(securityTempDir);
return OpcUaClient.create(endPointUrl,
endpoints ->
endpoints.stream()
.filter(e -> e.getSecurityPolicyUri().equals(SecurityPolicy.Basic256Sha256.getUri()))
// .filter(e -> e.getSecurityPolicyUri().equals(SecurityPolicy.None.getUri()))
.findFirst(),
configBuilder ->
configBuilder
.setApplicationName(LocalizedText.english("OPC UA test")) // huazh-01
.setApplicationUri("urn:eclipse:milo:client") // ns=2:s=huazh-01.device1.data-huazh
//访问方式 new AnonymousProvider()
.setCertificate(loader.getClientCertificate())
.setKeyPair(loader.getClientKeyPair())
.setIdentityProvider(new UsernameProvider("TOPNC", "TOPNC123"))
.setRequestTimeout(UInteger.valueOf(5000))
.build()
);
}
private static void subscribe(OpcUaClient client) throws Exception {
//创建发布间隔1000ms的订阅对象
client.getSubscriptionManager()
.createSubscription(1000.0)
.thenAccept(t -> {
//节点ns=2;s=test.device2.test2
// NodeId nodeId = new NodeId(3, "unit/Peri_I_O.gs_ComToRM.r32_A1_Axis_ActValue");
NodeId nodeId = new NodeId(3, 1003);
ReadValueId readValueId = new ReadValueId(nodeId, AttributeId.Value.uid(), null, null);
//创建监控的参数
MonitoringParameters parameters = new MonitoringParameters(UInteger.valueOf(1), 1000.0, null, UInteger.valueOf(10), true);
//创建监控项请求
//该请求最后用于创建订阅。
MonitoredItemCreateRequest request = new MonitoredItemCreateRequest(readValueId, MonitoringMode.Reporting, parameters);
List<MonitoredItemCreateRequest> requests = new ArrayList<>();
requests.add(request);
//创建监控项,并且注册变量值改变时候的回调函数。
t.createMonitoredItems(
TimestampsToReturn.Both,
requests,
(item, id) -> item.setValueConsumer((it, val) -> {
System.out.println("=====订阅nodeid====== :" + it.getReadValueId().getNodeId());
System.out.println("=====订阅value===== :" + val.getValue().getValue());
})
);
}).get();
//持续订阅
Thread.sleep(Long.MAX_VALUE);
}
public static void readValue(OpcUaClient client) {
try {
NodeId nodeId = new NodeId(3, 1002);
DataValue value = client.readValue(0.0, TimestampsToReturn.Both, nodeId).get();
System.out.println("=====读取ua1====:" + value.getValue().getValue());
} catch (Exception e) {
e.printStackTrace();
}
}
public static void writeValue(OpcUaClient client) {
try {
//创建变量节点 test.device2.test2
NodeId nodeId = new NodeId(2, "test.device2.test2");
//uda3 boolean
Short value = 11;
//创建Variant对象和DataValue对象
Variant v = new Variant(value);
DataValue dataValue = new DataValue(v, null, null);
StatusCode statusCode = client.writeValue(nodeId, dataValue).get();
System.out.println(statusCode);
System.out.println("=====写入ua1====:" + statusCode.isGood());
} catch (Exception e) {
e.printStackTrace();
}
}
}
证书加密类
import org.eclipse.milo.opcua.sdk.server.util.HostnameUtil;
import org.eclipse.milo.opcua.stack.core.util.SelfSignedCertificateBuilder;
import org.eclipse.milo.opcua.stack.core.util.SelfSignedCertificateGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.*;
import java.security.cert.X509Certificate;
import java.util.regex.Pattern;
/**
* Created by Cryan on 2021/8/4.
* TODO.OPCUA 证书生成
*/
class KeyStoreLoader {
private final Logger logger = LoggerFactory.getLogger(getClass());
private static final Pattern IP_ADDR_PATTERN = Pattern.compile(
"^(([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.){3}([01]?\\d\\d?|2[0-4]\\d|25[0-5])$");
// 证书别名
private static final String CLIENT_ALIAS = "client-ai";
// 获取私钥的密码
private static final char[] PASSWORD = "password".toCharArray();
// 证书对象
private X509Certificate clientCertificate;
// 密钥对对象
private KeyPair clientKeyPair;
KeyStoreLoader load(Path baseDir) throws Exception {
// 创建一个使用`PKCS12`加密标准的KeyStore。KeyStore在后面将作为读取和生成证书的对象。
KeyStore keyStore = KeyStore.getInstance("PKCS12");
// PKCS12的加密标准的文件后缀是.pfx,其中包含了公钥和私钥。
// 而其他如.der等的格式只包含公钥,私钥在另外的文件中。
Path serverKeyStore = baseDir.resolve("example-client.pfx");
logger.info("Loading KeyStore at {}", serverKeyStore);
// 如果文件不存在则创建.pfx证书文件。
if (!Files.exists(serverKeyStore)) {
keyStore.load(null, PASSWORD);
// 用2048位的RAS算法。`SelfSignedCertificateGenerator`为Milo库的对象。
KeyPair keyPair = SelfSignedCertificateGenerator.generateRsaKeyPair(2048);
// `SelfSignedCertificateBuilder`也是Milo库的对象,用来生成证书。
// 中间所设置的证书属性可以自行修改。
SelfSignedCertificateBuilder builder = new SelfSignedCertificateBuilder(keyPair)
.setCommonName("Eclipse Milo Example Client test")
.setOrganization("mjz")
.setOrganizationalUnit("dev")
.setLocalityName("mjz")
.setStateName("CA")
.setCountryCode("US")
.setApplicationUri("urn:eclipse:milo:client")
.addDnsName("localhost")
.addIpAddress("127.0.0.1");
// Get as many hostnames and IP addresses as we can listed in the certificate.
for (String hostname : HostnameUtil.getHostnames("0.0.0.0")) {
if (IP_ADDR_PATTERN.matcher(hostname).matches()) {
builder.addIpAddress(hostname);
} else {
builder.addDnsName(hostname);
}
}
// 创建证书
X509Certificate certificate = builder.build();
// 设置对应私钥的别名,密码,证书链
keyStore.setKeyEntry(CLIENT_ALIAS, keyPair.getPrivate(), PASSWORD, new X509Certificate[]{certificate});
try (OutputStream out = Files.newOutputStream(serverKeyStore)) {
// 保存证书到输出流
keyStore.store(out, PASSWORD);
}
} else {
try (InputStream in = Files.newInputStream(serverKeyStore)) {
// 如果文件存在则读取
keyStore.load(in, PASSWORD);
}
}
// 用密码获取对应别名的私钥。
Key serverPrivateKey = keyStore.getKey(CLIENT_ALIAS, PASSWORD);
if (serverPrivateKey instanceof PrivateKey) {
// 获取对应别名的证书对象。
clientCertificate = (X509Certificate) keyStore.getCertificate(CLIENT_ALIAS);
// 获取公钥
PublicKey serverPublicKey = clientCertificate.getPublicKey();
// 创建Keypair对象。
clientKeyPair = new KeyPair(serverPublicKey, (PrivateKey) serverPrivateKey);
}
return this;
}
// 返回证书
X509Certificate getClientCertificate() {
return clientCertificate;
}
// 返回密钥对
KeyPair getClientKeyPair() {
return clientKeyPair;
}
}
代码讲解
仔细阅读代码不难发现,匿名方式和用户名加正式方式仅仅只有这一块不太一样
配置完成之后,需要修改想要订阅的节点,进行读取数据,匿名方式和用户名加证书方式一致,都是在代码的NodeId nodeId = new NodeId(3, 1003);进行修改,其中的3和1003对应软件上Objects上的
运行
一切配置好并且修改好之后,先运行匿名方式!匿名方式!匿名方式!!!(用户名加证书方式还有一个点,下面再说)
可以看到已经能够读取到节点的数据了
第一次运行用户名加证书方式的时候,会报java.lang.RuntimeException: java.util.concurrent.ExecutionException: UaException: status=Bad_SecurityChecksFailed, message=Bad_SecurityChecksFailed (code=0x80130000, description="An error occurred verifying security.")的错误,这是因为证书没有被添加信任
在Certificates下找到自己的证书,将Reject改成Trust即可。
因为代码中setApplicationUri时写的是urn:eclipse:milo:client,所以这个就是刚刚代码创建的证书。
运行用户名加证书方式
已经可以正常读取到节点数据了
补充
问题一:运行代码时,可能会遇见java.lang.RuntimeException: UaException: status=Bad_ConfigurationError, message=no endpoint selected的错误,这是因为,OPCUA服务器端没有允许这种方式(OPCUA目前我看到的有三种方式:匿名、用户名、用户名加证书),所以需要修改OPCUA服务器端添加这种方式,添加在 Endpoints菜单下
,或者查看服务器端支持哪种方式,修改代码。
问题二:org.eclipse.milo.opcua.stack.core.UaException: no KeyPair configured
这种是因为没有配置密钥,代码方面出现了问题,需要在创建客户端的时候setKeyPair()
问题三:org.eclipse.milo.opcua.stack.core.UaException: no certificate configured
这种时因为没有配置证书,代码方面出现了问题,需要在创建客户端的时候setCertificate()