OPC UA 通讯开发笔记 - 基于本地dll文件

本文档基于自写的 OPC_Test_Pro 项目编写,讲解 OPC UA 通讯的原理、句柄关系及每个方法的实现细节。

本文只用于对该项目的技术分析。项目为自用项目,源码不对外开源。

目录


代码概述

项目信息

  • 项目名称: Related_OPCLib
  • 命名空间: Related_OPCLib
  • 主类: OPCUALib
  • OPC UA版本: 1.5.374.0
  • 框架: .NET Framework 4.8

功能特性

✅ 同步/异步连接

✅ 数据读写(单点/批量)

✅ 数据订阅与监控

✅ 自动重连机制

✅ 用户名密码认证

✅ 匿名登录支持


核心类结构

类关系图

复制代码
OPCUALib (核心类)
├── 字段成员
│   ├── ConfiguredEndpoint _endpoint          # OPC UA服务器端点
│   ├── UserIdentity _userIdentity            # 用户身份凭证
│   ├── Session _session                      # OPC UA通信会话
│   ├── ApplicationConfiguration _appConfig   # 应用程序配置
│   ├── ApplicationInstance application       # 应用实例
│   ├── SessionReconnectHandler _reconnectHandler  # 重连处理器
│   └── Dictionary<string, Subscription> dic_subscriptions  # 订阅字典
│
├── 构造函数
│   └── OPCUALib(appName, serverUrl, namespaceIndex, security, user, password)
│
├── 连接管理
│   ├── Connect(timeOut, keepAlive)           # 同步连接
│   ├── ConnectAsync(timeOut, keepAlive, ct)  # 异步连接
│   ├── DisConnect()                          # 断开连接
│   ├── DisconnectAsync(ct)                   # 异步断开
│   ├── KeepAlive(session, e)                 # 保活回调
│   └── Reconnect(sender, e)                 # 重连回调
│
├── 数据读取
│   ├── Read(address)                         # 读取单个值
│   ├── Read<TValue>(address)                 # 类型化读取
│   ├── ReadAsync(address, ct)                # 异步读取
│   ├── ReadAsync<TValue>(address, ct)        # 异步类型化读取
│   ├── ReadAsync(IEnumerable<string>, ct)    # 批量异步读取
│   └── Read(List<string>)                    # 批量读取
│
├── 数据写入
│   ├── Write(address, value)                 # 写入单个值
│   ├── Write(tag)                            # 写入Tag对象
│   ├── Write(List<Tag>)                      # 批量写入
│   ├── WriteAsync(address, value, ct)        # 异步写入
│   ├── WriteAsync(tag, ct)                   # 异步写入Tag
│   └── WriteAsync(List<Tag>, ct)             # 批量异步写入
│
├── 服务器浏览
│   ├── Devices(recursive)                    # 获取设备列表
│   ├── Groups(address, recursive)            # 获取组列表
│   ├── Tags(address)                         # 获取标签列表
│   ├── DevicesAsync(recursive, ct)           # 异步获取设备
│   ├── GroupsAsync(address, recursive, ct)   # 异步获取组
│   └── TagsAsync(address, ct)                # 异步获取标签
│
└── 订阅管理
    ├── AddSubscription(key, tag, callback)   # 添加单个订阅
    ├── AddSubscription(key, tags[], callback) # 批量添加订阅
    ├── RemoveSubscription(key)               # 移除订阅
    ├── RemoveAllSubscription()               # 清空所有订阅
    ├── Monitoring(address, ms, monitor)      # 监控变量
    └── MonitoringAsync(address, ms, monitor, ct)  # 异步监控

数据模型类
├── Tag              # 标签数据模型
├── Device           # 设备数据模型
├── Group            # 组数据模型
├── ReadException    # 读取异常
└── WriteException   # 写入异常

详细代码分析

一、构造函数分析

csharp 复制代码
public OPCUALib(string appName, string serverUrl, ushort namespaceIndex = 2, 
                bool security = false, string user = "", string password = "")
参数说明
参数 类型 默认值 说明
appName string - 应用程序名称,用于生成证书主题
serverUrl string - OPC UA服务器地址,如 opc.tcp://localhost:4840
namespaceIndex ushort 2 命名空间索引,用于构造NodeId
security bool false 是否启用安全认证(false=匿名,true=用户名密码)
user string "" 用户名(security=true时使用)
password string "" 密码(security=true时使用)
初始化流程详解
1. 创建证书目录结构
csharp 复制代码
string text = Path.Combine(Directory.GetCurrentDirectory(), "Certificates");
Directory.CreateDirectory(text);
Directory.CreateDirectory(Path.Combine(text, "Application"));    // 客户端证书
Directory.CreateDirectory(Path.Combine(text, "Trusted"));        // 信任的颁发者
Directory.CreateDirectory(Path.Combine(text, "TrustedPeer"));    // 信任的对等节点(服务器证书)
Directory.CreateDirectory(Path.Combine(text, "Rejected"));       // 拒绝的证书

目录作用

  • Application/: 存放客户端应用程序证书
  • Trusted/: 存放信任的证书颁发机构证书
  • TrustedPeer/: 存放信任的服务器证书
  • Rejected/: 存放被拒绝的证书
2. 设置用户身份
csharp 复制代码
_userIdentity = ((user.Length > 0 && password.Length > 0) ? 
    new UserIdentity(user, password) :  // 用户名密码认证
    new UserIdentity());                  // 匿名认证
3. 配置应用程序
csharp 复制代码
_appConfig = new ApplicationConfiguration
{
    ApplicationName = appName,
    ApplicationUri = Utils.Format("urn:{0}:{1}", hostName, appName),
    ApplicationType = ApplicationType.Client,  // 标识为客户端
    
    SecurityConfiguration = new SecurityConfiguration
    {
        ApplicationCertificate = new CertificateIdentifier
        {
            StorePath = Path.Combine(text, "Application"),
            SubjectName = "CN=" + appName + ", OU=" + hostName + ", O=" + hostName + ", C=CH",
        },
        TrustedIssuerCertificates = new CertificateTrustList
        {
            StoreType = "Directory",
            StorePath = Path.Combine(text, "Trusted")
        },
        TrustedPeerCertificates = new CertificateTrustList
        {
            StoreType = "Directory",
            StorePath = Path.Combine(text, "TrustedPeer")
        },
        RejectedCertificateStore = new CertificateTrustList
        {
            StoreType = "Directory",
            StorePath = Path.Combine(text, "Rejected")
        },
        AutoAcceptUntrustedCertificates = true,      // ⚠️ 自动接受不受信任的证书
        AddAppCertToTrustedStore = true,             // 自动添加应用证书到信任存储
        RejectSHA1SignedCertificates = false         // ⚠️ 不拒绝SHA1签名证书(不安全)
    },
    TransportQuotas = new TransportQuotas
    {
        OperationTimeout = 20000  // 操作超时20秒
    },
    ClientConfiguration = new ClientConfiguration
    {
        DefaultSessionTimeout = 5000  // 会话超时5秒
    }
};

关键配置点

  • ⚠️ AutoAcceptUntrustedCertificates = true: 自动接受不受信任的证书,但不包括不受信任的根证书
  • ⚠️ RejectSHA1SignedCertificates = false: 接受SHA1签名(存在安全风险)
4. 验证配置
csharp 复制代码
_appConfig.Validate(ApplicationType.Client).GetAwaiter().GetResult();

验证配置的有效性,确保所有必需的参数都已正确设置。

5. 设置证书验证回调
csharp 复制代码
if (_appConfig.SecurityConfiguration.AutoAcceptUntrustedCertificates)
{
    _appConfig.CertificateValidator.CertificateValidation += 
        delegate (CertificateValidator s, CertificateValidationEventArgs ee)
        {
            // ⚠️ 关键:只在StatusCode为Good时接受证书
            ee.Accept = StatusCode.IsGood(ee.Error.StatusCode);
        };
}

重要:这个回调逻辑有问题!

  • StatusCode.IsGood(ee.Error.StatusCode) 只在状态码本身为Good时才返回true
  • 对于证书错误(如UntrustedRoot),ee.Error.StatusCode本身就是Bad,所以IsGood返回false
  • 这导致不受信任的根证书被拒绝,连接失败
6. 检查应用证书
csharp 复制代码
application.CheckApplicationInstanceCertificate(
    silent: true,      // 静默模式,不显示UI
    minimumKeySize: 2048,  // 最小密钥大小2048位
    lifeTimeInMonths: this.LifeTimeInMonths  // 证书有效期(默认12个月)
).GetAwaiter().GetResult();

功能

  • 检查是否存在应用证书
  • 如果不存在或过期,自动创建新的证书
  • 证书保存在 Certificates/Application/ 目录
7. 选择服务器端点
csharp 复制代码
EndpointDescription description = CoreClientUtils.SelectEndpoint(
    _appConfig, 
    serverUrl, 
    security   // ⚠️ 使用security参数:false=选择None安全策略,true=选择Sign&Encrypt
);
EndpointConfiguration configuration = EndpointConfiguration.Create(_appConfig);
_endpoint = new ConfiguredEndpoint(null, description, configuration);

端点选择逻辑

  • security = false: 选择 None 安全策略(无加密、无签名)
  • security = true: 选择 Sign & Encrypt 安全策略(加密+签名)

二、连接方法分析

Connect() - 同步连接
csharp 复制代码
public void Connect(uint timeOut = 5u, bool keepAlive = false)
{
    DisConnect();  // 先断开现有连接
    
    _session = Task.Run(async () => await Session.Create(
        _appConfig,                        // 应用配置
        _endpoint,                         // 服务器端点
        updateBeforeConnect: false,        // ⚠️ 连接前不更新端点
        checkDomain: false,                // ⚠️ 不检查域名
        _appConfig.ApplicationName,        // 会话名称
        timeOut * 1000,                    // 超时时间(毫秒)
        _userIdentity,                     // 用户身份
        null                               // 优先区域
    )).GetAwaiter().GetResult();
    
    if (keepAlive)
    {
        _session.KeepAlive += KeepAlive;  // 注册保活事件
    }
    
    if (_session == null || !_session.Connected)
    {
        throw new ServerException("Error creating a session on the server");
    }
}

关键参数

  • updateBeforeConnect: false: 不在连接前更新端点描述
  • checkDomain: false: 不验证证书域名
  • timeOut * 1000: 将秒转换为毫秒

⚠️ 潜在问题

由于证书验证回调的问题(第128行),当服务器证书的根证书不受信任时,会抛出异常:

复制代码
Certificate validation failed. UntrustedRoot
ConnectAsync() - 异步连接
csharp 复制代码
public async Task ConnectAsync(uint timeOut = 5u, bool keepAlive = false, 
                               CancellationToken ct = default(CancellationToken))
{
    await DisconnectAsync(ct);
    
    _session = await Session.Create(
        _appConfig, 
        _endpoint, 
        updateBeforeConnect: false,  // ⚠️ 同样的问题
        checkDomain: false, 
        _appConfig.ApplicationName, 
        timeOut * 1000, 
        _userIdentity, 
        null, 
        ct  // 支持取消操作
    );
    
    if (keepAlive)
    {
        _session.KeepAlive += KeepAlive;
    }
    
    if (!(_session?.Connected ?? false))
    {
        throw new ServerException("Error creating a session on the server");
    }
}

优势

  • 不阻塞调用线程
  • 支持通过CancellationToken取消操作
  • 适合UI应用程序,避免界面冻结

三、保活机制分析

csharp 复制代码
private void KeepAlive(ISession session, KeepAliveEventArgs e)
{
    try
    {
        if (!ServiceResult.IsBad(e.Status))  // 如果状态正常
        {
            return;  // 直接返回,不做处理
        }
        
        lock (_lock)  // 加锁,防止多线程冲突
        {
            if (_reconnectHandler == null)  // 如果没有重连处理器
            {
                // 创建重连处理器
                _reconnectHandler = new SessionReconnectHandler(reconnectAbort: true);
                // 开始异步重连(10秒超时)
                _reconnectHandler.BeginReconnect(_session, 10000, Reconnect);
            }
        }
    }
    catch (Exception)
    {
        // 忽略异常
    }
}

工作原理

  1. 定期检查:OPC UA服务器定期发送保活消息
  2. 状态检测 :如果e.Status为Bad,说明连接有问题
  3. 自动重连 :创建SessionReconnectHandler在后台尝试重连
  4. 会话恢复 :重连成功后,Reconnect回调会更新_session
Reconnect() 回调
csharp 复制代码
private void Reconnect(object sender, EventArgs e)
{
    if (sender != _reconnectHandler)  // 确认发送者
    {
        return;
    }
    
    lock (_lock)
    {
        if (_reconnectHandler.Session != null)
        {
            // 更新会话为新重连的会话
            _session = (Session)_reconnectHandler.Session;
        }
        
        // 清理重连处理器
        _reconnectHandler.Dispose();
        _reconnectHandler = null;
    }
}

四、数据读取分析

1. 单点读取
csharp 复制代码
public Tag Read(string address)
{
    try
    {
        Tag tag = new Tag
        {
            Address = address,
            Value = null
        };
        
        ReadValueId readValueId = new ReadValueId
        {
            NodeId = new NodeId(address, _namespaceIndex),  // 节点ID
            AttributeId = 13u                                // 13=Value属性
        };
        
        ReadValueIdCollection nodesToRead = new ReadValueIdCollection();
        nodesToRead.Add(readValueId);
        
        // 执行读取
        _session.Read(
            null,                          // 预留头部
            0.0,                           // 最大年龄(0=读取最新值)
            TimestampsToReturn.Both,       // 返回源时间和服务器时间
            nodesToRead,                   // 要读取的节点
            out var results,               // 读取结果
            out var _                      // 诊断信息
        );
        
        tag.Value = results[0].Value;
        tag.Code = results[0].StatusCode;
        return tag;
    }
    catch (Exception)
    {
        return null;  // 读取失败返回null
    }
}

AttributeId说明

  • 1: NodeId - 节点标识符
  • 12: DisplayName - 显示名称
  • 13: Value - 值(最常用)
  • 14: DataType - 数据类型
  • 16: AccessLevel - 访问级别
2. 类型化读取
csharp 复制代码
public TValue Read<TValue>(string address)
{
    ReadValueIdCollection nodesToRead = new ReadValueIdCollection();
    nodesToRead.Add(new ReadValueId
    {
        NodeId = new NodeId(address, _namespaceIndex),
        AttributeId = 13u
    });
    
    _session.Read(null, 0.0, TimestampsToReturn.Both, 
                  nodesToRead, out var results, out var _);
    
    if (results[0].StatusCode != 0u)  // 如果状态码不为0(Good)
    {
        throw new ReadException(results[0].StatusCode.Code.ToString());
    }
    
    // 类型转换
    return (TValue)Convert.ChangeType(results[0].Value, typeof(TValue));
}

使用示例

csharp 复制代码
int value = opcLib.Read<int>("Device1.Tag1");
float temperature = opcLib.Read<float>("PLC.Temperature");
bool status = opcLib.Read<bool>("Sensor.Status");
3. 批量读取
csharp 复制代码
public List<Tag> Read(List<string> address)
{
    List<Tag> list = new List<Tag>();
    ReadValueIdCollection nodesToRead = new ReadValueIdCollection();
    
    // 添加所有节点
    nodesToRead.AddRange(address.Select(a => new ReadValueId
    {
        NodeId = new NodeId(a, _namespaceIndex),
        AttributeId = 13u
    }));
    
    // 一次请求读取所有节点
    _session.Read(null, 0.0, TimestampsToReturn.Both, 
                  nodesToRead, out var results, out var _);
    
    // 构造返回结果
    for (int i = 0; i < address.Count; i++)
    {
        list.Add(new Tag
        {
            Address = address[i],
            Value = results[i].Value,
            Code = results[i].StatusCode
        });
    }
    
    return list;
}

性能优势

  • 单次网络请求读取多个节点
  • 减少网络往返次数
  • 显著提高读取效率

五、数据写入分析

csharp 复制代码
public void Write(string address, object value)
{
    WriteValueCollection writeValueCollection = new WriteValueCollection();
    
    WriteValue item = new WriteValue
    {
        NodeId = new NodeId(address, _namespaceIndex),
        AttributeId = 13u,  // 写入Value属性
        Value = new DataValue { Value = value }
    };
    
    writeValueCollection.Add(item);
    
    // 执行写入
    _session.Write(null, writeValueCollection, out var results, out var _);
    
    // 检查结果
    if (!StatusCode.IsGood(results[0]))
    {
        throw new WriteException("Error writing value. Code: " + results[0].Code);
    }
}

写入流程

  1. 构造WriteValueCollection包含要写入的值
  2. 调用Session.Write()发送写入请求
  3. 检查返回的状态码
  4. 如果失败,抛出WriteException

批量写入

csharp 复制代码
public void Write(List<Tag> tags)
{
    WriteValueCollection writeValueCollection = new WriteValueCollection();
    writeValueCollection.AddRange(tags.Select(tag => new WriteValue
    {
        NodeId = new NodeId(tag.Address, _namespaceIndex),
        AttributeId = 13u,
        Value = new DataValue { Value = tag.Value }
    }));
    
    _session.Write(null, writeValueCollection, out var results, out var _);
    
    // 检查所有结果
    foreach (var item in results)
    {
        if (!StatusCode.IsGood(item.Code))
        {
            throw new WriteException("Error writing value. Code: " + results.Find(
                (StatusCode sc) => !StatusCode.IsGood(sc)).Code);
        }
    }
}

六、订阅机制分析

AddSubscription() 方法
csharp 复制代码
public void AddSubscription(string key, string[] tags, 
    Action<string, MonitoredItem, MonitoredItemNotificationEventArgs> callback)
{
    // 创建订阅对象
    Subscription m_subscription = new Subscription(_session.DefaultSubscription)
    {
        PublishingEnabled = true,             // 启用发布
        PublishingInterval = 0,               // 发布间隔(0=数据变化时立即推送)
        KeepAliveCount = 10000,               // 保活计数
        LifetimeCount = uint.MaxValue,        // 生命周期
        MaxNotificationsPerPublish = 1000,    // 每次发布最大通知数
        Priority = 100,                       // 优先级
        DisplayName = key
    };
    
    // 添加监控项
    for (int i = 0; i < tags.Length; i++)
    {
        var item = new MonitoredItem
        {
            StartNodeId = new NodeId(tags[i], _namespaceIndex),
            AttributeId = Attributes.Value,
            DisplayName = tags[i],
            SamplingInterval = 100,  // 采样间隔100毫秒
        };
        
        // 注册数据变化回调
        item.Notification += (MonitoredItem monitoredItem, MonitoredItemNotificationEventArgs args) =>
        {
            callback?.Invoke(key, monitoredItem, args);
        };
        
        m_subscription.AddItem(item);
    }
    
    // 添加到会话
    _session.AddSubscription(m_subscription);
    m_subscription.Create();  // 激活订阅
    
    // 管理订阅字典
    if (dic_subscriptions.ContainsKey(key))
    {
        // 如果key已存在,删除旧订阅
        dic_subscriptions[key].Delete(true);
        _session.RemoveSubscription(dic_subscriptions[key]);
        dic_subscriptions[key].Dispose();
        dic_subscriptions[key] = m_subscription;
    }
    else
    {
        dic_subscriptions.Add(key, m_subscription);
    }
}

订阅参数说明

参数 说明
PublishingInterval 0 发布间隔,0=数据变化时立即推送
KeepAliveCount 10000 保活计数,10000次无变化后发送保活消息
LifetimeCount uint.MaxValue 生命周期
MaxNotificationsPerPublish 1000 每次发布的最大通知数
Priority 100 优先级(数值越小优先级越高)
SamplingInterval 100 采样间隔(毫秒)

订阅工作流程

复制代码
客户端                          服务器
  |                               |
  |---Create Subscription-------->|  创建订阅上下文
  |                               |  
  |---Add MonitoredItem---------->|  添加监控项
  |                               |  开始监控节点变化
  |                               |
  |<---Publish (Notification)-----|  数据变化时推送通知
  |---Publish Ack--------------->|  确认接收
  |                               |
  |<---Publish (Notification)-----|  持续接收推送

使用示例

csharp 复制代码
opcLib.AddSubscription("Group1", new[] { "Tag1", "Tag2" }, 
    (key, item, args) =>
    {
        var newValue = args.NotificationValue.Value;
        Console.WriteLine($"{key}.{item.DisplayName} = {newValue}");
    });
Monitoring() 方法
csharp 复制代码
public void Monitoring(string address, int milliseconds, 
    MonitoredItemNotificationEventHandler monitor)
{
    Subscription subscription = new Subscription
    {
        PublishingEnabled = true,
        PublishingInterval = milliseconds,  // 自定义发布间隔
        Priority = 1,
        KeepAliveCount = 10u,
        LifetimeCount = 20u,
        MaxNotificationsPerPublish = 1000u
    };
    
    MonitoredItem monitoredItem = new MonitoredItem
    {
        StartNodeId = new NodeId(address, _namespaceIndex),
        AttributeId = 13u
    };
    
    monitoredItem.Notification += monitor;
    subscription.AddItem(monitoredItem);
    _session.AddSubscription(subscription);
    subscription.Create();
    subscription.ApplyChanges();  // 应用更改
}

与AddSubscription的区别

  • Monitoring创建独立的订阅
  • 可以自定义发布间隔
  • 适合监控单个变量

七、服务器浏览分析

csharp 复制代码
public List<Device> Devices(bool recursive = false)
{
    Browser browser = new Browser(_session)
    {
        BrowseDirection = BrowseDirection.Forward,  // 向前浏览
        NodeClassMask = 3,                          // 3=Object(1) + Variable(2)
        ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences  // 层次引用
    };
    
    // 从ObjectsFolder根节点开始浏览
    ReferenceDescriptionCollection source = browser.Browse(ObjectIds.ObjectsFolder);
    
    List<Device> list = new List<Device>();
    
    foreach (var item in source)
    {
        if (item.ToString() != "Server")  // 排除Server节点
        {
            Device device = new Device { Address = item.ToString() };
            device.Groups = Groups(device.Address, recursive);  // 递归获取组
            device.Tags = Tags(device.Address);                  // 获取标签
            list.Add(device);
        }
    }
    
    return list;
}

NodeClassMask值

  • 1: Object - 对象节点
  • 2: Variable - 变量节点
  • 3: Object + Variable - 对象和变量节点

数据结构示例

复制代码
ObjectsFolder
├── Server (被排除)
├── Device1
│   ├── Channel1
│   │   ├── Tag1 (Variable)
│   │   └── Tag2 (Variable)
│   └── Channel2
│       └── Tag3 (Variable)
└── Device2
    └── Tag4 (Variable)

工作原理

1. OPC UA通信架构

复制代码
应用程序层
   |
   | OPC UA API
   |
┌──┴───────────────────────────────┐
│         OPCUALib                 │
│  - 连接管理                       │
│  - 证书处理                       │
│  - 会话管理                       │
│  - 数据读写                       │
│  - 订阅管理                       │
└──┬───────────────────────────────┘
   |
   | OPC UA Foundation Library
   |
┌──┴───────────────────────────────┐
│      OPC UA Stack                │
│  - 安全通道 (Secure Channel)      │
│  - 序列化 (Serialization)         │
│  - 传输 (Transport)               │
└──┬───────────────────────────────┘
   |
   | TCP/IP
   |
┌──┴───────────────────────────────┐
│      OPC UA Server               │
└──────────────────────────────────┘

2. 连接建立流程

复制代码
1. 创建OPCUALib实例
   ├─ 创建证书目录
   ├─ 配置应用程序
   ├─ 设置证书验证回调
   ├─ 检查/创建应用证书
   └─ 选择服务器端点

2. 调用Connect()
   ├─ 断开旧连接
   ├─ 创建安全通道
   ├─ 创建会话
   ├─ 激活会话
   └─ 注册保活回调

3. 正常通信
   ├─ 读写操作
   ├─ 订阅通知
   └─ 保活检查

4. 调用DisConnect()
   ├─ 删除订阅
   ├─ 关闭会话
   └─ 关闭安全通道

3. 证书处理流程(原始版本)

复制代码
连接服务器
   |
   v
接收服务器证书
   |
   v
检查证书验证回调
   |
   v
StatusCode.IsGood(ee.Error.StatusCode)?
   |
   |-- No --> 拒绝证书 --> ❌ 连接失败
   |
   v
  Yes
   |
   v
接受证书 --> ✅ 连接成功

⚠️ 问题 :对于UntrustedRoot错误,ee.Error.StatusCode本身就是Bad,导致证书被拒绝。


使用指南

1. 基本连接示例

csharp 复制代码
// 创建客户端实例
var opcLib = new OPCUALib(
    appName: "MyClient",
    serverUrl: "opc.tcp://localhost:4840",
    namespaceIndex: 2,
    security: false  // 匿名登录
);

// 建立连接
try
{
    opcLib.Connect(timeOut: 5, keepAlive: true);
    Console.WriteLine("连接成功!");
}
catch (Exception ex)
{
    Console.WriteLine($"连接失败:{ex.Message}");
}

2. 用户名密码认证

csharp 复制代码
var opcLib = new OPCUALib(
    appName: "MyClient",
    serverUrl: "opc.tcp://localhost:4840",
    namespaceIndex: 2,
    security: true,       // 启用安全认证
    user: "admin",
    password: "password"
);

opcLib.Connect();

3. 数据读写

csharp 复制代码
// 读取单个值
Tag tag = opcLib.Read("Device1.Temperature1");
Console.WriteLine($"Value: {tag.Value}, Quality: {tag.Quality}");

// 类型化读取
int value = opcLib.Read<int>("Device1.Counter");
float temperature = opcLib.Read<float>("Device1.Temperature");

// 批量读取
var addresses = new List<string> { "Tag1", "Tag2", "Tag3" };
List<Tag> results = opcLib.Read(addresses);
foreach (var t in results)
{
    Console.WriteLine($"{t.Address} = {t.Value}");
}

// 写入数据
opcLib.Write("Device1.SetPoint", 100.5);

// 批量写入
var tags = new List<Tag>
{
    new Tag { Address = "Tag1", Value = 10 },
    new Tag { Address = "Tag2", Value = 20 },
    new Tag { Address = "Tag3", Value = 30 }
};
opcLib.Write(tags);

4. 数据订阅

csharp 复制代码
// 添加订阅
opcLib.AddSubscription("Group1", new[] { "Tag1", "Tag2", "Tag3" }, 
    (key, item, args) =>
    {
        var monitoredItemNotification = args.NotificationValue;
        var value = monitoredItemNotification.Value;
        var quality = monitoredItemNotification.StatusCode;
        
        Console.WriteLine($"[{key}] {item.DisplayName} = {value} (Quality: {quality})");
    });

// 移除订阅
opcLib.RemoveSubscription("Group1");

// 清空所有订阅
opcLib.RemoveAllSubscription();

5. 异步操作

csharp 复制代码
// 异步连接
await opcLib.ConnectAsync(timeOut: 10, keepAlive: true);

// 异步读取
Tag tag = await opcLib.ReadAsync("Device1.Tag1");

// 异步写入
await opcLib.WriteAsync("Device1.Tag1", 100);

// 异步批量读取
var addresses = new List<string> { "Tag1", "Tag2", "Tag3" };
IEnumerable<Tag> results = await opcLib.ReadAsync(addresses);

// 异步断开
await opcLib.DisconnectAsync();

6. 浏览服务器

csharp 复制代码
// 获取所有设备
List<Device> devices = opcLib.Devices(recursive: true);

foreach (var device in devices)
{
    Console.WriteLine($"设备: {device.Name}");
    
    foreach (var group in device.Groups)
    {
        Console.WriteLine($"  组: {group.Name}");
        
        foreach (var tag in group.Tags)
        {
            Console.WriteLine($"    标签: {tag.Name}");
        }
    }
}

常见问题

1. 证书验证错误

问题

复制代码
Certificate validation failed. UntrustedRoot: 已处理证书链,但是在不受信任提供程序信任的根证书中终止。

原因分析

代码第128行的证书验证回调:

csharp 复制代码
ee.Accept = StatusCode.IsGood(ee.Error.StatusCode);

这个逻辑只接受状态码本身为Good的证书。对于UntrustedRoot错误,ee.Error.StatusCodeBadCertificateUntrusted(值为0x8015000),所以StatusCode.IsGood()返回false,导致证书被拒绝。

解决方案

修改证书验证回调逻辑:

csharp 复制代码
_appConfig.CertificateValidator.CertificateValidation += 
    delegate (CertificateValidator s, CertificateValidationEventArgs ee)
    {
        ee.Accept = true;  // 强制接受所有证书
    };

或者在连接前手动添加服务器证书到信任存储:

csharp 复制代码
// 获取服务器证书
var serverCert = _endpoint.Description.ServerCertificate;
if (serverCert != null)
{
    // 保存到TrustedPeer目录
    File.WriteAllBytes(
        Path.Combine(certPath, "ServerCert.der"),
        serverCert.RawData
    );
}

2. 连接超时

问题

复制代码
Error creating a session on the server

可能原因

  • 服务器地址错误
  • 网络不通
  • 防火墙阻止
  • 服务器未运行

解决方法

csharp 复制代码
// 增加超时时间
opcLib.Connect(timeOut: 10);  // 10秒超时

// 检查连接状态
if (opcLib.IsConnected)
{
    Console.WriteLine("连接成功");
}

3. 节点不存在

问题

复制代码
BadNodeIdUnknown

原因

  • 节点地址错误
  • 命名空间索引错误

解决方法

csharp 复制代码
// 修改命名空间索引
opcLib.ChangeNamespaceIndex(3);

// 使用UAExpert工具查看正确的节点地址和命名空间

4. 类型转换错误

问题

复制代码
InvalidCastException

原因

  • 读取的数据类型与目标类型不匹配

解决方法

csharp 复制代码
// 先读取为object,再检查类型
Tag tag = opcLib.Read("Tag1");
if (tag.Value is int)
{
    int value = (int)tag.Value;
}

5. 订阅不触发

问题:添加订阅后,回调函数没有被调用

可能原因

  • 连接断开
  • 节点地址错误
  • 数据没有变化

解决方法

csharp 复制代码
// 检查连接状态
if (!opcLib.IsConnected)
{
    Console.WriteLine("未连接");
    return;
}

// 检查订阅是否添加成功
// 可以通过UAExpert工具查看服务器端的订阅状态

代码优化建议

1. 证书处理优化

当前问题:证书验证回调逻辑有缺陷

建议修改

csharp 复制代码
// 修改前
ee.Accept = StatusCode.IsGood(ee.Error.StatusCode);

// 修改后
ee.Accept = true;  // 强制接受所有证书(测试环境)

2. 异常处理优化

当前问题:Read方法返回null,调用者需要额外检查

建议修改

csharp 复制代码
public Tag Read(string address)
{
    try
    {
        // ... 读取逻辑
        return tag;
    }
    catch (Exception ex)
    {
        throw new ReadException($"读取失败: {address}, 错误: {ex.Message}");
    }
}

3. 资源释放优化

建议添加Dispose模式

csharp 复制代码
public class OPCUALib : IDisposable
{
    public void Dispose()
    {
        DisConnect();
        dic_subscriptions.Clear();
        _appConfig?.CertificateValidator?.Dispose();
    }
}

4. 日志记录

建议添加日志

csharp 复制代码
private void Log(string message)
{
    // 使用日志框架记录
    Debug.WriteLine($"[{DateTime.Now}] {message}");
}

总结

代码优点

✅ 功能完整,涵盖OPC UA主要功能

✅ 支持同步/异步操作

✅ 内置保活和重连机制

✅ 支持批量操作,提高效率

✅ 订阅机制设计合理

存在问题

⚠️ 证书验证逻辑有缺陷 (第128行)

⚠️ 缺少详细的异常信息

⚠️ 没有实现IDisposable接口

⚠️ 缺少日志记录机制

适用场景

  • 工业自动化数据采集
  • 设备监控系统
  • MES/SCADA系统集成
  • 实时数据处理
相关推荐
大强同学2 小时前
Obsidian CLI + Claude Code = 王炸组合
人工智能·windows·ai编程·cli
深蓝海拓2 小时前
S7-1500学习笔记:用户自定义数据类型(UDT)
笔记·学习·plc
罗罗攀3 小时前
PyTorch学习笔记|神经网络的损失函数
人工智能·pytorch·笔记·神经网络·学习
iceslime4 小时前
Windows10系统静音修复相关
windows·音频·修复
humors2214 小时前
AI工具合集,不定期更新
人工智能·windows·ai·工具·powershell·deepseek
Echo-J4 小时前
在 Windows 7 虚拟机上安装 VMware Tools 时遇到驱动无法安装的问题
windows
tq10864 小时前
价值:社会对劳动所产生的效用增量形成的局部共识
笔记
A923A5 小时前
【小兔鲜电商前台 | 项目笔记】第八天
前端·vue.js·笔记·项目·小兔鲜
猹叉叉(学习版)6 小时前
【系统分析师_知识点整理】 15.数学计算与知识产权
笔记·软考·知识产权·系统分析师