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();
}
}
}