📘 OPC DA 客户端(HTTP 转发)
命名空间: Snet.Opc.da.http | 类: OpcDaHttpOperate | 基类: DaqAbstract<OpcDaHttpOperate, OpcDaHttpData.Basics> | 接口: IDaq
基于纯 HTTP 的 OPC DA 客户端——无需 COM/DCOM。它连接 OPC DA 服务 HTTP 转发工具:一个运行在 OPC DA 服务端旁边的小型 Windows 工具,把 DA 服务的组、点位与值以 REST 风格 JSON 接口暴露出来。适用于 Web 服务器、容器、Linux 主机与跨机器等无法使用 DCOM 的场景。
与其余 OPC 客户端一样,OpcDaHttpOperate 是一个标准 IDaq 驱动——HTTP 桥接完全封装在内部。用 OnAsync 连接、用 ReadAsync/WriteAsync 读写点位、用 SubscribeAsync 订阅数据变化,用法与 OPC UA 客户端 完全一致。
https://shunnet.lanzoum.com/b0mcq2m0f (密码:4u3m)
在承载 OPC DA 服务端的 Windows 机器上运行该工具,配置工具内的 DA 服务连接,然后将 OpcDaHttpOperate 指向工具的 IP 与端口。客户端操作前,工具必须已运行并连接上 DA 服务。
快速开始
using Snet.Opc.da.http;
using Snet.Model.data;
using Snet.Model.@enum;
using System.Collections.Concurrent;
// 创建并连接——AutoOperate = true 时 OnAsync 自动创建组 "Group1" 并添加服务端全部点位
var http = await OpcDaHttpOperate.InstanceAsync(new OpcDaHttpData.Basics
{
IpAddress = "127.0.0.1", // 转发工具 IP
Port = 6688, // 转发工具端口
ServerName = "Knight.OPC.Server.Demo", // 工具内的 DA 服务名
GroupName = "Group1", // 本客户端使用的 DA 组
RequestType = OpcDaHttpData.RequestType.http,
AutoOperate = true
});
await http.OnAsync();
// 批量读取——AddressName 为 OPC DA 点位 id(标签路径)
var address = new Address(new List<AddressDetails>
{
new("Temperature", DataType.Float),
new("Pressure", DataType.Float)
});
var result = await http.ReadAsync(address);
if (result.Status)
{
// ResultData 是以点位名为键的字典
var data = result.GetSource<ConcurrentDictionary<string, AddressValue>>();
foreach (var kv in data)
Console.WriteLine($"{kv.Key} = {kv.Value.ResultValue}");
}
// 写入——字典键为点位 id
var writeResult = await http.WriteAsync(new ConcurrentDictionary<string, object>
{
["Channel1.Device1.Tag1"] = 100.5f
});
// 订阅(轮询式)——先绑定事件,再订阅
http.OnDataEventAsync += async (sender, e) =>
{
if (e.Status)
{
var data = e.GetSource<ConcurrentDictionary<string, AddressValue>>();
foreach (var kv in data)
Console.WriteLine($"事件数据: {kv.Key} = {kv.Value.ResultValue}");
}
};
await http.SubscribeAsync(new Address(new AddressDetails("Temperature", DataType.Float)));
await http.DisposeAsync();
安装
dotnet add package Snet.Opc
客户端只需能通过 HTTP(S) 访问转发工具——无需 COM、无 x86 要求、无需 DCOM 配置。
配置(OpcDaHttpData.Basics)
Basics 继承 SubscribeData.SCData(轮询订阅参数)。
| 参数 | 类型 | 默认值 | 描述 |
|---|---|---|---|
SN |
string |
Guid.NewGuid().ToUpperNString() |
唯一标识 |
IpAddress |
string? |
"127.0.0.1" |
HTTP 转发工具 IP(正则校验 IPv4) |
Port |
int |
6688 |
HTTP 转发工具端口 |
GroupName |
string |
"Snet" |
读写/订阅使用的 OPC DA 组名 |
ServerName |
string |
"Knight.OPC.Server.Demo" |
目标 OPC DA 服务名(必须存在于转发工具的服务清单中,必填) |
AutoOperate |
bool |
true |
OnAsync 时自动连接、获取全部点位、创建组并添加点位。若工具中组和点位已创建则设为 false |
RequestType |
RequestType |
http(枚举默认值) |
协议:http / https |
HandleInterval |
int |
1000 |
(继承)轮询间隔——每个订阅周期调用 ReadAsync 的频率(毫秒) |
ChangeOut |
bool |
true |
(继承)true = 只抛变化项;false = 每周期抛全部值 |
AllOut |
bool |
false |
(继承)当 ChangeOut = true 时:true = 未变化项与变化项一同抛出,保证批次数据完整 |
TaskNumber |
int |
5 |
(继承)轮询任务数量 |
API 参考
IDaq 生命周期(继承)
| 方法 | 描述 |
|---|---|
Task<OperateResult> OnAsync(CancellationToken token = default) |
确认 ServerName 存在于工具的服务清单并连接。AutoOperate = true 时还会获取全部点位、创建 GroupName 组并添加点位 |
Task<OperateResult> OffAsync(bool hardClose = false, CancellationToken token = default) |
停止轮询订阅并断开与 DA 服务的连接 |
Task<OperateResult> ReadAsync(Address address, CancellationToken token = default) |
读取 GroupName 组的点位值(断开时自动重连)并映射为 AddressValue(Bool 从 HTTP 字符串形式转换)。ResultData:以点位名为键的 ConcurrentDictionary<string, AddressValue> |
Task<OperateResult> WriteAsync(ConcurrentDictionary<string, (object value, EncodingType? encodingType)> values, CancellationToken token = default) |
按点位名逐项写入(值以字符串形式发送) |
Task<OperateResult> SubscribeAsync(Address address, CancellationToken token = default) |
基于 SubscribeOperate 的轮询订阅——每 HandleInterval 毫秒调用 ReadAsync 并触发 OnDataEventAsync。先绑定事件 |
Task<OperateResult> UnSubscribeAsync(Address address, CancellationToken token = default) |
移除轮询订阅(未建立订阅时返回失败) |
Task<OperateResult> GetStatusAsync(CancellationToken token = default) |
OnAsync 成功后返回 true("已连接") |
Task<OperateResult> GetBaseObjectAsync(CancellationToken token = default) |
始终失败("无底层公共对象")——HTTP 桥接背后没有原始对象 |
原始 HTTP 请求入口(高级)
Request / RequestAsync 直接暴露转发工具的 REST 接口。每次调用为向 {RequestType}://{IpAddress}:{Port}/{requestApi} 发送的 JSON POST;成功(ret = "1")时 ResultData 为类型化 Response 对象,否则按 ret 码映射失败消息(2 = 请求失败,3 = JSON 格式错误)。
| 方法 | 描述 |
|---|---|
OperateResult Request(RequestMethod requestApi, object? Param = null) |
同步请求包装 |
Task<OperateResult> RequestAsync(RequestMethod requestApi, object? Param = null, CancellationToken token = default) |
发送一条原始 HTTP 请求(Param 为请求体:structure 类实例或任意可 JSON 序列化对象) |
请求方法(RequestMethod 枚举 — 15 个值)
public enum RequestMethod
{
ReqOPCServerNameList, // 请求OPC服务名清单
ReqConnectToOPC, // 请求连接某个OPC服务
ReqGetGroupsOfOPC, // 请求获取指定OPC下已添加的组名清单
ReqGetALLItemsOfOPC, // 请求浏览某个opc服务下的所有标签列表
ReqReConnectOfOPC, // 请求重连某个OPC服务
ReqDisConnectOfOPC, // 请求断开与某个OPC服务的连接
ReqDeleteConnectOfOPC, // 请求删除与某个OPC服务的连接(该连接下已经建立的组和标签都会被删除)
ReqAddGroupOfOPC, // 请求添加组
ReqDelGroupOfOPC, // 请求删除组(该组下已经添加的点位都会被删除)
ReqAddItemsOfGroup, // 请求批量在某个组下添加标签
ReqGetItemsOfGroup, // 请求获取指定组下已添加的标签名清单
ReqDelItemsOfGroup, // 请求批量删除指定组下的指定标签
ReqReadItemsValueOfGroup, // 请求批量读取某个组下的标签的值
ReqGetConnectStatusOfOPC, // 请求获取与某个OPC的连接状态
ReqWriteItemValue // 请求写某个标签的值
}
请求/响应结构(Snet.Opc.da.http.structure)
所有请求类遵循 ReqXxx.Request(字段名保持源码小写形式)与 ReqXxx.Response(首字段 ret)的模式。
| 方法 | 请求字段 | 响应字段 |
|---|---|---|
ReqOPCServerNameList |
— | ret、opcnamelist(List<_opcnamelist>:opcsvrname、clsid) |
ReqConnectToOPC |
opcname |
ret、opcname、status |
ReqGetGroupsOfOPC |
opcname |
ret、opcname、grouplist(List<_grouplist>:groupname) |
ReqGetALLItemsOfOPC |
opcname |
ret、itemlist(List<_itemlist>:itemname) |
ReqReConnectOfOPC |
opcname |
ret、opcname、status |
ReqDisConnectOfOPC |
opcname |
ret、opcname、status |
ReqDeleteConnectOfOPC |
opcname |
ret、opcname、status |
ReqAddGroupOfOPC |
opcname、groupname |
ret、opcname、status、groupname |
ReqDelGroupOfOPC |
opcname、groupname |
ret、opcname、status、groupname |
ReqAddItemsOfGroup |
opcname、groupname、itemlist(List<_itemlist>) |
ret、opcname、status、groupname |
ReqGetItemsOfGroup |
opcname、groupname |
ret、opcname、groupname、itemlist(List<_itemlist>:itemname) |
ReqDelItemsOfGroup |
opcname、groupname、itemlist |
ret、opcname、groupname |
ReqReadItemsValueOfGroup |
opcname、groupname |
ret、opcname、groupname、itemvalues(List<_itemvalues>:itemname、value、datatype、timestamp、quality) |
ReqGetConnectStatusOfOPC |
opcname |
ret、opcname、connectstatus |
ReqWriteItemValue |
opcname、groupname、itemname、itemvalue |
ret、opcname、groupname、itemname |
代码示例
通过 IDaq 批量读写
// 混合数据类型批量读取
var mixed = new Address(new List<AddressDetails>
{
new("Tag1", DataType.Float),
new("Tag2", DataType.Int),
new("Alarm", DataType.Bool)
});
var read = await http.ReadAsync(mixed);
if (read.Status)
{
var data = read.GetSource<ConcurrentDictionary<string, AddressValue>>();
foreach (var kv in data)
Console.WriteLine($"{kv.Key} = {kv.Value.ResultValue}");
}
// 按点位分别写值
var write = await http.WriteAsync(new ConcurrentDictionary<string, object>
{
["Channel1.Device1.Tag1"] = 99.5f,
["Channel1.Device1.Tag2"] = 42
});
手动请求流程(AutoOperate = false)
当工具中组和点位已创建时,使用原始请求入口:
var http = await OpcDaHttpOperate.InstanceAsync(new OpcDaHttpData.Basics
{
IpAddress = "127.0.0.1",
Port = 6688,
ServerName = "Knight.OPC.Server.Demo",
GroupName = "Group1",
AutoOperate = false, // 工具中组和点位已创建
RequestType = OpcDaHttpData.RequestType.http
});
await http.OnAsync(); // 仅连接
// 显式添加组与点位
await http.RequestAsync(RequestMethod.ReqAddGroupOfOPC, new ReqAddGroupOfOPC.Request { opcname = "Knight.OPC.Server.Demo", groupname = "Group1" });
await http.RequestAsync(RequestMethod.ReqAddItemsOfGroup, new ReqAddItemsOfGroup.Request
{
opcname = "Knight.OPC.Server.Demo",
groupname = "Group1",
itemlist = new List<_itemlist> { new() { itemname = "Channel1.Device1.Tag1" } }
});
// 原始读取组值
var read = await http.RequestAsync(RequestMethod.ReqReadItemsValueOfGroup,
new ReqReadItemsValueOfGroup.Request { opcname = "Knight.OPC.Server.Demo", groupname = "Group1" });
var values = read.ResultData.GetSource<ReqReadItemsValueOfGroup.Response>();
foreach (var v in values.itemvalues)
Console.WriteLine($"{v.itemname} = {v.value} ({v.datatype}) quality={v.quality}");
查看工具已知的服务清单
var serversResult = await http.RequestAsync(RequestMethod.ReqOPCServerNameList);
if (serversResult.Status)
{
var list = serversResult.ResultData.GetSource<ReqOPCServerNameList.Response>();
foreach (var s in list.opcnamelist)
Console.WriteLine($"{s.opcsvrname} ({s.clsid})");
}
常见问题
Q: 转发工具从哪里下载?
A: https://shunnet.lanzoum.com/b0mcq2m0f(密码 4u3m)。运行在承载 OPC DA 服务端的 Windows 机器上,连接 DA 服务并配置 HTTP 端口;客户端连接工具的 IP/端口。
Q: OnAsync 提示"服务名不存在"。
A: ServerName 必须与 ReqOPCServerNameList 返回的名称完全一致——即转发工具已枚举/配置的 DA 服务名。
Q: 什么时候 AutoOperate 设为 false?
A: 当工具中组与点位已创建(例如其他客户端或上次运行已创建)时。AutoOperate = true 时客户端会自动重建组 GroupName 并添加服务端全部点位。
Q: ReadAsync 读的是哪个组?
A: basics.GroupName ——本客户端创建/使用的组。传入 ReadAsync 的所有点位必须存在于该组中。
Q: 值返回的是字符串?
A: 是——工具返回的 value 为字符串,并带 datatype/quality/timestamp。ReadAsync 会转换为类型化 AddressValue(含 Bool);直接用 Request 时保留字符串。
Q: ret 状态码含义?
A: "1" 成功、"2" 请求失败、"3" JSON 格式错误,其他为未知。非 "1" 响应会使 RequestAsync 返回失败的 OperateResult。
Q: 订阅是实时的吗?
A: 是轮询——SubscribeOperate 每 HandleInterval 毫秒调用一次 ReadAsync。调小间隔更新更快(CPU 占用更高),用 ChangeOut/AllOut 控制抛出哪些点位。
相关文档
- OPC DA 客户端(COM/DCOM) — 直连同一批 DA 服务端
- OPC 协议族 — 四个 OPC 组件
- DaqAbstract 基类 — 生命周期与同步包装
- IDaq 接口 · 地址模型
