OPC基金會提供了OPC UA .NET標準庫以及示例程序,但官方文檔過于簡單,光看官方文檔和示例程序很難弄懂OPC UA .NET標準庫怎么用,花了不少時間摸索才略微弄懂如何使用,以下記錄如何從一個控制臺程序開發一個OPC UA服務器。
安裝Nuget包
安裝OPCFoundation.NetStandard.Opc.Ua

主程序
修改Program.cs
代碼如下:
using Opc.Ua;
using Opc.Ua.Configuration;
using Opc.Ua.Server;
namespace SampleOpcUaServer
{
internal class Program
{
static void Main(string[] args)
{
ApplicationInstance application = new ApplicationInstance();
application.ConfigSectionName = "OpcUaServer";
application.LoadApplicationConfiguration(false).Wait();
application.CheckApplicationInstanceCertificate(false, 0).Wait();
var server = new StandardServer();
var nodeManagerFactory = new NodeManagerFactory();
server.AddNodeManager(nodeManagerFactory);
application.Start(server).Wait();
var nodeManager = nodeManagerFactory.NodeManager;
var simulationTimer = new System.Timers.Timer(1000);
var random = new Random();
simulationTimer.Elapsed += (sender, EventArgs) =>
{
nodeManager?.UpdateValue("ns=2;s=Root_Test", random.NextInt64());
};
simulationTimer.Start();
Console.WriteLine("Endpoints:");
foreach (var endpoint in server.GetEndpoints().DistinctBy(x => x.EndpointUrl))
{
Console.WriteLine(endpoint.EndpointUrl);
}
Console.WriteLine("按Enter添加新變量");
Console.ReadLine();
nodeManager?.AddVariable("ns=2;s=Root", null, "Test2", (int)BuiltInType.Int16, ValueRanks.Scalar);
Console.WriteLine("已添加變量");
Console.ReadLine();
}
}
}
上述代碼中:
ApplicationInstance
是OPC UA標準庫中用于配置并承載OPC UA Server和檢查證書的類。application.ConfigSectionName
指定了配置文件的名稱,配置文件是xml文件,將會在程序文件夾查找名為OpcUaServer.Config.xml
的配置文件。配置文件內容見后文。application.LoadApplicationConfiguration
加載前面指定的配置文件。如果不想使用配置文件,也可通過代碼給application.ApplicationConfiguration
賦值。- 有
StandardServer
和ReverseConnectServer
兩種作為OPC UA服務器的類,ReverseConnectServer
派生于StandardServer
,這兩種類的區別未深入研究,用StandardServer
可滿足基本的需求。 - OPC UA的地址空間由節點組成,簡單理解節點就是提供給OPC UA客戶端訪問的變量和文件夾。通過
server.AddNodeManager
方法添加節點管理工廠類,NodeManagerFactory
類定義見后文。 - 調用
application.Start(server)
方法后,OPC UA Server就會開始運行,并不會阻塞代碼,為了保持在控制臺程序中運行,所以使用Console.ReadLine()
阻塞程序。 nodeManager?.UpdateValue
是自定義的更新OPC UA地址空間中變量值的方法。nodeManager?.AddVariable
在此演示動態添加一個新的變量。
OPC UA配置文件
新建OpcUaServer.Config.xml
文件。

在屬性中設為“始終賦值”。

內容如下:
<?xml version="1.0" encoding="utf-8"?>
<ApplicationConfiguration
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:ua="http://opcfoundation.org/UA/2008/02/Types.xsd"
xmlns="http://opcfoundation.org/UA/SDK/Configuration.xsd"
>
<ApplicationName>Sample OPC UA Server</ApplicationName>
<ApplicationUri>urn:localhost:UA:OpcUaServer</ApplicationUri>
<ProductUri>uri:opcfoundation.org:OpcUaServer</ProductUri>
<ApplicationType>Server_0</ApplicationType>
<SecurityConfiguration>
<ApplicationCertificate>
<StoreType>Directory</StoreType>
<StorePath>%CommonApplicationData%\OPC Foundation\pki\own</StorePath>
<SubjectName>CN=Sample Opc Ua Server, C=US, S=Arizona, O=SomeCompany, DC=localhost</SubjectName>
</ApplicationCertificate>
<TrustedIssuerCertificates>
<StoreType>Directory</StoreType>
<StorePath>%CommonApplicationData%\OPC Foundation\pki\issuer</StorePath>
</TrustedIssuerCertificates>
<TrustedPeerCertificates>
<StoreType>Directory</StoreType>
<StorePath>%CommonApplicationData%\OPC Foundation\pki\trusted</StorePath>
</TrustedPeerCertificates>
<RejectedCertificateStore>
<StoreType>Directory</StoreType>
<StorePath>%CommonApplicationData%\OPC Foundation\pki\rejected</StorePath>
</RejectedCertificateStore>
</SecurityConfiguration>
<TransportConfigurations></TransportConfigurations>
<TransportQuotas>
<OperationTimeout>600000</OperationTimeout>
<MaxStringLength>1048576</MaxStringLength>
<MaxByteStringLength>1048576</MaxByteStringLength>
<MaxArrayLength>65535</MaxArrayLength>
<MaxMessageSize>4194304</MaxMessageSize>
<MaxBufferSize>65535</MaxBufferSize>
<ChannelLifetime>300000</ChannelLifetime>
<SecurityTokenLifetime>3600000</SecurityTokenLifetime>
</TransportQuotas>
<ServerConfiguration>
<BaseAddresses>
<ua:String>https://localhost:62545/OpcUaServer/</ua:String>
<ua:String>opc.tcp://localhost:62546/OpcUaServer</ua:String>
</BaseAddresses>
<SecurityPolicies>
<ServerSecurityPolicy>
<SecurityMode>SignAndEncrypt_3</SecurityMode>
<SecurityPolicyUri>http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256</SecurityPolicyUri>
</ServerSecurityPolicy>
<ServerSecurityPolicy>
<SecurityMode>None_1</SecurityMode>
<SecurityPolicyUri>http://opcfoundation.org/UA/SecurityPolicy#None</SecurityPolicyUri>
</ServerSecurityPolicy>
<ServerSecurityPolicy>
<SecurityMode>Sign_2</SecurityMode>
<SecurityPolicyUri></SecurityPolicyUri>
</ServerSecurityPolicy>
<ServerSecurityPolicy>
<SecurityMode>SignAndEncrypt_3</SecurityMode>
<SecurityPolicyUri></SecurityPolicyUri>
</ServerSecurityPolicy>
</SecurityPolicies>
<UserTokenPolicies>
<ua:UserTokenPolicy>
<ua:TokenType>Anonymous_0</ua:TokenType>
</ua:UserTokenPolicy>
<ua:UserTokenPolicy>
<ua:TokenType>UserName_1</ua:TokenType>
</ua:UserTokenPolicy>
<ua:UserTokenPolicy>
<ua:TokenType>Certificate_2</ua:TokenType>
</ua:UserTokenPolicy>
</UserTokenPolicies>
<DiagnosticsEnabled>false</DiagnosticsEnabled>
<MaxSessionCount>100</MaxSessionCount>
<MinSessionTimeout>10000</MinSessionTimeout>
<MaxSessionTimeout>3600000</MaxSessionTimeout>
<MaxBrowseContinuationPoints>10</MaxBrowseContinuationPoints>
<MaxQueryContinuationPoints>10</MaxQueryContinuationPoints>
<MaxHistoryContinuationPoints>100</MaxHistoryContinuationPoints>
<MaxRequestAge>600000</MaxRequestAge>
<MinPublishingInterval>100</MinPublishingInterval>
<MaxPublishingInterval>3600000</MaxPublishingInterval>
<PublishingResolution>50</PublishingResolution>
<MaxSubscriptionLifetime>3600000</MaxSubscriptionLifetime>
<MaxMessageQueueSize>10</MaxMessageQueueSize>
<MaxNotificationQueueSize>100</MaxNotificationQueueSize>
<MaxNotificationsPerPublish>1000</MaxNotificationsPerPublish>
<MinMetadataSamplingInterval>1000</MinMetadataSamplingInterval>
<AvailableSamplingRates>
<SamplingRateGroup>
<Start>5</Start>
<Increment>5</Increment>
<Count>20</Count>
</SamplingRateGroup>
<SamplingRateGroup>
<Start>100</Start>
<Increment>100</Increment>
<Count>4</Count>
</SamplingRateGroup>
<SamplingRateGroup>
<Start>500</Start>
<Increment>250</Increment>
<Count>2</Count>
</SamplingRateGroup>
<SamplingRateGroup>
<Start>1000</Start>
<Increment>500</Increment>
<Count>20</Count>
</SamplingRateGroup>
</AvailableSamplingRates>
<MaxRegistrationInterval>30000</MaxRegistrationInterval>
<NodeManagerSaveFile>OpcUaServer.nodes.xml</NodeManagerSaveFile>
</ServerConfiguration>
<TraceConfiguration>
<OutputFilePath>Logs\SampleOpcUaServer.log</OutputFilePath>
<DeleteOnLoad>true</DeleteOnLoad>
<TraceMasks>515</TraceMasks>
</TraceConfiguration>
</ApplicationConfiguration>
需要關注的內容有:
ApplicationName:在通過OPC UA工具連接此服務器時,顯示的服務器名稱就是該值。
ApplicationType:應用類型,可用的值有:
- Server_0:服務器
- Client_1:客戶端
- ClientAndServer_2:客戶機和服務器
- DisconveryServer_3:發現服務器。發現服務器用于注冊OPC UA服務器,然后提供OPC UA客戶端搜索到服務器。
SecurityConfiguration:該節點中指定了OPC UA的證書存儲路徑,一般保持默認,不需修改。
ServerConfiguration.BaseAddresses:該節點指定OPC UA服務器的url地址。
ServerConfiguration.SecurityPolicies:該節點配置允許的服務器安全策略,配置通訊是否要簽名和加密。
ServerConfiguration.UserTokenPolicies:該節點配置允許的用戶Token策略,例如是否允許匿名訪問。
AvailableSamplingRates:配置支持的變量采樣率。
TraceConfiguration:配置OPC UA服務器的日志記錄,設定日志記錄路徑,配置的路徑是在系統臨時文件夾下的路徑,日志文件的完整路徑是在%TEMP%\Logs\SampleOpcUaServer.log
。
NodeManagerFactory
新建NodeManagerFactory
類,OPC UA server將調用該類的Create
方法創建INodeManager
實現類,而INodeManager
實現類用于管理OPC UA地址空間。內容如下:
using Opc.Ua;
using Opc.Ua.Server;
namespace SampleOpcUaServer
{
internal class NodeManagerFactory : INodeManagerFactory
{
public NodeManager? NodeManager { get; private set; }
public StringCollection NamespacesUris => new StringCollection() { "http://opcfoundation.org/OpcUaServer" };
public INodeManager Create(IServerInternal server, ApplicationConfiguration configuration)
{
if (NodeManager != null)
return NodeManager;
NodeManager = new NodeManager(server, configuration, NamespacesUris.ToArray());
return NodeManager;
}
}
}
- 實現
INodeManagerFactory
接口,需實現NamespacesUris
屬性和Create
方法。 NodeManager
類是自定義的類,定義見后文。- 為了獲取
Create
方法返回的NodeManager
類,定義了NodeManager
屬性。
NodeManager
新建NodeManager
類:
using Opc.Ua;
using Opc.Ua.Server;
namespace SampleOpcUaServer
{
internal class NodeManager : CustomNodeManager2
{
public NodeManager(IServerInternal server, params string[] namespaceUris)
: base(server, namespaceUris)
{
}
public NodeManager(IServerInternal server, ApplicationConfiguration configuration, params string[] namespaceUris)
: base(server, configuration, namespaceUris)
{
}
protected override NodeStateCollection LoadPredefinedNodes(ISystemContext context)
{
FolderState root = CreateFolder(null, null, "Root");
root.AddReference(ReferenceTypes.Organizes, true, ObjectIds.ObjectsFolder);
root.EventNotifier = EventNotifiers.SubscribeToEvents;
AddRootNotifier(root);
CreateVariable(root, null, "Test", BuiltInType.Int64, ValueRanks.Scalar);
return new NodeStateCollection(new List<NodeState> { root });
}
protected virtual FolderState CreateFolder(NodeState? parent, string? path, string name)
{
if (string.IsNullOrWhiteSpace(path))
path = parent?.NodeId.Identifier is string id ? id + "_" + name : name;
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(path, NamespaceIndex);
folder.DisplayName = new LocalizedText("en", name);
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)
{
if (string.IsNullOrWhiteSpace(path))
path = parent?.NodeId.Identifier is string id ? id + "_" + name : name;
BaseDataVariableState variable = new BaseDataVariableState(parent);
variable.SymbolicName = name;
variable.ReferenceTypeId = ReferenceTypes.Organizes;
variable.TypeDefinitionId = VariableTypeIds.BaseDataVariableType;
variable.NodeId = new NodeId(path, NamespaceIndex);
variable.BrowseName = new QualifiedName(path, NamespaceIndex);
variable.DisplayName = new LocalizedText("en", name);
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 NodeId AddFolder(NodeId parentId, string? path, string name)
{
var node = Find(parentId);
var newNode = CreateFolder(node, path, name);
AddPredefinedNode(SystemContext, node);
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)
{
var node = Find(parentId);
var newNode = CreateVariable(node, path, name, dataType, valueRank);
AddPredefinedNode(SystemContext, node);
return newNode.NodeId;
}
}
}
上述代碼中:
- 需繼承
CustomNodeManager2
,這是OPC UA標準庫中提供的類。 - 重寫
LoadPredefinedNodes
方法,在該方法中配置預定義節點。其中創建了一個Root文件夾,Root文件夾中添加了Test變量。 root.AddReference(ReferenceTypes.Organizes, true, ObjectIds.ObjectsFolder)
該語句將節點添加到OPC UA服務器根節點,如果不使用該語句,可在Server
節點下看到添加的節點。CreateFolder
是自定義的方法,用于簡化創建文件夾節點。CreateVariable
是自定義的方法,用于簡化創建變量節點。UpdateValue
是用于更新變量節點值的方法。其中修改值后,需調用ClearChangeMasks
方法,才能通知客戶端更新值。AddFolder
用于啟動服務器后添加新的文件夾。AddVariable
用于啟動服務器后添加新的變量。
測試服務器
比較好用的測試工具有:
以下用OpcExpert測試。
瀏覽本地計算機可發現OPC UA服務器,可看到添加的Root節點和Test變量,Test變量的值會每秒更新。

源碼地址:https://github.com/Yada-Yang/SampleOpcUaServer
轉自https://www.cnblogs.com/yada/p/18257593
該文章在 2025/5/17 10:09:56 編輯過