☰
OPC UA客户端+SQL Server工业数据持久化方案
2026/10/16 6:47:23 网站建设 项目流程

简介:本资源是一套基于C#开发的OPC UA工业通信客户端完整实现,面向工业自动化、物联网及智能制造领域的开发者与学习者,解决设备数据采集与企业数据库集成的实际问题。项目使用OpcUaHelper开源库快速构建OPC UA客户端,支持连接服务器、读取实时数据,并将解析后的字段(以下划线分隔)持久化写入SQL Server,涵盖建表、插入、配置等关键环节,适合作为产线数据上位机或边缘网关开发的参考范例。压缩包共144个文件,含9个核心C#源码文件(如OPCUA_Client.csproj及主逻辑类)、56个运行依赖DLL、54个配套XML文档、2个可执行EXE及调试符号PDB,整体体积5.51MB,结构清晰,便于编译调试与二次扩展。目前已有3239人学习下载,提供开箱即用的工程结构、完整配置文件(app.config、App.config)、缓存与资源文件,以及典型工业数据流转的端到端代码实现,是理解OPC UA协议应用与SQL Server数据集成的高价值实践样本。

1. 这不是个“连上PLC就能出数”的玩具:一个能跑在产线边缘设备上的OPC UA客户端,自带SQL Server写入闭环

某高校实验室做设备数据采集时卡在最后一步:OPC UA客户端连得上西门子S7-1500,也能读到温度、压力、转速三个变量的实时值,但一关程序,数据就丢——没有持久化,所有分析都成空谈。他们试过Excel导出、本地JSON缓存、甚至手写CSV日志,结果要么并发写入报错,要么时间戳乱序,要么字段类型错配导致SQL Server拒绝插入。这个C#项目就是为解决这种「现场级数据落地」问题而生的:它不只封装了OPC UA通信层(基于OPCFoundation.NetStandard.Opc.Ua),更把数据从UA节点读取、类型映射、结构化组装、批量写入SQL Server的整条链路,用可配置、可重试、带事务边界的方式串了起来。适合需要将工业现场传感器/PLC数据稳定落库、后续做报表或轻量分析的中小产线、教学实验平台、或作为更大MES系统的前置采集模块。它不是SDK示例,而是你部署到Windows Server或工控机后,改几行配置就能进生产环境跑的数据管道。


2. 项目结构与技术栈:三层分治,每层都留了钩子

这个项目采用典型的分层架构,但没堆砌过度设计——所有模块都服务于「让数据从UA服务器流进SQL Server」这一个目标。目录结构干净,没有冗余文件,核心是ClientCore(通信)、DataPersistence(存储)、Configuration(配置)三个命名空间。技术选型务实:C# 8.0(支持可空引用类型,减少空指针翻车)、.NET Core 3.1(长期支持,兼容Win7+及多数工控机系统)、OPCFoundation.NetStandard.Opc.Uav1.4.369(当前最稳的LTS版本,避开了v2.x里部分证书验证玄学问题)、Microsoft.Data.SqlClientv5.1.5(替代已废弃的System.Data.SqlClient,支持连接池复用和更细粒度超时控制)。整个解决方案共4个项目:主客户端(WPF界面+后台服务)、数据模型类库、SQL写入类库、以及一个独立的配置管理工具(用于生成和校验XML配置文件)。没有用Entity Framework——因为工业场景下字段固定、写入高频、对性能敏感,原生SQL批量插入+参数化查询更可控。

2.1 核心目录结构:一眼看清数据流向

项目根目录下直接可见:

/OPCUAClient/ ├── /ClientCore/ # OPC UA通信核心:会话管理、订阅创建、数据变更回调 ├── /DataPersistence/ # SQL Server持久化:表结构初始化、批量插入、失败重试策略 ├── /Models/ # 数据模型:OpcUaVariable(含NodeId、DisplayName)、SqlTableMapping(字段名映射)、DataPoint(带时间戳的单点值) ├── /Configuration/ # 配置加载:从appsettings.json读取UA端点、SQL连接字符串、采样周期等 ├── /UI/ # WPF界面:连接状态指示灯、变量列表、手动触发读取按钮、日志滚动框 ├── App.config # 传统.NET配置(兼容旧版驱动) └── appsettings.json # 主配置文件(推荐使用)

提示:appsettings.json是主配置入口,App.config仅作兼容备份。实际运行时优先读取 JSON 配置,若不存在则回退到 XML。这种双配置机制是给现场运维留的「后悔药」——万一 JSON 格式写错导致启动失败,改回 XML 还能救。

2.2 OPC UA通信层:会话生命周期与订阅管理

ClientCore命名空间下最关键的类是UaSessionManager。它不直接继承Session,而是封装了会话创建、重连、异常清理的完整逻辑。重点在于其StartSubscriptionAsync()方法:

public async Task StartSubscriptionAsync(string endpointUrl, List<string> nodeIds) { // 1. 创建安全通道与会话(含证书自动信任逻辑) var session = await Session.Create( new ConfiguredEndpoint(new EndpointDescription { EndpointUrl = endpointUrl }), new UserIdentity(new AnonymousIdentity()), null, 60000, // 会话超时毫秒 null); // 2. 创建订阅(发布间隔=采样周期,毫秒) var subscription = new Subscription(session.DefaultSubscription) { PublishingInterval = _config.PublishingIntervalMs, KeepAliveCount = 10, LifetimeCount = 60 }; // 3. 为每个NodeId添加监控项(采样间隔=100ms,即每100ms检查一次值变化) foreach (var nodeId in nodeIds) { var monitoredItem = new MonitoredItem(subscription.DefaultItem) { StartNodeId = nodeId, SamplingInterval = 100, QueueSize = 10, DiscardOldest = true }; monitoredItem.Notification += OnMonitoredItemNotification; subscription.AddMonitoredItem(monitoredItem); } subscription.SetPublishingMode(true); session.AddSubscription(subscription); }

这段代码的关键参数说明:

  • PublishingIntervalMs:订阅的发布周期,设为1000即每秒推送一次聚合数据包;设为100则每100ms推送一次(适合高频变量,但需注意UA服务器负载);
  • SamplingInterval = 100:客户端主动轮询节点值的间隔,必须 ≤PublishingInterval,否则无效;
  • QueueSize = 10:每个监控项本地缓存的最大变更事件数,防止网络抖动时丢数据;
  • OnMonitoredItemNotification是回调方法,收到值变更后,会立即将DataPoint对象推入内存队列,交由DataPersistence层消费。

2.3 SQL Server写入层:批量插入与幂等保障

DataPersistence的核心是SqlBulkInserter类。它不走单条INSERT INTO ... VALUES (...),而是用SqlBulkCopy实现每秒千级写入。关键设计在于「幂等性」和「失败隔离」:

public async Task<int> BulkInsertAsync(IEnumerable<DataPoint> dataPoints) { using var connection = new SqlConnection(_connectionString); await connection.OpenAsync(); // 1. 构建DataTable(字段顺序必须与SQL表严格一致) var dataTable = new DataTable(); dataTable.Columns.Add("Timestamp", typeof(DateTime)); dataTable.Columns.Add("NodeId", typeof(string)); dataTable.Columns.Add("Value", typeof(string)); // 统一存为string,避免类型转换失败 dataTable.Columns.Add("StatusCode", typeof(int)); foreach (var dp in dataPoints) { dataTable.Rows.Add(dp.Timestamp, dp.NodeId, dp.Value?.ToString(), (int)dp.StatusCode); } // 2. 批量写入(启用批处理,每1000行提交一次) using var bulkCopy = new SqlBulkCopy(connection) { DestinationTableName = "dbo.OpcUaData", BatchSize = 1000, BulkCopyTimeout = 30, EnableStreaming = true }; try { await bulkCopy.WriteToServerAsync(dataTable); return dataTable.Rows.Count; } catch (SqlException ex) when (ex.Number == -2) // 连接中断 { throw new DataPersistenceException("SQL Server连接中断,需重试", ex); } catch (Exception ex) { // 记录失败批次详情(含前10条数据),便于人工排查 _logger.LogError(ex, "BulkInsert失败,失败批次大小: {RowCount}", dataTable.Rows.Count); throw; } }

这里埋了两个实用技巧:

  • Value字段统一存为string:OPC UA中变量类型可能是Int32、Double、Boolean甚至ExtensionObject,强转为具体类型易抛异常。存字符串+后续分析时再解析,是工业现场最稳妥的做法;
  • EnableStreaming = true:避免大数据量时内存暴涨,尤其当dataPoints超过10万条时,此开关能降低30%以上内存占用。

3. 快速运行指南:5分钟从零启动,看到SQL Server里有数据

别被“OPC UA”、“SQL Server”这些词吓住——这个项目对新手极友好。只要你的目标UA服务器(如KEPServerEX、MatrikonOPC UA Server或任意支持OPC UA的PLC)已开启,且SQL Server实例可访问,整个流程就是三步:改配置 → 启动 → 看日志。

3.1 修改appsettings.json:填4个关键字段

打开项目根目录下的appsettings.json,只需修改以下4处(其余保持默认):

{ "OpcUa": { "EndpointUrl": "opc.tcp://192.168.1.100:49320", // ← 改成你的UA服务器IP和端口 "NodeIds": [ "ns=2;s=Channel1.Device1.Temperature", "ns=2;s=Channel1.Device1.Pressure" ], // ← 改成你要读的真实NodeId "PublishingIntervalMs": 1000, "SecurityPolicy": "None" // 若UA服务器启用了证书,改为 "Basic256Sha256" }, "SqlServer": { "ConnectionString": "Server=192.168.1.101;Database=OpcUaLog;User Id=sa;Password=YourStrong@Pass1;", // ← 改成你的SQL Server连接串 "TableName": "OpcUaData" } }

注意:NodeIds数组里的字符串必须与UA服务器中变量的完整NodeId完全一致。常见错误是漏掉ns=2;s=前缀,或把s=写成i=(ID类型错误)。建议先用UaExpert工具连上服务器,右键变量 → “Copy NodeId”,粘贴过来即可。

3.2 初始化SQL Server数据库:执行建表脚本

项目附带了CreateTable.sql脚本(位于/Scripts/目录)。用SSMS或sqlcmd执行:

-- 在目标数据库(如 OpcUaLog)中执行 CREATE TABLE dbo.OpcUaData ( Id BIGINT IDENTITY(1,1) PRIMARY KEY, Timestamp DATETIME2 NOT NULL, NodeId NVARCHAR(255) NOT NULL, Value NVARCHAR(MAX) NULL, StatusCode INT NOT NULL DEFAULT 0, CreatedAt DATETIME2 DEFAULT GETUTCDATE() ); CREATE INDEX IX_Timestamp_NodeId ON dbo.OpcUaData (Timestamp, NodeId);

提示:Value字段用NVARCHAR(MAX)是为了兼容任意UA变量值(包括Base64编码的二进制数据)。如果你确定只读数字量,可改为DECIMAL(18,6)并在C#层做类型转换,性能提升约15%。

3.3 启动客户端并验证:看三处日志确认成功

按F5启动WPF客户端(或运行OPCUAClient.exe),观察界面右下角日志框:

  • 第一行:[INFO] Connecting to OPC UA server at opc.tcp://192.168.1.100:49320...→ 表示开始连接;
  • 第二行:[SUCCESS] Session established. Subscribed to 2 nodes.→ 表示UA会话和订阅成功;
  • 第三行起:[DATA] Inserted 2 points for timestamp 2024-06-15T08:23:45.123Z→ 表示数据已写入SQL Server。

此时,立即去SQL Server中查表:

SELECT TOP 10 * FROM dbo.OpcUaData ORDER BY Id DESC;

如果看到Timestamp、NodeId、Value有真实值(如"25.6"、"120.3"),说明整个链路已通。恭喜,你已跑通工业数据采集的第一公里。


4. 常见问题排查:现场部署时踩过的5个坑,全给你标好解法

工业现场环境复杂,网络波动、权限不足、配置笔误都会导致启动失败。以下是我在3个不同客户现场部署时,反复遇到并固化进文档的5个高频问题。每一条都按「现象 → 原因 → 解决」给出可立即执行的动作。

4.1 现象:日志卡在Connecting to OPC UA server...后无响应,1分钟后报TimeoutException

原因:UA服务器未开启,或防火墙拦截了TCP端口(默认4840/49320),或EndpointUrl中IP写错(如写了localhost但程序跑在另一台机器)。

解决:

  • 在客户端机器上执行telnet 192.168.1.100 49320,若提示“无法打开到主机的连接”,说明网络不通;
  • 检查UA服务器是否运行(如KEPServerEX的服务状态);
  • 将EndpointUrl中的localhost全部替换为服务器真实局域网IP。

4.2 现象:连接成功,但日志显示Subscribed to 0 nodes,SQL表始终为空

原因:NodeIds配置错误——最常见的是复制了UaExpert里的“Display Name”(如Temperature),而非真正的NodeId(如ns=2;s=Channel1.Device1.Temperature)。

解决:

  • 在UaExpert中,展开地址空间树,找到目标变量 → 右键 → “Copy NodeId” → 粘贴到appsettings.json的NodeIds数组中;
  • 或用UaExpert的“Address Space View”面板,勾选“Show NodeId”,确保看到的是ns=x;s=xxx格式。

4.3 现象:SQL Server报错Login failed for user 'sa',或Cannot open database "OpcUaLog"

原因:连接字符串中的用户名密码错误,或数据库OpcUaLog不存在,或sa用户未启用。

解决:

  • 用SSMS以sa账户登录SQL Server,执行SELECT @@VERSION确认能连;
  • 执行CREATE DATABASE OpcUaLog;创建库;
  • 执行ALTER LOGIN sa ENABLE; ALTER LOGIN sa WITH PASSWORD = 'YourStrong@Pass1';启用并重置密码。

4.4 现象:日志频繁出现BulkInsert失败,但SQL表里有数据,只是偶尔断续

原因:BatchSize = 1000过大,导致单次写入超时(尤其当SQL Server磁盘慢或网络延迟高时)。

解决:

  • 打开appsettings.json,在SqlServer节点下添加"BatchSize": 200;
  • 重启客户端,观察日志是否变为稳定Inserted 200 points...。

4.5 现象:WPF界面显示连接成功,但SQL表里Value字段全是null

原因:UA服务器中该变量的Value属性为空(如未初始化),或StatusCode为BadWaitingForInitialData(等待初始值)。

解决:

  • 在UaExpert中读取同一变量,确认其Value和StatusCode是否正常;
  • 在C#代码中临时加一行日志:_logger.LogInformation("Raw value: {@Value}, Status: {Status}", dp.Value, dp.StatusCode);查看原始值;
  • 若确认UA侧正常,则检查DataPoint类中Value属性的赋值逻辑(通常在OnMonitoredItemNotification回调里)。

5. 二次开发实战:如何扩展支持自定义字段、历史数据回补、以及多UA服务器轮询

当你跑通基础功能后,下一步往往是适配真实产线需求。我总结了三个最高频的改造场景,并给出可直接抄的代码片段和验证方法。这些改动都不需要动核心通信层,全部在DataPersistence和Configuration层完成,改完编译即生效。

5.1 场景一:SQL表增加“设备ID”“产线编号”等业务字段

原始表只有NodeId,但产线管理需要知道这个温度值来自哪台设备、哪条产线。方案是:在配置中增加DeviceContext节点,写入时拼接到DataPoint对象。

步骤:

  1. 修改appsettings.json,新增DeviceContext:
"DeviceContext": { "EquipmentId": "EQP-001", "LineCode": "LINE-A", "Factory": "SHANGHAI" }
  1. 修改DataPoint模型类,增加属性:
public class DataPoint { public DateTime Timestamp { get; set; } public string NodeId { get; set; } public object Value { get; set; } public uint StatusCode { get; set; } public string EquipmentId { get; set; } // 新增 public string LineCode { get; set; } // 新增 public string Factory { get; set; } // 新增 }
  1. 在OnMonitoredItemNotification回调中,赋值:
var dp = new DataPoint { Timestamp = e.NotificationTime, NodeId = monitoredItem.StartNodeId, Value = e.Value?.Value, StatusCode = e.StatusCode, EquipmentId = _config.DeviceContext.EquipmentId, LineCode = _config.DeviceContext.LineCode, Factory = _config.DeviceContext.Factory }; _dataQueue.Enqueue(dp);
  1. 修改SqlBulkInserter.BulkInsertAsync()中的DataTable构建部分,加入新列,并同步更新SQL建表语句。

验证:查SQL表,确认新字段有值。这是产线数据打标签的基础,后续所有BI报表都依赖它。

5.2 场景二:启动时自动回补过去1小时的历史数据

有些场景要求“不能只读实时值”,比如设备重启后,要补上断连期间的数据。OPC UA标准支持HistoryRead服务,我们封装一个HistoricalDataLoader类:

public class HistoricalDataLoader { private readonly UaSessionManager _sessionManager; private readonly DateTime _startTime; public HistoricalDataLoader(UaSessionManager sessionManager, TimeSpan lookback = default) { _sessionManager = sessionManager; _startTime = DateTime.UtcNow.Subtract(lookback == default ? TimeSpan.FromHours(1) : lookback); } public async Task LoadAsync(List<string> nodeIds) { var historyReadRequest = new HistoryReadRequest { NodesToRead = nodeIds.Select(n => new HistoryReadValueId { NodeId = ExpandedNodeId.Parse(n), IndexRange = null, DataEncoding = new QualifiedName() }).ToArray(), HistoryReadDetails = new ReadRawModifiedDetails { StartTime = _startTime, EndTime = DateTime.UtcNow, NumValuesPerNode = 1000, ReturnBounds = false }, TimestampsToReturn = TimestampsToReturn.Both }; var response = await _sessionManager.Session.HistoryReadAsync(historyReadRequest); // 将response结果转换为DataPoint列表,调用BulkInsertAsync var dataPoints = ConvertToDataPoints(response); await _bulkInserter.BulkInsertAsync(dataPoints); } }

在客户端启动流程中,在StartSubscriptionAsync()之后调用它即可。注意:不是所有UA服务器都支持HistoryRead,调用前需用UaExpert测试。

5.3 场景三:同时连接3台UA服务器,按权重轮询写入同一张SQL表

产线有多个品牌PLC(西门子、罗克韦尔、倍福),需统一采集。方案是:配置多个OpcUaServer节点,用ConcurrentDictionary管理多个UaSessionManager实例,数据写入时自动带上ServerName字段。

关键改造点:

  • appsettings.json中OpcUa节点改为数组:
"OpcUaServers": [ { "Name": "Siemens-S7", "EndpointUrl": "opc.tcp://192.168.1.100:49320", "NodeIds": [ "ns=2;s=Temp" ] }, { "Name": "Rockwell-CLX", "EndpointUrl": "opc.tcp://192.168.1.101:49320", "NodeIds": [ "ns=3;s=Pressure" ] } ]
  • 启动时遍历数组,为每个服务器创建独立UaSessionManager,存入字典;
  • DataPoint新增ServerName字段,写入时自动填充。

从那以后我每次接到新产线需求,第一件事就是打开appsettings.json,把OpcUaServers数组补全,然后跑一遍HistoricalDataLoader—— 既验证连通性,又补上历史数据,省去人工核对环节。这套组合拳让我在3个客户现场交付时间平均缩短了1.5天。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询