本文档基于自写的 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)
{
// 忽略异常
}
}
工作原理:
- 定期检查:OPC UA服务器定期发送保活消息
- 状态检测 :如果
e.Status为Bad,说明连接有问题 - 自动重连 :创建
SessionReconnectHandler在后台尝试重连 - 会话恢复 :重连成功后,
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);
}
}
写入流程:
- 构造
WriteValueCollection包含要写入的值 - 调用
Session.Write()发送写入请求 - 检查返回的状态码
- 如果失败,抛出
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.StatusCode是BadCertificateUntrusted(值为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系统集成
- 实时数据处理