文章目录
一、前置条件
二、KEPServerEX 6的配置
三、OPC 客户端开发
四、测试
五、注意点
一、前置条件
jdk:1.8、maven:3.8.1、OPC服务端:KEPServerEX 6、Milo
二、KEPServerEX 6的配置
若有下角没有图中的图标,运行KEPServerEX 6 Administration
维护服务器端点信息:
指定url和相应的安全策略
url:opc.tcp://127.0.0.1:49320 安全策略:None、Basic128Rsa15、Basic256、Basic256Sha256、Aes128_Sha256_RsaOaep、Aes256_Sha256_RsaPss
添加完指定的用户信息,如下图:
运行项目后选择允许访问的客户端名称对应的证书,点击信任后如下:
创建通道->添加设备->维护相应的标记(根据项目需要指定相应的类型和访问权限)
<dependency>
<groupId>org.eclipse.milo</groupId>
<artifactId>sdk-client</artifactId>
<version>0.6.10</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.69</version>
</dependency>
<dependency>
<groupId>org.eclipse.milo</groupId>
<artifactId>sdk-server</artifactId>
<version>0.6.10</version>
</dependency>
2.创建OPC客户端
package com.example.opc;
import com.example.opc.service.OpcUAClientService;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.milo.opcua.sdk.client.OpcUaClient;
import org.eclipse.milo.opcua.stack.core.Stack;
import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.uint;
/**
* @License : (C) Copyright, MaxSense (Shanghai) Technology Co., Ltd.
*
* @file : OpcUAClientRunner.java
*
* @desc :
*
* @author : xxx
*
* @date : 2023/8/3 11:44
*
* @version : V1.0
*
*/
@Slf4j
@Service("OpcUAClientRunner")
public class OpcUAClientRunner {
private final Logger logger = LoggerFactory.getLogger(getClass());
private final CompletableFuture<OpcUaClient> future = new CompletableFuture<>();
private final OpcUAClientService opcUAClientService;
public OpcUAClientRunner(OpcUAClientService opcUAClientService) {
this.opcUAClientService = opcUAClientService;
}
/**
* OPC UA的运行入口程序
*/
public void run() {
try {
// 创建OPC UA客户端
OpcUaClient opcUaClient = createClient();
// future执行完毕后,异步判断状态
future.whenCompleteAsync((c, ex) -> {
if (ex != null) {
logger.error("连接OPC UA服务错误: {}", ex.getMessage(), ex);
}
// 关闭OPC UA客户端
try {
opcUaClient.disconnect().get();
Stack.releaseSharedResources();
} catch (InterruptedException | ExecutionException e) {
logger.error("OPC UA服务关闭错误: {}", e.getMessage(), e);
}
});
try {
// 获取OPC UA服务器的数据
opcUAClientService.run(opcUaClient, future);
future.get(5, TimeUnit.SECONDS);
} catch (Throwable t) {
logger.error("OPC UA客户端运行错误: {}", t.getMessage(), t);
future.completeExceptionally(t);
}
} catch (Throwable t) {
logger.error("OPC UA客户端创建错误: {}", t.getMessage(), t);
future.completeExceptionally(t);
}
}
/**
* 创建OPC UA的服务连接对象
*/
private OpcUaClient createClient() throws Exception {
Path securityTempDir = Paths.get(System.getProperty("java.io.tmpdir"), "client", "security");
Files.createDirectories(securityTempDir);
if (!Files.exists(securityTempDir)) {
throw new Exception("unable to create security dir: " + securityTempDir);
}
KeyStoreLoader loader = new KeyStoreLoader().load(securityTempDir);
// 创建OPC UA客户端
return OpcUaClient.create(
opcUAClientService.getEndpointUrl(),
endpoints ->
endpoints.stream()
.filter(opcUAClientService.endpointFilter())
.findFirst(),
configBuilder ->
configBuilder
.setApplicationName(LocalizedText.english("eclipse milo opc-ua client"))
.setApplicationUri("urn:eclipse:milo:client")
.setKeyPair(loader.getClientKeyPair())
.setCertificate(loader.getClientCertificate())
.setCertificateChain(loader.getClientCertificateChain())
// .setCertificateValidator(certificateValidator)
.setIdentityProvider(opcUAClientService.getIdentityProvider())
.setRequestTimeout(uint(5000))
.build()
);
}
}
3.创建opc客户端证书
package com.example.opc;
import lombok.extern.slf4j.Slf4j;
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.Arrays;
import java.util.regex.Pattern;
/**
* @License : (C) Copyright, MaxSense (Shanghai) Technology Co., Ltd.
*
* @file : KeyStoreLoader.java
*
* @desc :
*
* @author : xxx
*
* @date : 2023/8/3 11:45
*
* @version : V1.0
*
*/
@Slf4j
public class KeyStoreLoader {
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 = "milo".toCharArray();
private final Logger logger = LoggerFactory.getLogger(getClass());
private X509Certificate[] clientCertificateChain;
private X509Certificate clientCertificate;
private KeyPair clientKeyPair;
KeyStoreLoader load(Path baseDir) throws Exception {
KeyStore keyStore = KeyStore.getInstance("PKCS12");
Path serverKeyStore = baseDir.resolve("milo-client.pfx");
logger.info("Loading KeyStore at {}", serverKeyStore);
if (!Files.exists(serverKeyStore)) {
keyStore.load(null, PASSWORD);
KeyPair keyPair = SelfSignedCertificateGenerator.generateRsaKeyPair(2048);
SelfSignedCertificateBuilder builder = new SelfSignedCertificateBuilder(keyPair)
.setCommonName("Eclipse Milo Client")
.setOrganization("digitalpetri")
.setOrganizationalUnit("dev")
.setLocalityName("Folsom")
.setStateName("CA")
.setCountryCode("CN")
.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 clientPrivateKey = keyStore.getKey(CLIENT_ALIAS, PASSWORD);
if (clientPrivateKey instanceof PrivateKey) {
clientCertificate = (X509Certificate) keyStore.getCertificate(CLIENT_ALIAS);
clientCertificateChain = Arrays.stream(keyStore.getCertificateChain(CLIENT_ALIAS))
.map(X509Certificate.class::cast)
.toArray(X509Certificate[]::new);
PublicKey serverPublicKey = clientCertificate.getPublicKey();
clientKeyPair = new KeyPair(serverPublicKey, (PrivateKey) clientPrivateKey);
}
return this;
}
X509Certificate getClientCertificate() {
return clientCertificate;
}
public X509Certificate[] getClientCertificateChain() {
return clientCertificateChain;
}
KeyPair getClientKeyPair() {
return clientKeyPair;
}
}
四.opc客户端服务类
package com.example.opc.service;
import org.eclipse.milo.opcua.sdk.client.OpcUaClient;
import org.eclipse.milo.opcua.sdk.client.api.identity.IdentityProvider;
import org.eclipse.milo.opcua.sdk.client.api.identity.UsernameProvider;
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.structured.EndpointDescription;
import java.util.concurrent.CompletableFuture;
import java.util.function.Predicate;
/**
* @License : (C) Copyright, MaxSense (Shanghai) Technology Co., Ltd.
*
* @file : OpcUAClientService.java
*
* @desc :
*
* @author : xxx
*
* @date : 2023/8/3 12:24
*
* @version : V1.0
*
*/
public interface OpcUAClientService {
/**
* OPC UA服务器地址和接口
*/
default String getEndpointUrl() {
return "opc.tcp://127.0.0.1:49320";
}
/**
* 过滤返回的server endpoint
*/
default Predicate<EndpointDescription> endpointFilter() {
return e -> getSecurityPolicy().getUri().equals(e.getSecurityPolicyUri());
}
/**
* 连接服务器的安全策略
* <p>
* opc客户端指定的安全策略必须被包含在opc服务端指定安全策略中
* 否则抛{@link UaException} no endpoint selected
* </p>
* None、Basic128Rsa15、Basic256、Basic256Sha256、Aes128_Sha256_RsaOaep、Aes256_Sha256_RsaPss
*/
default SecurityPolicy getSecurityPolicy() {
return SecurityPolicy.Basic256;
}
/**
* 提供身份验证
* <p>
* username和password需求服务端创建然后提供
* </p>
*/
default IdentityProvider getIdentityProvider() {
return new UsernameProvider("jfy", "123456");
}
/**
* 实际操作服务、由实现类重写实现
*/
void run(OpcUaClient client, CompletableFuture<OpcUaClient> future) throws Exception;
}
五.opc客户端实现类
package com.example.opc.service.impl;
import com.example.opc.service.OpcUAClientService;
import com.google.common.collect.ImmutableList;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.milo.opcua.sdk.client.OpcUaClient;
import org.eclipse.milo.opcua.sdk.client.nodes.UaNode;
import org.eclipse.milo.opcua.sdk.client.nodes.UaVariableNode;
import org.eclipse.milo.opcua.stack.core.Identifiers;
import org.eclipse.milo.opcua.stack.core.UaException;
import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue;
import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId;
import org.eclipse.milo.opcua.stack.core.types.enumerated.ServerState;
import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import static com.example.opc.opcenum.UaNodeEnum.CHANNEL_1;
/**
* @License : (C) Copyright, MaxSense (Shanghai) Technology Co., Ltd.
*
* @file : OpcUAClientServiceImpl.java
*
* @desc :
*
* @author : xxx
*
* @date : 2023/8/3 12:24
*
* @version : V1.0
*
*/
@Slf4j
@Service("OpcUAClientService")
public class OpcUAClientServiceImpl implements OpcUAClientService {
/**
* 覆盖接口的方法,建立和OPC UA的服务
*/
@Override
public void run(OpcUaClient client, CompletableFuture<OpcUaClient> future) throws Exception {
// 同步建立连接
client.connect().get();
// synchronous read request via VariableNode
UaVariableNode node = client.getAddressSpace().getVariableNode(Identifiers.Server_ServerStatus_StartTime);
DataValue value = node.readValue();
log.info("StartTime={}", value.getValue().getValue());
// 异步读取数据
readTagData(client).thenAccept(values -> {
DataValue nodeId_Tag1 = values.get(0);
DataValue nodeId_Tag2 = values.get(1);
System.out.println("#########Tag1=" + nodeId_Tag1.getValue().getValue());
System.out.println("#########Tag2=" + nodeId_Tag2.getValue().getValue());
log.info("State={}", ServerState.from((int) nodeId_Tag1.getStatusCode().getValue()));
log.info("CurrentTime={}", nodeId_Tag1.getServerTime().getJavaDate());
future.complete(client);
});
}
/**
* 读取标签点的数据
*/
private CompletableFuture<List<DataValue>> readTagData(OpcUaClient client) throws UaException {
// 获取节点列表
// Identifiers.RootFolder-->Milo库预定义的根目录
final List<? extends UaNode> nodes = new ArrayList<>();
browseNode(0, "", client, Identifiers.RootFolder, (List<UaNode>) nodes);
NodeId nodeId_Tag1 = new NodeId(2, "通道 1.Device1.Tag1");
NodeId nodeId_Tag2 = new NodeId(2, "通道 1.Device1.Tag2");
List<NodeId> nodeIds = ImmutableList.of(nodeId_Tag1, nodeId_Tag2);
return client.readValues(0.0, TimestampsToReturn.Both, nodeIds);
}
/**
* 遍历树形节点
* @param deep
* @param indent
* @param client
* @param browseRoot
* @param nodes
* @return
*/
private List<? extends UaNode> browseNode(final int deep, final String indent, final OpcUaClient client, final NodeId browseRoot, final List<UaNode> nodes) {
try {
List<? extends UaNode> nodeList = client.getAddressSpace().browseNodes(browseRoot);
for (final UaNode node : nodeList) {
//排除系统性节点,这些系统性节点名称一般都是以"_"开头
if (Objects.requireNonNull(node.getBrowseName().getName()).contains("_")) continue;
// 匹配指定节点的数据(包含对应的子节点)
log.info("{}--{} Node={}", deep, indent, node.getBrowseName().getName());
if (deep == 1 && !CHANNEL_1.getName().equals(node.getBrowseName().getName())) continue;
nodes.add(node);
// recursively browse to children
browseNode(deep + 1, indent + " ", client, node.getNodeId(), nodes);
}
} catch (UaException e) {
log.error("Browsing nodeId={} failed: {}", browseRoot, e.getMessage(), e);
}
return nodes;
}
private CompletableFuture<List<DataValue>> readServerStateAndTime(OpcUaClient client) {
List<NodeId> nodeIds = ImmutableList.of(
Identifiers.Server_ServerStatus_State,
Identifiers.Server_ServerStatus_CurrentTime);
return client.readValues(0.0, TimestampsToReturn.Both, nodeIds);
}
}
四、测试
五、注意点
1、opc服务端更新过配置后需要初始化
2、创建OPC客户端时,下图中的ApplicationUri需和KeyStoreLoader中的保持一致,不然抛异常,
3、url、安全策略、用户名和密码需要配置正确