
一、项目准备
1.1 NuGet包选择
C#中常用的OPC UA客户端库:
| 库 | 说明 | 推荐度 |
|---|---|---|
| OPCFoundation.NetStandard.Opc.Ua | 官方开源库 | ⭐⭐⭐⭐⭐ |
| Opc.Ua.Client | 老版本库 | ⭐⭐⭐ |
推荐使用官方库,功能完整且持续更新。
1.2 安装依赖
bash
# 在项目目录执行
dotnet add package OPCFoundation.NetStandard.Opc.Ua
dotnet add package OPCFoundation.NetStandard.Opc.Ua.Client
1.3 创建项目
bash
dotnet new console -n OpcUaDemo
cd OpcUaDemo
dotnet add package OPCFoundation.NetStandard.Opc.Ua
dotnet add package OPCFoundation.NetStandard.Opc.Ua.Client
二、基础连接
2.1 发现服务器端点
csharp
using Opc.Ua;
using Opc.Ua.Client;
using Opc.Ua.Configuration;
using System.Net;
class Program
{
static async Task Main(string[] args)
{
// 服务器地址
string serverUrl = "opc.tcp://localhost:4840";
// 创建应用配置
var application = new ApplicationInstance
{
ApplicationName = "OPC UA Client Demo",
ApplicationType = ApplicationType.Client
};
// 加载或创建证书
var config = await application.Build(
"urn:OpcUaDemo:Client",
"http://opcfoundation.org/UA/DeviceNode"
).Create().Result;
// 发现端点
var selectedEndpoint = CoreClientUtils.SelectEndpoint(serverUrl, false);
Console.WriteLine($"服务器地址: {selectedEndpoint.EndpointUrl}");
Console.WriteLine($"安全策略: {selectedEndpoint.SecurityPolicyUri}");
}
}
2.2 完整连接代码
csharp
using Opc.Ua;
using Opc.Ua.Client;
using Opc.Ua.Configuration;
using System.Security.Cryptography.X509Certificates;
class OpcUaClient : IDisposable
{
private ApplicationConfiguration _configuration = null!;
private Session? _session;
public Session? Session => _session;
/// <summary>
/// 连接到OPC UA服务器
/// </summary>
public async Task<bool> Connect(string serverUrl, string username = "", string password = "")
{
try
{
// 1. 创建应用配置
_configuration = await CreateConfiguration();
// 2. 选择端点(选择无加密用于测试)
var endpoint = CoreClientUtils.SelectEndpoint(serverUrl, false);
// 3. 创建通道
var channel = new SessionChannel(
_configuration,
endpoint.Description,
endpoint.EndpointDescription,
_configuration.SecurityConfiguration,
new Dictionary<string, thumbprint>(),
60000);
// 4. 创建会话(匿名登录)
var session = await channel.CreateSessionAsync(
username, // 用户名
password, // 密码
"zh-CN"); // 语言
if (session != null)
{
_session = session;
Console.WriteLine("连接成功!");
return true;
}
}
catch (Exception ex)
{
Console.WriteLine($"连接失败: {ex.Message}");
}
return false;
}
private async Task CreateConfiguration()
{
var certificate = new CertificateIdentifier
{
StoreType = CertificateStoreType.X509Store,
StoreName = "My",
StoreLocation = Location.CurrentUser
};
_configuration = new ApplicationConfiguration
{
ApplicationName = "OPC UA Client",
ApplicationType = ApplicationType.Client,
SecurityConfiguration = new SecurityConfiguration
{
ApplicationCertificate = certificate,
AutoAcceptUntrustedCertificates = true // 测试环境
},
ClientConfiguration = new ClientConfiguration
{
DefaultSessionTimeout = 60000
}
};
await _configuration.Validate();
}
public void Disconnect()
{
_session?.Close();
_session?.Dispose();
_session = null;
}
public void Dispose()
{
Disconnect();
}
}
三、浏览节点
3.1 浏览地址空间
csharp
/// <summary>
/// 浏览服务器地址空间
/// </summary>
public void BrowseAddressSpace()
{
if (_session == null)
{
Console.WriteLine("请先连接服务器");
return;
}
// 浏览根节点
var references = _session.Browse(null, null, ObjectIds.RootFolder, 0u);
Console.WriteLine("根节点下的对象:");
foreach (var reference in references)
{
Console.WriteLine($" {reference.DisplayName}: {reference.NodeId}");
}
}
/// <summary>
/// 递归浏览节点树
/// </summary>
public void BrowseTree(ExpandedNodeId parentId, int indent = 0)
{
if (_session == null) return;
var references = _session.Browse(null, null, parentId, 0u);
foreach (var reference in references)
{
// 打印节点信息
var prefix = new string(' ', indent * 2);
Console.WriteLine($"{prefix}{reference.BrowseName}: {reference.NodeId}");
// 递归浏览子节点
if (reference.NodeId != parentId)
{
BrowseTree(reference.NodeId, indent + 1);
}
}
}
3.2 浏览结果说明
Objects:
Server:
ServerStatus
NamespaceArray
ServerCapabilities
Device1: ← 你的设备
Variables:
Temperature: Double ← 温度值
Pressure: Double ← 压力值
Status: Boolean ← 运行状态
Methods:
Start(): Void ← 启动方法
Stop(): Void ← 停止方法
四、读写数据
4.1 读取单个值
csharp
/// <summary>
/// 读取变量值
/// </summary>
public void ReadValue(string nodeId)
{
if (_session == null) return;
// 读取值
var value = _session.ReadValue(nodeId);
Console.WriteLine($"节点: {nodeId}");
Console.WriteLine($"值: {value}");
Console.WriteLine($"类型: {value?.GetType().Name}");
}
/// <summary>
/// 批量读取
/// </summary>
public void ReadMultipleValues(string[] nodeIds)
{
if (_session == null) return;
// 准备读取请求
var nodesToRead = nodeIds.Select(id =>
new ReadValueId
{
NodeId = id,
AttributeId = Attributes.Value
}).ToArray();
// 执行批量读取
var values = _session.Read(null, 0, TimestampsToReturn.Neither, nodesToRead,
out var results, out var diagnosticInfos);
// 输出结果
for (int i = 0; i < nodeIds.Length; i++)
{
Console.WriteLine($"{nodeIds[i]}: {values[i].Value}");
}
}
4.2 写入值
csharp
/// <summary>
/// 写入变量值
/// </summary>
public bool WriteValue(string nodeId, object value)
{
if (_session == null) return false;
try
{
// 准备写入请求
var nodesToWrite = new WriteValue[]
{
new WriteValue
{
NodeId = nodeId,
AttributeId = Attributes.Value,
Value = new DataValue(new Variant(value))
}
};
// 执行写入
var results = _session.Write(null, nodesToWrite, out var errors);
if (errors[0] == StatusCodes.Good)
{
Console.WriteLine($"写入成功: {nodeId} = {value}");
return true;
}
else
{
Console.WriteLine($"写入失败: {errors[0]}");
return false;
}
}
catch (Exception ex)
{
Console.WriteLine($"写入异常: {ex.Message}");
return false;
}
}
五、订阅数据
5.1 监控数据变化
csharp
using Opc.Ua.Client.Subscriptions;
class DataMonitor
{
private Session _session;
private Subscription _subscription;
private List<MonitoredItem> _monitorItems = new List<MonitoredItem>();
public DataMonitor(Session session)
{
_session = session;
// 创建订阅
_subscription = new Subscription(_session.DefaultSubscription)
{
PublishingInterval = 1000, // 1秒
PublishingEnabled = true,
LifetimeCount = 100,
MaxNotificationsPerPublish = 1000
};
_session.AddSubscription(_subscription);
_subscription.Create();
}
/// <summary>
/// 添加监控项
/// </summary>
public void AddMonitoredItem(string nodeId, Action? dataChangedHandler = null)
{
var monitoredItem = new MonitoredItem(_subscription.DefaultItem)
{
StartNodeId = nodeId,
SamplingInterval = 1000,
QueueSize = 1,
DiscardOldest = true
};
// 数据变化回调
monitoredItem.Notification += (item, args) =>
{
var value = args.NotificationValue as MonitoredItemNotification;
if (value != null)
{
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] {nodeId} = {value.Value.WrappedValue}");
dataChangedHandler?.Invoke();
}
};
_subscription.AddItem(monitoredItem);
_subscription.ApplyChanges();
_monitorItems.Add(monitoredItem);
}
/// <summary>
/// 移除监控项
/// </summary>
public void RemoveMonitoredItem(string nodeId)
{
var item = _monitorItems.FirstOrDefault(m => m.StartNodeId == nodeId);
if (item != null)
{
_subscription.RemoveItem(item);
_subscription.ApplyChanges();
_monitorItems.Remove(item);
}
}
/// <summary>
/// 停止监控
/// </summary>
public void Stop()
{
_subscription.Delete(true);
_session.RemoveSubscription(_subscription);
}
}
5.2 使用示例
csharp
// 连接服务器
var client = new OpcUaClient();
await client.Connect("opc.tcp://localhost:4840");
// 创建监控器
var monitor = new DataMonitor(client.Session!);
// 添加监控项
monitor.AddMonitoredItem("ns=2;s=Temperature", () =>
{
// 值变化时的处理
});
monitor.AddMonitoredItem("ns=2;s=Pressure", () =>
{
// 值变化时的处理
});
Console.WriteLine("监控中,按任意键退出...");
Console.ReadKey();
monitor.Stop();
client.Disconnect();
六、调用方法
6.1 调用服务器方法
csharp
/// <summary>
/// 调用方法
/// </summary>
public void CallMethod(string objectId, string methodId, params object[] inputArgs)
{
if (_session == null) return;
try
{
// 调用方法
var result = _session.Call(
objectId, // 对象节点ID
methodId, // 方法节点ID
inputArgs); // 输入参数
if (result.StatusCode.IsGood())
{
Console.WriteLine("方法调用成功");
// 输出返回值
if (result.OutputArguments != null)
{
foreach (var arg in result.OutputArguments)
{
Console.WriteLine($"返回值: {arg}");
}
}
}
else
{
Console.WriteLine($"方法调用失败: {result.StatusCode}");
}
}
catch (Exception ex)
{
Console.WriteLine($"调用异常: {ex.Message}");
}
}
6.2 调用示例
csharp
// 调用启动方法
client.CallMethod(
"ns=2;s=Device1", // 设备对象
"ns=2;s=Device1.Start", // 启动方法
100, // 参数:延迟时间
"Production" // 参数:生产线名称
);
// 调用停止方法
client.CallMethod(
"ns=2;s=Device1",
"ns=2;s=Device1.Stop"
);
七、异常处理
7.1 常见异常
csharp
public class OpcUaException : Exception
{
public StatusCode StatusCode { get; }
}
try
{
// OPC UA操作
client.Connect(serverUrl);
}
catch (Opc.Ua.ServiceResultException ex)
{
switch (ex.StatusCode)
{
case StatusCodes.BadConnectionClosed:
Console.WriteLine("连接已断开");
// 尝试重连
break;
case StatusCodes.BadTimeout:
Console.WriteLine("操作超时");
break;
case StatusCodes.BadUserAccessDenied:
Console.WriteLine("无权限访问");
break;
default:
Console.WriteLine($"OPC UA错误: {ex.StatusCode}");
break;
}
}
7.2 重连机制
csharp
public class OpcUaClientWithReconnect : IDisposable
{
private OpcUaClient _client;
private SessionReconnectHandler? _reconnectHandler;
public OpcUaClientWithReconnect()
{
_client = new OpcUaClient();
}
public async Task ConnectWithReconnect(string serverUrl)
{
await _client.Connect(serverUrl);
EnableReconnect();
}
public void EnableReconnect()
{
if (_client.Session == null) return;
// 注册重连处理器
_reconnectHandler = new SessionReconnectHandler();
_reconnectHandler.BeginReconnect(
_client.Session,
5000, // 重连间隔
(sender, args) =>
{
// 重连成功
Console.WriteLine("重连成功!");
});
}
public void Dispose()
{
_reconnectHandler?.Dispose();
_client.Dispose();
}
}
八、完整示例
csharp
using Opc.Ua;
using Opc.Ua.Client;
using Opc.Ua.Client.Subscriptions;
class Program
{
static async Task Main(string[] args)
{
var serverUrl = "opc.tcp://localhost:4840";
using var client = new DemoClient();
// 1. 连接
Console.WriteLine("正在连接...");
if (!await client.ConnectAsync(serverUrl))
{
Console.WriteLine("连接失败!");
return;
}
// 2. 浏览
Console.WriteLine("\n浏览地址空间:");
client.BrowseAddressSpace();
// 3. 读取
Console.WriteLine("\n读取数据:");
client.ReadValue("ns=2;s=Temperature");
// 4. 写入
Console.WriteLine("\n写入数据:");
client.WriteValue("ns=2;s=SetPoint", 100.0);
// 5. 订阅
Console.WriteLine("\n开始监控(Ctrl+C退出):");
client.StartMonitoring("ns=2;s=Temperature");
Console.ReadLine();
}
}
class DemoClient : IDisposable
{
private OpcUaClient _client = new OpcUaClient();
private DataMonitor? _monitor;
public Session? Session => _client.Session;
public async Task<bool> ConnectAsync(string serverUrl)
=> await _client.Connect(serverUrl);
public void BrowseAddressSpace() => _client.BrowseAddressSpace();
public void ReadValue(string nodeId) => _client.ReadValue(nodeId);
public bool WriteValue(string nodeId, object value)
=> _client.WriteValue(nodeId, value);
public void StartMonitoring(string nodeId)
{
if (Session == null) return;
_monitor = new DataMonitor(Session);
_monitor.AddMonitoredItem(nodeId);
}
public void Disconnect() => _client.Disconnect();
public void Dispose()
{
_monitor?.Stop();
_client.Dispose();
}
}
九、总结
本文要点:
- 使用OPCFoundation.NetStandard.Opc.Ua库开发客户端
- 连接服务器需要配置证书和安全策略
- Browse方法浏览地址空间,Read/Write读写数据
- Subscription实现数据变化监控
- Call方法调用服务器端方法
注意事项:
- 生产环境务必配置正确的安全策略和证书
- 实现重连机制应对网络波动
- 注意订阅项数量,避免资源耗尽