目录

  1. 工程概述
  2. 环境依赖
  3. 项目文件结构
  4. UI 界面布局
  5. 核心概念
  6. 服务启动流程
  7. 地址空间架构
  8. 节点管理器详解
  9. 变量生命周期管理
  10. UI 与 OPC UA 数据同步
  11. 证书管理

1. 工程概述

本项目是一个基于 OPC Foundation .NET Standard SDK 的 OPC UA 服务端 WinForms 程序。

核心功能

  • 启动/停止 OPC UA 服务器(可配置端口和应用名)
  • 动态创建/删除/更新 OPC UA 变量节点
  • 500ms 轮询刷新 UI 显示(客户端写入自动同步)
  • 运行日志(终端风格黑底绿字)
  • 支持本地和远程客户端连接(UaExpert、自定义客户端等)

目标框架: .NET Framework 4.8
SDK 版本: OPCFoundation.NetStandard.Opc.Ua v1.5.376.244


2. 环境依赖

2.1 NuGet 包

Install-Package OPCFoundation.NetStandard.Opc.Ua.Server -Version 1.5.376.244
Install-Package OPCFoundation.NetStandard.Opc.Ua.Configuration -Version 1.5.376.244
Install-Package OPCFoundation.NetStandard.Opc.Ua.Security.Certificates -Version 1.5.376.244

Server 包会自动拉取 CoreClientBouncyCastleNewtonsoft.Json 等。

2.2 引用命名空间

using Opc.Ua;                  // OPC UA 基础类型
using Opc.Ua.Server;            // 服务端 API(StandardServer, CustomNodeManager2, INodeManager 等)
using Opc.Ua.Configuration;     // 应用配置(ApplicationConfiguration, CertificateFactory)

3. 项目文件结构

OPC_UA_Claude_Server/
├── Program.cs                    // 程序入口
├── OPC_UA_Server.cs             // ★ 主窗体(启停逻辑 + 变量管理 UI)★
├── OPC_UA_Server.Designer.cs    // 窗体控件声明
└── MyNodeManager.cs             // ★ 自定义节点管理器(地址空间核心)★

核心就两个文件:OPC_UA_Server.cs(UI 层)和 MyNodeManager.cs(OPC UA 地址空间层)。


4. UI 界面布局

┌──────────────────────────────────────────────────────────┐
│ 服务端控制                                                 │
│ 端口: [4841]  应用名: [MyOpcUaServer]                      │
│ [启动服务] [停止服务]  ● 已停止/运行中                      │
│ 端点: [opc.tcp://DESKTOP-XXX:4841]                        │
├─────────────────────────┬────────────────────────────────┤
│ 变量管理                 │ 运行日志                        │
│ ┌──────────┬──────┬───┐ │ ┌────────────────────────────┐ │
│ │ 变量名    │ 类型  │值 │ │ │ [10:30:01] 服务器已启动     │ │
│ │(只读表格) │      │   │ │ │ [10:30:15] 客户端已连接     │ │
│ │Tempera..│Int32│25 │ │ │ [10:30:20] 写入: Temp=30    │ │
│ └──────────┴──────┴───┘ │ └────────────────────────────┘ │
│                [删除选中] │                                │
│ ◆ 新建变量               │                                │
│   变量名:[__]类型:[Int32▼]│                                │
│   初始值:[__]    [添加]   │                                │
│ ◆ 更新变量值             │                                │
│   当前值:[___]新值:[___]  │                                │
│                 [更新]    │                                │
└─────────────────────────┴────────────────────────────────┘

5. 核心概念

5.1 OPC UA 服务端架构

┌──────────────────────────────────────────┐
│ StandardServer(服务端主类)              │
│   ├── 管理 TCP 监听器                     │
│   ├── 管理安全通道                         │
│   └── 管理会话                            │
├──────────────────────────────────────────┤
│ MasterNodeManager(主节点管理器)          │
│   ├── CoreNodeManager(核心命名空间 ns=0)│
│   │     └── Objects, Types, Views 等      │
│   ├── MyNodeManager(自定义命名空间 ns=2)│
│   │     └── MyVariables → 变量节点        │
│   └── 协调跨命名空间引用                   │
└──────────────────────────────────────────┘

5.2 地址空间树结构

Root (ns=0)
  ├── Views (ns=0, 核心管理)
  ├── Objects (ns=0, 核心管理)
  │     ├── Server (ns=0)
  │     └── My Variables (ns=2, 自定义节点管理器)  ← 通过跨空间引用挂载
  │           ├── Temperature (ns=2, Organizes 引用)
  │           ├── Pressure (ns=2)
  │           └── Status (ns=2)
  └── Types (ns=0)

5.3 关键对象说明

对象 类型 说明
m_server StandardServer OPC UA 服务端主实例
m_nodeManager MyNodeManager 自定义节点管理器
m_configuration ApplicationConfiguration 应用配置
FolderState FolderState 文件夹节点(容器)
BaseDataVariableState BaseDataVariableState 变量节点(可读写)
INodeManagerFactory INodeManagerFactory 工厂接口,StandardServer 通过它创建节点管理器
Organizes ReferenceTypeIds.Organizes 组织引用类型(文件夹→内容)

5.4 命名空间

OPC UA 每个节点管理器管理一个或多个命名空间。不同命名空间的节点通过"外部引用"链接:

命名空间索引 命名空间 URI 管理者
0 http://opcfoundation.org/UA/ CoreNodeManager(SDK 内置)
1 http://opcfoundation.org/UA/Diagnostics SDK 内置
2 http://myopcuaserver/MyNodeManager/ MyNodeManager(自定义)

5.5 NodeId 命名规则

本服务端变量 NodeId:
  ns=2;s=Temperature    → 命名空间2,字符串型 "Temperature"
  ns=2;s=Pressure       → 命名空间2,字符串型 "Pressure"
  ns=2;s=MyVariables    → 命名空间2,字符串型 "MyVariables"(根文件夹)

6. 服务启动流程

用户点击 [启动服务]
    │
    ├── 步骤1: BuildServerConfiguration()
    │     ├── 创建 ApplicationConfiguration
    │     │     ├── 应用名 + URI + 端口
    │     │     ├── 安全策略(None + SignAndEncrypt)
    │     │     ├── 证书配置
    │     │     └── 传输配额 + 线程池设置
    │     ├── config.Validate(ApplicationType.Server)
    │     └── FindOrCreateCertificate() → 创建/查找自签名证书
    │
    ├── 步骤2: 收集 DataGridView 变量
    │     遍历表格每一行 → 构建 List<VariableDef>
    │        VariableDef.Name   = 变量名 (如 "Temperature")
    │        VariableDef.Type   = BuiltInType.Int32
    │        VariableDef.Value  = 25
    │
    ├── 步骤3: 创建服务器并注册节点管理器
    │     m_server = new StandardServer()
    │     factory = new MyNodeManagerFactory(initialVars)
    │     m_server.AddNodeManager(factory)
    │
    ├── 步骤4: m_server.Start(m_configuration)
    │     内部流程:
    │       MasterNodeManager.Startup()
    │         → 创建 CoreNodeManager(管理 ns=0 核心节点)
    │         → factory.Create(server, config)
    │             → new MyNodeManager(server, config, initialVars)
    │         → nodeManager.CreateAddressSpace(externalReferences)
    │             → base.CreateAddressSpace(externalReferences)
    │                 → LoadPredefinedNodes(context)
    │                     → 我们的重写:创建 MyVariables 文件夹 + 变量节点
    │                 → AddPredefinedNode(每个节点)  ← 递归处理子节点
    │                 → AddReverseReferences(externalReferences)
    │                     → 扫描节点引用 → 发现跨命名空间引用
    │                     → 填充 externalReferences 字典
    │         → 处理 externalReferences(核心里添加 Organizes 引用)
    │       → 启动 TCP 监听器
    │
    └── 步骤5: 更新 UI + 启动刷新定时器

6.1 BuildServerConfiguration — 构建配置

private void BuildServerConfiguration(string appName, int port)
{
    string machineName = Environment.MachineName;

    m_configuration = new ApplicationConfiguration
    {
        ApplicationName = appName,
        ApplicationUri = $"urn:{machineName}:{appName}",
        ApplicationType = ApplicationType.Server,   // ★ 标记为服务端
        ProductUri = "https://github.com",

        SecurityConfiguration = new SecurityConfiguration
        {
            AutoAcceptUntrustedCertificates = false,
            ApplicationCertificate = new CertificateIdentifier
            {
                StoreType = CertificateStoreType.X509Store,
                StorePath = "CurrentUser\\My",        // 证书存 Windows 个人存储
                SubjectName = $"CN={appName}"
            },
            TrustedPeerCertificates = new CertificateTrustList
            {
                StoreType = CertificateStoreType.X509Store,
                StorePath = "CurrentUser\\Root"        // 信任客户端证书的位置
            }
        },

        TransportQuotas = new TransportQuotas
        {
            OperationTimeout = 360000,
            MaxStringLength = 67108864
        },

        // ★ 服务端专属配置 ★
        ServerConfiguration = new ServerConfiguration
        {
            // 监听地址:opc.tcp://主机名:端口
            BaseAddresses = new StringCollection
            {
                $"opc.tcp://{machineName}:{port}"
            },

            // 支持的安全策略
            SecurityPolicies = new ServerSecurityPolicyCollection
            {
                new ServerSecurityPolicy
                {
                    SecurityMode = MessageSecurityMode.None,
                    SecurityPolicyUri =
                        "http://opcfoundation.org/UA/SecurityPolicy#None"
                },
                new ServerSecurityPolicy
                {
                    SecurityMode = MessageSecurityMode.SignAndEncrypt,
                    SecurityPolicyUri =
                        "http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256"
                }
            },

            MinRequestThreadCount = 5,
            MaxRequestThreadCount = 100,
            MaxQueuedRequestCount = 200
        }
    };

    // 验证配置 + 创建证书
    m_configuration.Validate(ApplicationType.Server);
    FindOrCreateCertificate(appName);
}

6.2 服务端证书

服务端必须有自己的 X.509 证书用于 TLS 握手和身份标识。逻辑与客户端创建证书相同:

private void FindOrCreateCertificate(string appName)
{
    var certId = m_configuration.SecurityConfiguration.ApplicationCertificate;
    var certTask = certId.Find(true);
    certTask.Wait();

    if (certTask.Result != null) return;  // 已有证书

    // 创建 RSA 2048 位自签名证书,有效期 24 个月
    var localIps = new List<string>();
    // ... 收集本机 IP ...
    var builder = CertificateFactory.CreateCertificate(
        m_configuration.ApplicationUri, appName, null, localIps);

    X509Certificate2 newCert = builder
        .SetNotBefore(DateTime.Now)
        .SetNotAfter(DateTime.Now.AddMonths(24))
        .SetHashAlgorithm(X509Utils.GetRSAHashAlgorithmName(256))
        .SetRSAKeySize(2048)
        .CreateForRSA();

    newCert.FriendlyName = appName;
    newCert.AddToStore("X509Store", "CurrentUser\\My", null);
}

6.3 启动服务器

// 从 DataGridView 收集初始变量
var initialVars = new List<VariableDef>();
foreach (DataGridViewRow row in Variable_dGV.Rows)
{
    if (row.IsNewRow) continue;
    initialVars.Add(new VariableDef
    {
        Name = row.Cells[0].Value?.ToString(),
        Type = MyNodeManager.MapBuiltInType(row.Cells[1].Value?.ToString()),
        Value = MyNodeManager.ConvertValue(row.Cells[2].Value?.ToString(), type)
    });
}

// 创建服务器 + 注册节点管理器工厂 + 启动
m_server = new StandardServer();
var factory = new MyNodeManagerFactory(initialVars);
m_server.AddNodeManager(factory);    // ← 注册工厂(不是节点管理器本身!)
m_server.Start(m_configuration);     // ← 内部:factory.Create → CreateAddressSpace
m_nodeManager = factory.NodeManager; // ← 启动后才能拿到节点管理器实例

关键注意:

  • AddNodeManager() 参数类型是 INodeManagerFactory,不是 INodeManager
  • 真正的节点管理器在 server.Start() 内部通过工厂创建
  • 启动后才能从 factory.NodeManager 获取实例引用

7. 地址空间架构

7.1 CustomNodeManager2 继承链

INodeManager (接口)
  └── CustomNodeManager2 (SDK 提供的简化基类)
        └── MyNodeManager (我们的实现)

CustomNodeManager2 提供了:

  • AddPredefinedNode() — 将节点注册到地址空间
  • AddReverseReferences() — 自动处理跨命名空间引用
  • PredefinedNodes — 节点存储字典(NodeId → NodeState)
  • SystemContext — 服务端系统上下文
  • Lock — 线程安全锁
  • NamespaceIndex — 当前命名空间索引

7.2 节点创建流程(官方标准模式)

LoadPredefinedNodes(context)          ← 我们重写此方法
  ↓
返回 NodeStateCollection(包含所有要创建的节点)
  ↓
SDK 框架对每个节点调用 AddPredefinedNode()
  ├── 节点加入 PredefinedNodes 字典
  ├── 如果是类型节点 → 注册到类型树
  └── 递归处理子节点
  ↓
SDK 框架调用 AddReverseReferences(externalReferences)
  ├── 扫描所有 PredefinedNodes 的引用
  ├── 发现跨命名空间引用 → 添加到 externalReferences
  └── 核心节点管理器读取 externalReferences 添加引用

7.3 引用类型对比

引用类型 用途 浏览可见?
Organizes 文件夹→内容(组织关系) 是(HierarchicalReferences)
HasComponent 对象→组件(包含关系) 是(HierarchicalReferences子类型)
HasProperty 对象→属性

使用 Organizes:本服务端使用 Organizes 管理文件夹和变量之间的层级关系。这是 OPC UA 标准推荐的方式:

  • MyVariables → Organizes(inverse) → Objects (MyVariables 是 Objects 的子节点)
  • MyVariables → Organizes(forward) → Temperature (Temperature 是 MyVariables 的子节点)

8. 节点管理器详解

8.1 完整代码(带注释)

public class MyNodeManager : CustomNodeManager2
{
    private Dictionary<string, BaseDataVariableState> m_variables;
    private FolderState m_rootFolder;
    private List<VariableDef> m_initialVars;

    // ★ 构造函数通过 base() 注册命名空间 URI ★
    public MyNodeManager(IServerInternal server, ApplicationConfiguration config,
        List<VariableDef> initialVars)
        : base(server, config, "http://myopcuaserver/MyNodeManager/")
    {
        m_variables = new Dictionary<string, BaseDataVariableState>();
        m_initialVars = initialVars ?? new List<VariableDef>();
    }

    // ★ 核心方法:SDK 框架调用,返回要创建的所有节点 ★
    protected override NodeStateCollection LoadPredefinedNodes(ISystemContext context)
    {
        var nodes = new NodeStateCollection();

        // 1. 创建根文件夹 "My Variables"
        m_rootFolder = new FolderState(null)  // null = 顶层节点
        {
            NodeId = new NodeId("MyVariables", NamespaceIndex),
            BrowseName = new QualifiedName("MyVariables", NamespaceIndex),
            DisplayName = new LocalizedText("My Variables"),
            TypeDefinitionId = ObjectTypeIds.FolderType
        };
        // ★ 反向引用:MyVariables → Organizes(inverse) → Objects
        // 这样浏览 Objects 时能看到 MyVariables 文件夹
        m_rootFolder.AddReference(
            ReferenceTypeIds.Organizes, true, ObjectIds.ObjectsFolder);
        nodes.Add(m_rootFolder);

        // 2. 创建变量节点
        foreach (var def in m_initialVars)
        {
            // ★ 重要:先 null 父节点创建,设好 NodeId,再手动加引用
            // 不能 new BaseDataVariableState(parent),因为构造器内
            // parent.AddChild(this) 会在 C# 初始化器赋值 NodeId 之前执行
            var variable = new BaseDataVariableState(null)
            {
                NodeId = new NodeId(def.Name, NamespaceIndex),
                BrowseName = new QualifiedName(def.Name, NamespaceIndex),
                DisplayName = new LocalizedText(def.Name),
                DataType = (uint)def.Type,           // 数据类型
                Value = def.Value,                   // 初始值
                ValueRank = ValueRanks.Scalar,        // 标量(非数组)
                AccessLevel = AccessLevels.CurrentReadOrWrite,
                UserAccessLevel = AccessLevels.CurrentReadOrWrite
            };

            // ★ 双向引用:MyVariables ← Organizes → variable ★
            m_rootFolder.AddReference(
                ReferenceTypeIds.Organizes, false, variable.NodeId);
            variable.AddReference(
                ReferenceTypeIds.Organizes, true, m_rootFolder.NodeId);

            nodes.Add(variable);
            m_variables[def.Name] = variable;
        }

        return nodes;
    }

    // ★ CreateAddressSpace 只需要调用 base,框架内部处理所有逻辑 ★
    public override void CreateAddressSpace(
        IDictionary<NodeId, IList<IReference>> externalReferences)
    {
        lock (Lock) { base.CreateAddressSpace(externalReferences); }
    }
}

8.2 关键坑点:NodeId 初始化顺序

// ★★★ 错误写法 ★★★
// C# 初始化器 { } 在构造函数之后执行,但 BaseDataVariableState(parent)
// 构造函数内部就调用了 parent.AddChild(this),此时 NodeId 还是默认值!
var variable = new BaseDataVariableState(m_rootFolder)  // ← 构造器调用 AddChild(NodeId=null)
{
    NodeId = new NodeId("Temperature", NamespaceIndex), // ← 初始化器:此时才赋值 NodeId
    Value = 25
};

// ★★★ 正确写法 ★★★
// 先 null 父节点创建(不会触发 AddChild),设好所有属性,再手动挂到父节点
var variable = new BaseDataVariableState(null)  // ← 构造器不调用 AddChild(父为 null)
{
    NodeId = new NodeId("Temperature", NamespaceIndex), // ← 赋值完成
    Value = 25
};
// 手动添加引用(此时 NodeId 已经正确赋值)
m_rootFolder.AddReference(ReferenceTypeIds.Organizes, false, variable.NodeId);
variable.AddReference(ReferenceTypeIds.Organizes, true, m_rootFolder.NodeId);

8.3 跨命名空间引用机制

我们的节点 (ns=2)               核心节点 (ns=0)
     │                              │
     │  m_rootFolder.AddReference   │
     │  (Organizes, true,           │
     │   ObjectIds.ObjectsFolder)   │
     │─────────────────────────────→│  标记了"MyVariables 属于 Objects"
     │                              │
     │  AddReverseReferences 扫描    │
     │  发现这个跨空间引用           │
     │                              │
     │  externalReferences 字典      │
     │  [ObjectsFolder] = [         │
     │    Organizes(forward) →      │
     │    MyVariables               │
     │  ]                           │
     │                              │
     │  MasterNodeManager 调用       │
     │  CoreNodeManager.            │
     │  AddReferences(              │
     │    externalReferences)        │
     │─────────────────────────────→│  Objects 添加了
     │                              │  Organizes → MyVariables
     │                              │
     ↓                              ↓
  客户端浏览 Objects 时能看到 MyVariables 文件夹!

9. 变量生命周期管理

9.1 启动时创建(LoadPredefinedNodes)

服务启动时,SDK 调用 LoadPredefinedNodes() → 创建所有初始变量。

9.2 运行时动态添加(AddVariable)

服务器运行期间可以动态添加变量:

public void AddVariable(string name, BuiltInType dataType, object initialValue)
{
    lock (Lock)
    {
        if (m_variables.ContainsKey(name)) return;

        var variable = new BaseDataVariableState(null)
        {
            NodeId = new NodeId(name, NamespaceIndex),
            // ... 设置属性
        };
        // 挂到 MyVariables 文件夹
        m_rootFolder.AddReference(
            ReferenceTypeIds.Organizes, false, variable.NodeId);
        variable.AddReference(
            ReferenceTypeIds.Organizes, true, m_rootFolder.NodeId);

        // ★ 注册到地址空间(使客户端可发现)
        AddPredefinedNode(SystemContext, variable);
        m_variables[name] = variable;
    }
}

9.3 运行时更新值(UpdateVariableValue)

public void UpdateVariableValue(string name, object value)
{
    lock (Lock)
    {
        if (m_variables.TryGetValue(name, out var variable))
        {
            variable.Value = value;
            // ★ 清除变更掩码 → 通知所有订阅此变量的客户端 ★
            variable.ClearChangeMasks(SystemContext, false);
        }
    }
}

9.4 运行时删除(RemoveVariable)

public void RemoveVariable(string name)
{
    lock (Lock)
    {
        if (m_variables.TryGetValue(name, out var variable))
        {
            PredefinedNodes.Remove(variable.NodeId);  // 从地址空间移除
            m_variables.Remove(name);                 // 从本地字典移除
        }
    }
}

10. UI 与 OPC UA 数据同步

10.1 数据流向

┌───────────────────────────────────────────────────────┐
│                     服务端 UI                          │
│                                                        │
│  服务端手动更新值                                       │
│  [更新] 按钮                                            │
│    → m_nodeManager.UpdateVariableValue(name, value)    │
│    → 客户端订阅者自动收到推送                            │
│                                                        │
│  客户端写入数据                                         │
│  → OPC UA 栈自动更新节点值                              │
│  → 500ms 定时器轮询                                      │
│  → RefreshTimer_Tick()                                  │
│  → m_nodeManager.GetVariableValue(name)                  │
│  → 更新 DataGridView                                    │
└───────────────────────────────────────────────────────┘

10.2 轮询定时器

// 构造器中初始化
m_refreshTimer = new Timer();
m_refreshTimer.Interval = 500;  // 每 500ms 刷新一次
m_refreshTimer.Tick += RefreshTimer_Tick;
m_refreshTimer.Start();  // 服务启动时开始
m_refreshTimer.Stop();   // 服务停止时停止

// 定时器回调
private void RefreshTimer_Tick(object sender, EventArgs e)
{
    if (m_nodeManager == null) return;

    foreach (DataGridViewRow row in Variable_dGV.Rows)
    {
        if (row.IsNewRow) continue;
        string name = row.Cells[0].Value?.ToString();
        if (string.IsNullOrEmpty(name)) continue;

        // 从 OPC UA 节点读取当前值
        object val = m_nodeManager.GetVariableValue(name);
        string display = val?.ToString() ?? "";
        if (val is bool b) display = b ? "True" : "False";

        // 有变化才更新(减少闪烁)
        if (row.Cells[2].Value?.ToString() != display)
            row.Cells[2].Value = display;
    }
}

10.3 DataGridView 设为只读

this.Variable_dGV.ReadOnly = true;  // 防止用户直接在表格编辑

变量的值只能通过"更新变量值"区域的按钮更新,或者通过 OPC UA 客户端写入后由定时器同步。


11. 证书管理

11.1 证书存储位置

证书类型 Windows 存储路径 说明
服务端证书 CurrentUser\My 标识服务端身份的自签名证书
受信任的客户端 CurrentUser\Root 被信任的客户端证书

可通过 certmgr.msc 查看和管理证书。

11.2 防火墙配置

服务端监听 TCP 端口,需要 Windows 防火墙允许入站连接:

netsh advfirewall firewall add rule name="OPC UA Server 4841" dir=in action=allow protocol=TCP localport=4841

12. 完整代码清单

MyNodeManager.cs — 自定义节点管理器

核心要点:

  1. 继承 CustomNodeManager2
  2. 构造函数通过 base(server, config, "namespaceUri") 注册命名空间
  3. 重写 LoadPredefinedNodes(context) 返回 NodeStateCollection
  4. 变率先用 null 父节点创建,再手动加 Organizes 引用
  5. 重写 CreateAddressSpace 只需调用 base.CreateAddressSpace

MyNodeManagerFactory — 工厂类

public class MyNodeManagerFactory : INodeManagerFactory
{
    private List<VariableDef> m_initialVars;
    public MyNodeManager NodeManager { get; private set; }

    public MyNodeManagerFactory(List<VariableDef> initialVars)
    {
        m_initialVars = initialVars;
    }

    // ★ 返回命名空间 URI 列表(服务端据此注册命名空间)
    public StringCollection NamespacesUris
    {
        get { return new StringCollection { "http://myopcuaserver/MyNodeManager/" }; }
    }

    // ★ SDK 框架调用:创建节点管理器实例
    public INodeManager Create(IServerInternal server, ApplicationConfiguration config)
    {
        NodeManager = new MyNodeManager(server, config, m_initialVars);
        return NodeManager;
    }
}
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐