手把手搭建OPC UA服务器

 NodeManager.cs - 节点管理核心

using Opc.Ua;
using Opc.Ua.Server;

namespace OpcUaServerDemo
{
    internal class NodeManager : CustomNodeManager2
    {
        private FolderState _rootFolder;
        private string _rootNodeName;

        public NodeManager(IServerInternal server, ApplicationConfiguration configuration, string rootNodeName, params string[] namespaceUris)
            : base(server, configuration, namespaceUris)
        {
            _rootNodeName = rootNodeName;
        }

        public NodeManager(IServerInternal server, params string[] namespaceUris)
            : base(server, namespaceUris)
        {
            _rootNodeName = "opcdata"; // 默认值
        }

        protected override NodeStateCollection LoadPredefinedNodes(ISystemContext context)
        {
            _rootFolder = CreateFolder(null, null, _rootNodeName);
            _rootFolder.AddReference(ReferenceTypes.Organizes, true, ObjectIds.ObjectsFolder); // 将节点添加到服务器根节点
            _rootFolder.EventNotifier = EventNotifiers.SubscribeToEvents;
            AddRootNotifier(_rootFolder);

            return new NodeStateCollection(new List<NodeState> { _rootFolder });
        }
        public NodeId? GetRootNodeId()
        {
            if (_rootFolder != null)
            {
                return _rootFolder.NodeId;
            }

            return null;
        }

        public FolderState? GetRootFolder()
        {
            return _rootFolder;  // 直接返回保存的根节点对象
        }

        protected virtual FolderState CreateFolder(NodeState? parent, string? path, string name, string displayName = "")
        {
            if (string.IsNullOrWhiteSpace(path))
                path = parent?.NodeId.Identifier is string id ? id + "/" + name : name;
            string finalDisplayName = string.IsNullOrEmpty(displayName) ? name : displayName;
            FolderState folder = new FolderState(parent);
            folder.SymbolicName = name;
            folder.ReferenceTypeId = ReferenceTypes.Organizes;
            folder.TypeDefinitionId = ObjectTypeIds.FolderType;
            folder.NodeId = new NodeId(path, NamespaceIndex);
            folder.BrowseName = new QualifiedName(name, NamespaceIndex);
            folder.DisplayName = new LocalizedText("en", finalDisplayName);
            folder.WriteMask = AttributeWriteMask.None;
            folder.UserWriteMask = AttributeWriteMask.None;
            folder.EventNotifier = EventNotifiers.None;

            if (parent != null)
            {
                parent.AddChild(folder);
            }

            return folder;
        }

        protected virtual BaseDataVariableState CreateVariable(NodeState? parent, string? path, string name, BuiltInType dataType, int valueRank)
        {
            return CreateVariable(parent, path, name, (uint)dataType, valueRank);
        }

        protected virtual BaseDataVariableState CreateVariable(NodeState? parent, string? path, string name, NodeId dataType, int valueRank, string displayName = "", string symbolicName = "")
        {
            if (string.IsNullOrWhiteSpace(path))
                path = parent?.NodeId.Identifier is string id ? id + "/" + name : name;
            string finalDisplayName = string.IsNullOrEmpty(displayName) ? name : displayName;
            BaseDataVariableState variable = new BaseDataVariableState(parent);
            variable.SymbolicName = string.IsNullOrEmpty(symbolicName) ? name : symbolicName;
            variable.ReferenceTypeId = ReferenceTypes.Organizes;
            variable.TypeDefinitionId = VariableTypeIds.BaseDataVariableType;
            variable.NodeId = new NodeId(path, NamespaceIndex);
            variable.BrowseName = new QualifiedName(name, NamespaceIndex);
            variable.DisplayName = new LocalizedText("cn", finalDisplayName);
            variable.WriteMask = AttributeWriteMask.None;
            variable.UserWriteMask = AttributeWriteMask.None;
            variable.DataType = dataType;
            variable.ValueRank = valueRank;
            variable.AccessLevel = AccessLevels.CurrentReadOrWrite;
            variable.UserAccessLevel = AccessLevels.CurrentReadOrWrite;
            variable.Historizing = false;
            variable.Value = Opc.Ua.TypeInfo.GetDefaultValue(dataType, valueRank, Server.TypeTree);
            variable.StatusCode = StatusCodes.Good;
            variable.Timestamp = DateTime.UtcNow;

            if (valueRank == ValueRanks.OneDimension)
            {
                variable.ArrayDimensions = new ReadOnlyList<uint>(new List<uint> { 0 });
            }
            else if (valueRank == ValueRanks.TwoDimensions)
            {
                variable.ArrayDimensions = new ReadOnlyList<uint>(new List<uint> { 0, 0 });
            }

            if (parent != null)
            {
                parent.AddChild(variable);
            }

            return variable;
        }

        public void UpdateValue(NodeId nodeId, object value)
        {
            var variable = (BaseDataVariableState)FindPredefinedNode(nodeId, typeof(BaseDataVariableState));
            if (variable != null)
            {
                variable.Value = value;
                variable.Timestamp = DateTime.UtcNow;
                variable.ClearChangeMasks(SystemContext, false);
            }
        }

        public void ClearNodeChangeMasks(NodeState node, bool includeChildren = false)
        {
            node.ClearChangeMasks(SystemContext, includeChildren);
        }

        public NodeId AddFolder(NodeId parentId, string? path, string name, string displayName = "")
        {
            var node = Find(parentId);
            if (node is null)
            {
                Console.WriteLine("父级节点不存在");
                return null;
            }
            var newNode = CreateFolder(node, path, name, displayName);
            AddPredefinedNode(SystemContext, newNode);
            return newNode.NodeId;
        }

        public NodeId AddVariable(NodeId parentId, string? path, string name, BuiltInType dataType, int valueRank)
        {
            return AddVariable(parentId, path, name, (uint)dataType, valueRank);
        }

        public NodeId AddVariable(NodeId parentId, string? path, string name, NodeId dataType, int valueRank, string displayName = "", string symbolicName = "")
        {
            var node = Find(parentId);
            if (node is null)
            {
                Console.WriteLine("父级节点不存在");
                return null;
            }
            var newNode = CreateVariable(node, path, name, dataType, valueRank, displayName, symbolicName);
            AddPredefinedNode(SystemContext, newNode);
            return newNode.NodeId;
        }
    }
}

OpcUaServer.cs - 服务器扩展

using Opc.Ua;
using Opc.Ua.Server;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;

namespace OpcUaServerDemo
{
    public partial class OpcUaServer : StandardServer
    {
        private static readonly object s_statusLock = new object();
        /// <summary>
        /// Initializes the server before it starts up.
        /// </summary>
        /// <remarks>
        /// This method is called before any startup processing occurs. The sub-class may update the 
        /// configuration object or do any other application specific startup tasks.
        /// </remarks>
        protected async override void OnServerStarting(ApplicationConfiguration configuration)
        {
            Utils.Trace("The server is starting.");

            base.OnServerStarting(configuration);

            // it is up to the application to decide how to validate user identity tokens.
            // this function creates validator for X509 identity tokens.
            CreateUserIdentityValidators(configuration);
        }

        /// <summary>
        /// Called after the server has been started.
        /// </summary>
        protected override void OnServerStarted(IServerInternal server)
        {
            base.OnServerStarted(server);

            // request notifications when the user identity is changed. all valid users are accepted by default.
            server.SessionManager.ImpersonateUser += new ImpersonateEventHandler(SessionManager_ImpersonateUser);

            try
            {
                // allow a faster sampling interval for CurrentTime node.
                //server.UpdateServerStatus(val => val.Variable.CurrentTime.MinimumSamplingInterval = 250);

                lock (s_statusLock)
                {
                    server.Status.Variable.CurrentTime.MinimumSamplingInterval = 250;
                }

            }
            catch
            { }

        }

        #region User Validation Functions
        private ICertificateValidator m_userCertificateValidator;

        /// <summary>
        /// Creates the objects used to validate the user identity tokens supported by the server.
        /// </summary>
        private void CreateUserIdentityValidators(ApplicationConfiguration configuration)
        {
            for (int ii = 0; ii < configuration.ServerConfiguration.UserTokenPolicies.Count; ii++)
            {
                UserTokenPolicy policy = configuration.ServerConfiguration.UserTokenPolicies[ii];

                // create a validator for a certificate token policy.
                if (policy.TokenType == UserTokenType.Certificate)
                {
                    // check if user certificate trust lists are specified in configuration.
                    if (configuration.SecurityConfiguration.TrustedUserCertificates != null &&
                        configuration.SecurityConfiguration.UserIssuerCertificates != null)
                    {
                        CertificateValidator certificateValidator = new CertificateValidator();
                        certificateValidator.Update(configuration.SecurityConfiguration);
                        certificateValidator.Update(configuration.SecurityConfiguration.UserIssuerCertificates,
                            configuration.SecurityConfiguration.TrustedUserCertificates,
                            configuration.SecurityConfiguration.RejectedCertificateStore);

                        // set custom validator for user certificates.
                        m_userCertificateValidator = certificateValidator.GetChannelValidator();
                    }
                }
            }
        }

        /// <summary>
        /// Called when a client tries to change its user identity.
        /// </summary>
        private void SessionManager_ImpersonateUser(Session session, ImpersonateEventArgs args)
        {
            // check for a user name token.
            UserNameIdentityToken userNameToken = args.NewIdentity as UserNameIdentityToken;

            if (userNameToken != null)
            {
                args.Identity = VerifyPassword(userNameToken);

                // set AuthenticatedUser role for accepted user/password authentication
                args.Identity.GrantedRoleIds.Add(ObjectIds.WellKnownRole_AuthenticatedUser);

                if (args.Identity is SystemConfigurationIdentity)
                {
                    // set ConfigureAdmin role for user with permission to configure server
                    args.Identity.GrantedRoleIds.Add(ObjectIds.WellKnownRole_ConfigureAdmin);
                    args.Identity.GrantedRoleIds.Add(ObjectIds.WellKnownRole_SecurityAdmin);
                }

                return;
            }

            // check for x509 user token.
            X509IdentityToken x509Token = args.NewIdentity as X509IdentityToken;

            if (x509Token != null)
            {
                VerifyUserTokenCertificate(x509Token.Certificate);
                args.Identity = new UserIdentity(x509Token);
                Utils.Trace("X509 Token Accepted: {0}", args.Identity.DisplayName);

                // set AuthenticatedUser role for accepted certificate authentication
                args.Identity.GrantedRoleIds.Add(ObjectIds.WellKnownRole_AuthenticatedUser);

                return;
            }

            // allow anonymous authentication and set Anonymous role for this authentication
            args.Identity = new UserIdentity();
            args.Identity.GrantedRoleIds.Add(ObjectIds.WellKnownRole_Anonymous);
        }

        /// <summary>
        /// Validates the password for a username token.
        /// </summary>
        private IUserIdentity VerifyPassword(UserNameIdentityToken userNameToken)
        {
            var userName = userNameToken.UserName;
            var password = userNameToken.DecryptedPassword;
            if (String.IsNullOrEmpty(userName))
            {
                // an empty username is not accepted.
                throw ServiceResultException.Create(StatusCodes.BadIdentityTokenInvalid,
                    "Security token is not a valid username token. An empty username is not accepted.");
            }

            if (String.IsNullOrEmpty(password))
            {
                // an empty password is not accepted.
                throw ServiceResultException.Create(StatusCodes.BadIdentityTokenRejected,
                    "Security token is not a valid username token. An empty password is not accepted.");
            }// standard users for CTT verification
            if (!((userName == "user1" && password == "user1") ||
                (userName == "user2" && password == "user2")))
            {
                // construct translation object with default text.
                TranslationInfo info = new TranslationInfo(
                    "InvalidPassword",
                    "en-US",
                    "Invalid username or password.",
                    userName);

                // create an exception with a vendor defined sub-code.
                throw new ServiceResultException(new ServiceResult(
                    StatusCodes.BadUserAccessDenied,
                    "InvalidPassword",
                    LoadServerProperties().ProductUri,
                    new LocalizedText(info)));
            }

            return new UserIdentity(userNameToken);
        }

        /// <summary>
        /// Verifies that a certificate user token is trusted.
        /// </summary>
        private void VerifyUserTokenCertificate(X509Certificate2 certificate)
        {
            try
            {
                if (m_userCertificateValidator != null)
                {
                    m_userCertificateValidator.Validate(certificate);
                }
                else
                {
                    CertificateValidator.Validate(certificate);
                }
            }
            catch (Exception e)
            {
                TranslationInfo info;
                StatusCode result = StatusCodes.BadIdentityTokenRejected;
                ServiceResultException se = e as ServiceResultException;
                if (se != null && se.StatusCode == StatusCodes.BadCertificateUseNotAllowed)
                {
                    info = new TranslationInfo(
                        "InvalidCertificate",
                        "en-US",
                        "'{0}' is an invalid user certificate.",
                        certificate.Subject);

                    result = StatusCodes.BadIdentityTokenInvalid;
                }
                else
                {
                    // construct translation object with default text.
                    info = new TranslationInfo(
                        "UntrustedCertificate",
                        "en-US",
                        "'{0}' is not a trusted user certificate.",
                        certificate.Subject);
                }

                // create an exception with a vendor defined sub-code.
                throw new ServiceResultException(new ServiceResult(
                    result,
                    info.Key,
                    LoadServerProperties().ProductUri,
                    new LocalizedText(info)));
            }
        }
        #endregion

    }
}

RunOpcServer.cs - 业务逻辑

using Opc.Ua;
using Opc.Ua.Configuration;
using System.Collections.Concurrent;

namespace OpcUaServerDemo
{
    public class RunOpcServer : IDisposable
    {
        private Task _serverTask;
        private CancellationTokenSource _cancellationTokenSource;
        private System.Timers.Timer _simulationTimer;
        private ConcurrentDictionary<string, BaseDataVariableState> _nodeDic = new ConcurrentDictionary<string, BaseDataVariableState>();
        public RunOpcServer()
        {
            _cancellationTokenSource = new CancellationTokenSource();
            // 启动服务器任务并保存引用
            _serverTask = StartServerAsync(_cancellationTokenSource.Token);
        }

        private async Task StartServerAsync(CancellationToken cancellationToken)
        {
            try
            {
                var rootNodeName = "opcdata";
                // 启动OPC UA服务器
                ApplicationInstance application = new ApplicationInstance();
                application.ConfigSectionName = "OpcUaServer";
                application.LoadApplicationConfiguration(false);

                bool certOk = application.CheckApplicationInstanceCertificate(false, 0).Result;
                if (!certOk)
                {
                    Console.WriteLine("警告:应用程序证书检查未通过,但服务器将继续启动。");
                }

                var server = new OpcUaServer();
                var nodeManagerFactory = new NodeManagerFactory(rootNodeName);
                server.AddNodeManager(nodeManagerFactory);

                // 启动服务器(这会阻塞直到服务器停止)
                application.Start(server).Wait(cancellationToken);

                Console.WriteLine("OPC UA 服务器已启动!");

                // 模拟数据
                var nodeManager = nodeManagerFactory.NodeManager;

                var root = nodeManager.GetRootFolder();
                if (root is null)
                {
                    Console.WriteLine($"OPCUA_Server异常:获取根节点失败");
                    return;
                }
                var deviceList = new List<string> { "Machine1", "Machine2", "Machine3" };

                var machineFolderId = nodeManager.AddFolder(root.NodeId, null, "Machine", "Machine");
                FolderState folder = nodeManager.Find(machineFolderId) as FolderState;

                foreach (var deviceName in deviceList)
                {
                    var deviceId = nodeManager.AddFolder(machineFolderId, null, deviceName, deviceName);
                    var device = nodeManager.Find(deviceId) as FolderState;

                    var sensorList = new List<string> { "Sensor1", "Sensor2", "Sensor3" };
                    foreach (var sensorName in sensorList)
                    {
                        var variableNodeId = nodeManager.AddVariable(deviceId, null, sensorName, (int)BuiltInType.Int16, ValueRanks.Scalar);
                        BaseDataVariableState variable = nodeManager.FindPredefinedNode(variableNodeId, typeof(BaseDataVariableState)) as BaseDataVariableState;

                        var ok = _nodeDic.TryAdd(deviceName + "_" + sensorName, variable);
                        if (!ok)
                        {
                            Console.WriteLine($"添加节点失败:{deviceName}_{sensorName}");
                        }
                    }
                }

                _simulationTimer = new System.Timers.Timer(500);
                var random = new Random();
                _simulationTimer.Elapsed += (sender, e) =>
                {
                    if (cancellationToken.IsCancellationRequested)
                        return;

                    try
                    {
                        foreach (var item in _nodeDic)
                        {
                            var node = item.Value;
                            var value = random.Next(1, 60000);
                            node.Value = value;
                            node.Timestamp = DateTime.Now;
                            nodeManager.ClearNodeChangeMasks(node, false);
                        }
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine($"更新节点数据出错: {ex.Message}");
                    }
                };

                _simulationTimer.Start();
                await Task.Delay(Timeout.Infinite, cancellationToken);
            }
            catch (OperationCanceledException)
            {
                Console.WriteLine("服务器正在停止...");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"服务器发生错误: {ex.Message}");
            }
            finally
            {
                _simulationTimer?.Stop();
                _simulationTimer?.Dispose();
                Console.WriteLine("服务器已停止。");
            }
        }
        public void Stop()
        {
            _cancellationTokenSource?.Cancel();
            _serverTask?.Wait(5000);
        }

        public void Dispose()
        {
            Stop();
            _cancellationTokenSource?.Dispose();
            _simulationTimer?.Dispose();
        }
    }
}

 

posted @ 2025-12-23 14:46  daviyoung  阅读(14)  评论(0)    收藏  举报