|
|
|
@ -1,8 +1,11 @@
|
|
|
|
package cn.iocoder.yudao.framework.common.util.opc;
|
|
|
|
package cn.iocoder.yudao.framework.common.util.opc;
|
|
|
|
|
|
|
|
|
|
|
|
import org.eclipse.milo.opcua.sdk.client.OpcUaClient;
|
|
|
|
import org.eclipse.milo.opcua.sdk.client.OpcUaClient;
|
|
|
|
|
|
|
|
import org.eclipse.milo.opcua.sdk.client.api.config.OpcUaClientConfig;
|
|
|
|
|
|
|
|
import org.eclipse.milo.opcua.sdk.client.api.config.OpcUaClientConfigBuilder;
|
|
|
|
import org.eclipse.milo.opcua.sdk.client.api.identity.AnonymousProvider;
|
|
|
|
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.client.api.identity.UsernameProvider;
|
|
|
|
|
|
|
|
import org.eclipse.milo.opcua.stack.client.DiscoveryClient;
|
|
|
|
import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue;
|
|
|
|
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.builtin.NodeId;
|
|
|
|
import org.eclipse.milo.opcua.stack.core.types.builtin.Variant;
|
|
|
|
import org.eclipse.milo.opcua.stack.core.types.builtin.Variant;
|
|
|
|
@ -10,7 +13,11 @@ import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger;
|
|
|
|
import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn;
|
|
|
|
import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn;
|
|
|
|
import org.eclipse.milo.opcua.stack.core.types.structured.EndpointDescription;
|
|
|
|
import org.eclipse.milo.opcua.stack.core.types.structured.EndpointDescription;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import java.net.InetAddress;
|
|
|
|
|
|
|
|
import java.util.List;
|
|
|
|
|
|
|
|
import java.util.Optional;
|
|
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
|
|
|
|
import java.util.function.Function;
|
|
|
|
import java.util.function.Predicate;
|
|
|
|
import java.util.function.Predicate;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
@ -37,9 +44,6 @@ public class OpcUtils {
|
|
|
|
return connect(url, null, null, timeoutSeconds);
|
|
|
|
return connect(url, null, null, timeoutSeconds);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 连接OPC UA服务器(支持匿名和用户名密码认证)
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
public static boolean connect(String url, String username, String password, int timeoutSeconds) {
|
|
|
|
public static boolean connect(String url, String username, String password, int timeoutSeconds) {
|
|
|
|
if (isConnected) {
|
|
|
|
if (isConnected) {
|
|
|
|
System.out.println(LOG_PREFIX + "客户端已连接,无需重复连接");
|
|
|
|
System.out.println(LOG_PREFIX + "客户端已连接,无需重复连接");
|
|
|
|
@ -51,24 +55,105 @@ public class OpcUtils {
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
System.out.println(LOG_PREFIX + "正在连接到OPC UA服务器: " + url);
|
|
|
|
System.out.println(LOG_PREFIX + "正在连接到OPC UA服务器: " + url);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 提取主机和端口
|
|
|
|
|
|
|
|
final String targetHost = extractHostFromUrl(url);
|
|
|
|
|
|
|
|
final int targetPort = extractPortFromUrl(url);
|
|
|
|
|
|
|
|
final String path = extractPathFromUrl(url);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
System.out.println(LOG_PREFIX + "目标主机: " + targetHost + ", 端口: " + targetPort + ", 路径: " + path);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 将主机名解析为IP地址
|
|
|
|
|
|
|
|
final String ipAddress = resolveToIpAddress(targetHost);
|
|
|
|
|
|
|
|
System.out.println(LOG_PREFIX + "解析为IP地址: " + ipAddress);
|
|
|
|
|
|
|
|
|
|
|
|
if (username != null && password != null && !username.isEmpty()) {
|
|
|
|
if (username != null && password != null && !username.isEmpty()) {
|
|
|
|
System.out.println(LOG_PREFIX + "使用用户名密码认证: " + username);
|
|
|
|
System.out.println(LOG_PREFIX + "使用用户名密码认证: " + username);
|
|
|
|
|
|
|
|
|
|
|
|
Predicate<EndpointDescription> endpointPredicate = e ->
|
|
|
|
// 用户名密码认证
|
|
|
|
"http://opcfoundation.org/UA/SecurityPolicy#None".equals(e.getSecurityPolicyUri());
|
|
|
|
client = OpcUaClient.create(url, endpoints -> {
|
|
|
|
|
|
|
|
if (endpoints == null || endpoints.isEmpty()) {
|
|
|
|
client = OpcUaClient.create(url, endpoints ->
|
|
|
|
System.err.println(LOG_PREFIX + "服务器未返回任何端点");
|
|
|
|
endpoints.stream()
|
|
|
|
return Optional.empty();
|
|
|
|
.filter(endpointPredicate)
|
|
|
|
}
|
|
|
|
.findFirst(),
|
|
|
|
|
|
|
|
configBuilder -> configBuilder
|
|
|
|
System.out.println(LOG_PREFIX + "发现端点数量: " + endpoints.size());
|
|
|
|
.setIdentityProvider(new UsernameProvider(username, password))
|
|
|
|
|
|
|
|
.setRequestTimeout(UInteger.valueOf(timeoutSeconds * 1000L))
|
|
|
|
// 查找无安全策略的端点
|
|
|
|
.build()
|
|
|
|
for (EndpointDescription endpoint : endpoints) {
|
|
|
|
);
|
|
|
|
String endpointUrl = endpoint.getEndpointUrl();
|
|
|
|
|
|
|
|
System.out.println(LOG_PREFIX + "检查端点: " + endpointUrl +
|
|
|
|
|
|
|
|
" | 安全策略: " + endpoint.getSecurityPolicyUri());
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if ("http://opcfoundation.org/UA/SecurityPolicy#None".equals(endpoint.getSecurityPolicyUri())) {
|
|
|
|
|
|
|
|
// 修正端点URL,强制使用IP地址
|
|
|
|
|
|
|
|
String correctedUrl = forceIpAddressEndpoint(endpointUrl, ipAddress, targetPort, path);
|
|
|
|
|
|
|
|
System.out.println(LOG_PREFIX + "强制使用IP地址: " + endpointUrl + " -> " + correctedUrl);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return Optional.of(new EndpointDescription(
|
|
|
|
|
|
|
|
correctedUrl,
|
|
|
|
|
|
|
|
endpoint.getServer(),
|
|
|
|
|
|
|
|
endpoint.getServerCertificate(),
|
|
|
|
|
|
|
|
endpoint.getSecurityMode(),
|
|
|
|
|
|
|
|
endpoint.getSecurityPolicyUri(),
|
|
|
|
|
|
|
|
endpoint.getUserIdentityTokens(),
|
|
|
|
|
|
|
|
endpoint.getTransportProfileUri(),
|
|
|
|
|
|
|
|
endpoint.getSecurityLevel()
|
|
|
|
|
|
|
|
));
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
System.err.println(LOG_PREFIX + "未找到无安全策略的端点");
|
|
|
|
|
|
|
|
return Optional.empty();
|
|
|
|
|
|
|
|
}, configBuilder -> configBuilder
|
|
|
|
|
|
|
|
.setIdentityProvider(new UsernameProvider(username, password))
|
|
|
|
|
|
|
|
.setRequestTimeout(UInteger.valueOf(timeoutSeconds * 1000L))
|
|
|
|
|
|
|
|
.build());
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
System.out.println(LOG_PREFIX + "使用匿名认证");
|
|
|
|
System.out.println(LOG_PREFIX + "使用匿名认证");
|
|
|
|
client = OpcUaClient.create(url);
|
|
|
|
|
|
|
|
|
|
|
|
// 对于匿名认证,手动发现端点并修正
|
|
|
|
|
|
|
|
List<EndpointDescription> endpoints = DiscoveryClient.getEndpoints(url).get(timeoutSeconds, TimeUnit.SECONDS);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (endpoints == null || endpoints.isEmpty()) {
|
|
|
|
|
|
|
|
System.err.println(LOG_PREFIX + "服务器未返回任何端点");
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 查找无安全策略的端点
|
|
|
|
|
|
|
|
Optional<EndpointDescription> selectedEndpoint = Optional.empty();
|
|
|
|
|
|
|
|
for (EndpointDescription endpoint : endpoints) {
|
|
|
|
|
|
|
|
if ("http://opcfoundation.org/UA/SecurityPolicy#None".equals(endpoint.getSecurityPolicyUri())) {
|
|
|
|
|
|
|
|
selectedEndpoint = Optional.of(endpoint);
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!selectedEndpoint.isPresent()) {
|
|
|
|
|
|
|
|
System.err.println(LOG_PREFIX + "未找到无安全策略的端点");
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
EndpointDescription endpoint = selectedEndpoint.get();
|
|
|
|
|
|
|
|
String correctedUrl = forceIpAddressEndpoint(endpoint.getEndpointUrl(), ipAddress, targetPort, path);
|
|
|
|
|
|
|
|
System.out.println(LOG_PREFIX + "强制使用IP地址: " + endpoint.getEndpointUrl() + " -> " + correctedUrl);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
EndpointDescription correctedEndpoint = new EndpointDescription(
|
|
|
|
|
|
|
|
correctedUrl,
|
|
|
|
|
|
|
|
endpoint.getServer(),
|
|
|
|
|
|
|
|
endpoint.getServerCertificate(),
|
|
|
|
|
|
|
|
endpoint.getSecurityMode(),
|
|
|
|
|
|
|
|
endpoint.getSecurityPolicyUri(),
|
|
|
|
|
|
|
|
endpoint.getUserIdentityTokens(),
|
|
|
|
|
|
|
|
endpoint.getTransportProfileUri(),
|
|
|
|
|
|
|
|
endpoint.getSecurityLevel()
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
OpcUaClientConfigBuilder configBuilder = OpcUaClientConfig.builder()
|
|
|
|
|
|
|
|
.setEndpoint(correctedEndpoint)
|
|
|
|
|
|
|
|
.setIdentityProvider(new AnonymousProvider())
|
|
|
|
|
|
|
|
.setRequestTimeout(UInteger.valueOf(timeoutSeconds * 1000L));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
client = OpcUaClient.create(configBuilder.build());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
client.connect().get(timeoutSeconds, TimeUnit.SECONDS);
|
|
|
|
client.connect().get(timeoutSeconds, TimeUnit.SECONDS);
|
|
|
|
@ -84,10 +169,321 @@ public class OpcUtils {
|
|
|
|
|
|
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
} catch (Exception e) {
|
|
|
|
System.err.println(LOG_PREFIX + "连接失败: " + e.getMessage());
|
|
|
|
System.err.println(LOG_PREFIX + "连接失败: " + e.getMessage());
|
|
|
|
|
|
|
|
e.printStackTrace();
|
|
|
|
return false;
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 将主机名解析为IP地址
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
private static String resolveToIpAddress(String host) {
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
// 如果已经是IP地址,直接返回
|
|
|
|
|
|
|
|
if (host.matches("\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}")) {
|
|
|
|
|
|
|
|
return host;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 尝试解析主机名
|
|
|
|
|
|
|
|
InetAddress address = InetAddress.getByName(host);
|
|
|
|
|
|
|
|
return address.getHostAddress();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
|
|
|
System.err.println(LOG_PREFIX + "解析主机名失败: " + host + ", 错误: " + e.getMessage());
|
|
|
|
|
|
|
|
return host; // 返回原始值
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 强制使用IP地址构建端点URL
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
private static String forceIpAddressEndpoint(String endpointUrl, String ipAddress, int defaultPort, String defaultPath) {
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
// 提取端口和路径
|
|
|
|
|
|
|
|
int port = extractPortFromUrl(endpointUrl);
|
|
|
|
|
|
|
|
if (port <= 0) {
|
|
|
|
|
|
|
|
port = defaultPort;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
String path = extractPathFromUrl(endpointUrl);
|
|
|
|
|
|
|
|
if (path.isEmpty()) {
|
|
|
|
|
|
|
|
path = defaultPath;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return String.format("opc.tcp://%s:%d%s", ipAddress, port, path);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
|
|
|
System.err.println(LOG_PREFIX + "构建IP端点URL失败: " + e.getMessage());
|
|
|
|
|
|
|
|
return endpointUrl;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 从URL中提取主机地址
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
private static String extractHostFromUrl(String url) {
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
// 格式: opc.tcp://host:port/path
|
|
|
|
|
|
|
|
if (url == null || url.trim().isEmpty()) {
|
|
|
|
|
|
|
|
return "localhost";
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
String withoutProtocol = url.replace("opc.tcp://", "");
|
|
|
|
|
|
|
|
// 处理IPv6地址
|
|
|
|
|
|
|
|
if (withoutProtocol.startsWith("[")) {
|
|
|
|
|
|
|
|
// IPv6地址格式: [2001:db8::1]:4840/path
|
|
|
|
|
|
|
|
int closeBracket = withoutProtocol.indexOf("]");
|
|
|
|
|
|
|
|
if (closeBracket > 0) {
|
|
|
|
|
|
|
|
String host = withoutProtocol.substring(1, closeBracket);
|
|
|
|
|
|
|
|
return host;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 普通IPv4或主机名
|
|
|
|
|
|
|
|
String[] parts = withoutProtocol.split("[:/]");
|
|
|
|
|
|
|
|
if (parts.length > 0 && !parts[0].isEmpty()) {
|
|
|
|
|
|
|
|
return parts[0];
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
|
|
|
System.err.println(LOG_PREFIX + "提取主机地址失败: " + e.getMessage());
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return "localhost";
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 从URL中提取端口
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
private static int extractPortFromUrl(String url) {
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
if (url == null || url.trim().isEmpty()) {
|
|
|
|
|
|
|
|
return 4840; // 默认OPC UA端口
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
String withoutProtocol = url.replace("opc.tcp://", "");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 查找端口号
|
|
|
|
|
|
|
|
int portStart = -1;
|
|
|
|
|
|
|
|
int portEnd = -1;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (withoutProtocol.startsWith("[")) {
|
|
|
|
|
|
|
|
// IPv6地址: [2001:db8::1]:4840/path
|
|
|
|
|
|
|
|
int closeBracket = withoutProtocol.indexOf("]");
|
|
|
|
|
|
|
|
if (closeBracket > 0) {
|
|
|
|
|
|
|
|
portStart = withoutProtocol.indexOf(":", closeBracket);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
// IPv4或主机名: host:4840/path
|
|
|
|
|
|
|
|
int firstColon = withoutProtocol.indexOf(":");
|
|
|
|
|
|
|
|
if (firstColon > 0) {
|
|
|
|
|
|
|
|
portStart = firstColon;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (portStart > 0) {
|
|
|
|
|
|
|
|
portStart++; // 跳过冒号
|
|
|
|
|
|
|
|
portEnd = withoutProtocol.indexOf("/", portStart);
|
|
|
|
|
|
|
|
if (portEnd < 0) {
|
|
|
|
|
|
|
|
portEnd = withoutProtocol.length();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
String portStr = withoutProtocol.substring(portStart, portEnd);
|
|
|
|
|
|
|
|
if (portStr.matches("\\d+")) {
|
|
|
|
|
|
|
|
return Integer.parseInt(portStr);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
|
|
|
System.err.println(LOG_PREFIX + "提取端口失败: " + e.getMessage());
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return 4840; // 默认OPC UA端口
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 从URL中提取路径
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
private static String extractPathFromUrl(String url) {
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
if (url == null || url.trim().isEmpty()) {
|
|
|
|
|
|
|
|
return "";
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
String withoutProtocol = url.replace("opc.tcp://", "");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 查找路径开始位置
|
|
|
|
|
|
|
|
int pathStart = -1;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (withoutProtocol.startsWith("[")) {
|
|
|
|
|
|
|
|
// IPv6地址: [2001:db8::1]:4840/path
|
|
|
|
|
|
|
|
int closeBracket = withoutProtocol.indexOf("]");
|
|
|
|
|
|
|
|
if (closeBracket > 0) {
|
|
|
|
|
|
|
|
// 检查是否有端口
|
|
|
|
|
|
|
|
int colonAfterBracket = withoutProtocol.indexOf(":", closeBracket);
|
|
|
|
|
|
|
|
if (colonAfterBracket > 0) {
|
|
|
|
|
|
|
|
// 有端口: [2001:db8::1]:4840/path
|
|
|
|
|
|
|
|
int slashAfterPort = withoutProtocol.indexOf("/", colonAfterBracket);
|
|
|
|
|
|
|
|
if (slashAfterPort > 0) {
|
|
|
|
|
|
|
|
return withoutProtocol.substring(slashAfterPort);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
// 无端口: [2001:db8::1]/path
|
|
|
|
|
|
|
|
int slashAfterBracket = withoutProtocol.indexOf("/", closeBracket);
|
|
|
|
|
|
|
|
if (slashAfterBracket > 0) {
|
|
|
|
|
|
|
|
return withoutProtocol.substring(slashAfterBracket);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
// IPv4或主机名
|
|
|
|
|
|
|
|
// 先找主机名结束位置
|
|
|
|
|
|
|
|
int hostEnd = withoutProtocol.indexOf(":");
|
|
|
|
|
|
|
|
if (hostEnd < 0) {
|
|
|
|
|
|
|
|
// 无端口: host/path
|
|
|
|
|
|
|
|
hostEnd = withoutProtocol.indexOf("/");
|
|
|
|
|
|
|
|
if (hostEnd > 0) {
|
|
|
|
|
|
|
|
return withoutProtocol.substring(hostEnd);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
// 有端口: host:port/path
|
|
|
|
|
|
|
|
int portEnd = withoutProtocol.indexOf("/", hostEnd);
|
|
|
|
|
|
|
|
if (portEnd > 0) {
|
|
|
|
|
|
|
|
return withoutProtocol.substring(portEnd);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
|
|
|
System.err.println(LOG_PREFIX + "提取路径失败: " + e.getMessage());
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return "";
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 从URL中提取协议
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
private static String extractProtocolFromUrl(String url) {
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
if (url == null || url.trim().isEmpty()) {
|
|
|
|
|
|
|
|
return "opc.tcp";
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
int protocolEnd = url.indexOf("://");
|
|
|
|
|
|
|
|
if (protocolEnd > 0) {
|
|
|
|
|
|
|
|
return url.substring(0, protocolEnd);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
|
|
|
System.err.println(LOG_PREFIX + "提取协议失败: " + e.getMessage());
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return "opc.tcp";
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 构建完整的URL
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
private static String buildUrl(String protocol, String host, int port, String path) {
|
|
|
|
|
|
|
|
StringBuilder url = new StringBuilder();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (protocol == null || protocol.isEmpty()) {
|
|
|
|
|
|
|
|
protocol = "opc.tcp";
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
url.append(protocol).append("://");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 处理IPv6地址
|
|
|
|
|
|
|
|
if (host.contains(":")) {
|
|
|
|
|
|
|
|
url.append("[").append(host).append("]");
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
url.append(host);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (port > 0) {
|
|
|
|
|
|
|
|
url.append(":").append(port);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (path != null && !path.isEmpty()) {
|
|
|
|
|
|
|
|
if (!path.startsWith("/")) {
|
|
|
|
|
|
|
|
url.append("/");
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
url.append(path);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return url.toString();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 修正端点URL,强制使用指定的主机和端口
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
private static String correctEndpointUrl(String endpointUrl, String targetHost, int targetPort) {
|
|
|
|
|
|
|
|
if (endpointUrl == null || endpointUrl.trim().isEmpty()) {
|
|
|
|
|
|
|
|
return endpointUrl;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
// 提取原URL的各个部分
|
|
|
|
|
|
|
|
String protocol = extractProtocolFromUrl(endpointUrl);
|
|
|
|
|
|
|
|
String originalHost = extractHostFromUrl(endpointUrl);
|
|
|
|
|
|
|
|
int originalPort = extractPortFromUrl(endpointUrl);
|
|
|
|
|
|
|
|
String path = extractPathFromUrl(endpointUrl);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 决定使用哪个主机
|
|
|
|
|
|
|
|
String useHost = targetHost;
|
|
|
|
|
|
|
|
int usePort = (targetPort > 0) ? targetPort : originalPort;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 如果原端口无效,使用默认端口
|
|
|
|
|
|
|
|
if (usePort <= 0) {
|
|
|
|
|
|
|
|
usePort = 4840;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 构建修正后的URL
|
|
|
|
|
|
|
|
return buildUrl(protocol, useHost, usePort, path);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
|
|
|
System.err.println(LOG_PREFIX + "修正端点URL失败: " + e.getMessage());
|
|
|
|
|
|
|
|
return endpointUrl;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 检查是否为本地地址
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
private static boolean isLocalAddress(String url) {
|
|
|
|
|
|
|
|
if (url == null) return false;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return url.contains("127.0.0.1") ||
|
|
|
|
|
|
|
|
url.contains("localhost") ||
|
|
|
|
|
|
|
|
url.contains("127.0.1.1") ||
|
|
|
|
|
|
|
|
url.contains("[::1]") ||
|
|
|
|
|
|
|
|
url.startsWith("opc.tcp://localhost") ||
|
|
|
|
|
|
|
|
url.startsWith("opc.tcp://127.");
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 修正端点URL中的本地地址
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
private static String correctEndpointUrl(String endpointUrl, String targetHost) {
|
|
|
|
|
|
|
|
if (endpointUrl == null) return endpointUrl;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 替换各种本地地址表示形式
|
|
|
|
|
|
|
|
String corrected = endpointUrl
|
|
|
|
|
|
|
|
.replace("127.0.0.1", targetHost)
|
|
|
|
|
|
|
|
.replace("localhost", targetHost)
|
|
|
|
|
|
|
|
.replace("127.0.1.1", targetHost)
|
|
|
|
|
|
|
|
.replace("[::1]", targetHost);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 如果还有localhost:端口的形式
|
|
|
|
|
|
|
|
if (corrected.contains("localhost:")) {
|
|
|
|
|
|
|
|
corrected = corrected.replace("localhost", targetHost);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return corrected;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
* 断开连接
|
|
|
|
* 断开连接
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
|