C# OPC UA客户端开发实战

一、项目准备

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方法调用服务器端方法

注意事项:

  • 生产环境务必配置正确的安全策略和证书
  • 实现重连机制应对网络波动
  • 注意订阅项数量,避免资源耗尽
相关推荐
十五年专注C++开发1 小时前
Qt程序设计涉及到的开发软件
开发语言·c++·qt
asdzx671 小时前
使用 C# 从 URL 下载 Word 文档
开发语言·c#·word
大萌神Nagato2 小时前
python 包管理器uv
开发语言·python·uv
humcomm2 小时前
AI 编程时代-全栈开发技术栈解析
开发语言·人工智能
人道领域2 小时前
【黑马点评日记】社交平台用户关注功能全解析Feed流相关操作
java·开发语言·数据库·redis·python
海域云-罗鹏2 小时前
豆包开启付费订阅,想白嫖越来越难了,企业不如部署自己的算力服务器
服务器·人工智能·github
德迅云安全-小潘2 小时前
APP运营服务器配置全攻略:从选型到网络安全,你需要知道的一切
运维·服务器·web安全
xiaoshuaishuai82 小时前
C# DeepSeek V4 与 V3对比
开发语言·c#·量子计算
shehuiyuelaiyuehao2 小时前
算法18,二分查找
java·开发语言·算法