OPC DA HTTP 转发 - Snet Docs

📘 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 retopcnamelistList<_opcnamelist>opcsvrnameclsid
ReqConnectToOPC opcname retopcnamestatus
ReqGetGroupsOfOPC opcname retopcnamegrouplistList<_grouplist>groupname
ReqGetALLItemsOfOPC opcname retitemlistList<_itemlist>itemname
ReqReConnectOfOPC opcname retopcnamestatus
ReqDisConnectOfOPC opcname retopcnamestatus
ReqDeleteConnectOfOPC opcname retopcnamestatus
ReqAddGroupOfOPC opcnamegroupname retopcnamestatusgroupname
ReqDelGroupOfOPC opcnamegroupname retopcnamestatusgroupname
ReqAddItemsOfGroup opcnamegroupnameitemlistList<_itemlist> retopcnamestatusgroupname
ReqGetItemsOfGroup opcnamegroupname retopcnamegroupnameitemlistList<_itemlist>itemname
ReqDelItemsOfGroup opcnamegroupnameitemlist retopcnamegroupname
ReqReadItemsValueOfGroup opcnamegroupname retopcnamegroupnameitemvaluesList<_itemvalues>itemnamevaluedatatypetimestampquality
ReqGetConnectStatusOfOPC opcname retopcnameconnectstatus
ReqWriteItemValue opcnamegroupnameitemnameitemvalue retopcnamegroupnameitemname

代码示例

通过 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/timestampReadAsync 会转换为类型化 AddressValue(含 Bool);直接用 Request 时保留字符串。

Q: ret 状态码含义? A: "1" 成功、"2" 请求失败、"3" JSON 格式错误,其他为未知。非 "1" 响应会使 RequestAsync 返回失败的 OperateResult

Q: 订阅是实时的吗? A: 是轮询——SubscribeOperateHandleInterval 毫秒调用一次 ReadAsync。调小间隔更新更快(CPU 占用更高),用 ChangeOut/AllOut 控制抛出哪些点位。

相关文档