From cfaa34fa81f0ebfdab4df565c0a350b47036fd99 Mon Sep 17 00:00:00 2001 From: Marc Date: Tue, 11 Aug 2026 17:29:24 +0200 Subject: [PATCH] Move the data channel engine into Opc.Ua.Core.Channels The engine now consumes the message-extension seam rather than living inside the assembly that provides it, so Opc.Ua.Core no longer carries a streaming feature it does not need. Opc.Ua.Core.Channels references Opc.Ua.Core and nothing else, ships as OPCFoundation.NetStandard.Opc.Ua.Core.Channels, and targets the same six frameworks as Core so the net48 coverage survives. What moved: the twenty-one engine files, the data channel half of the UASC partial - now extension methods, because a partial class cannot span assemblies - and IServerDataChannelTransport with InlineServerDataChannelTransport, which turned out to touch only SecureChannelContext, Profiles and the engine. What deliberately did not. StandardServer.DataChannels.cs is a partial of StandardServer, so it cannot move at all; its Service overrides and the authorizer and auditor that read ServerInternal, MasterNodeManager and node states are server concerns and stay. Opc.Ua.Bindings.Quic stays its own net9.0+ package: System.Net.Quic is behind RequiresPreviewFeatures on net8.0, and it references Opc.Ua.Server, so folding it into a Core.* package would put that package above the Server layer. Two things moved the other way, because they are the SecureChannel's own concerns rather than the feature's. DataChannelSequenceBudget becomes SequenceNumberBudget in Core: the space is shared by MSG, OPN, CLO and every extension alike, and the client channel already used it to decide when to renew a token. Keeping one instance on the channel also removes the second, redundant budget the seam had briefly introduced. The SecureChannel registry becomes UaSCSecureChannelRegistry, since mapping an identifier to the channel that owns it says nothing about data channels. Core loses EnableDataChannels and DataChannels on both channel types, MaxDataChannelBodySize, SequenceBudget's data channel name, and the CoreEventIds.DataChannel blocks; TransportChannelFeatures.DataChannels becomes MessageExtensions. The feature is [Experimental], so removing rather than forwarding is allowed. Log EventIds keep their existing numbers in ChannelsEventIds so an operator's filters keep working across the move. DataChannelFeature carries the experimental marker in Core, because it marks the generated Service Set that Clients call, not the engine. expected-packages.txt gains Core.Channels and Bindings.Quic - the latter was packable but missing, which the file itself says should be a conscious act. Opc.Ua.Core.DataChannels.Tests 284 pass on net10.0 and 212 on net48; Opc.Ua.Server.Tests 4072 and Opc.Ua.Sessions.Tests 777 pass, which is what covers the shared receive and send path the seam sits on. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5d026b47-265d-4c97-8715-26e207dfa03c --- .azurepipelines/expected-packages.txt | 2 + docs/DataChannels.md | 24 ++- docs/README.md | 2 +- .../ConsoleDataChannelStreaming.csproj | 1 + .../StreamingHarness.cs | 4 +- .../Opc.Ua.Bindings.Quic.csproj | 1 + .../QuicMultiplexedTransport.cs | 2 +- .../Session/ManagedSession.Services.cs | 12 +- src/Opc.Ua.Core.Channels/ChannelsEventIds.cs | 46 +++++ .../DataChannels/DataChannel.cs | 0 .../DataChannels/DataChannelConstants.cs | 5 +- .../DataChannels/DataChannelExtension.cs | 20 +- .../DataChannels/DataChannelLog.cs | 12 +- .../DataChannels/DataChannelManager.cs | 0 .../DataChannels/DataChannelMessage.cs | 0 .../DataChannels/DataChannelSettings.cs | 0 .../FlowControl/DataChannelCredit.cs | 0 .../DataChannels/Framing/DataChannelFrame.cs | 0 .../Framing/DataChannelFrameCodec.cs | 0 .../Framing/DataChannelFrameError.cs | 0 .../Framing/DataChannelFrameType.cs | 0 .../DataChannels/IDataChannelTransport.cs | 0 .../DataChannels/Model/DataChannelModel.cs | 0 .../Scheduling/DataChannelSendQueue.cs | 0 .../Sequencing/DataChannelReceiveWindow.cs | 0 .../Sequencing/DataChannelSequence.cs | 0 .../Services/DataChannelNegotiator.cs | 0 .../Services/DataChannelServiceHandler.cs | 0 .../Services/DataChannelSourceRegistry.cs | 0 .../DataChannels/Services/QuicPeerBinding.cs | 0 .../IServerDataChannelTransport.cs | 109 +++++++++++ .../InlineServerDataChannelTransport.cs | 12 +- .../Properties/AssemblyInfo.cs | 32 ++++ .../UaSCDataChannelExtensions.cs | 161 ++++++++++++++++ src/Opc.Ua.Core/EventIds.cs | 2 - src/Opc.Ua.Core/Stack/DataChannelFeature.cs | 58 ++++++ .../Tcp/ISecureChannelMessageExtension.cs | 14 +- .../SequenceNumberBudget.cs} | 22 ++- .../Stack/Tcp/TcpListenerChannel.cs | 4 +- .../Tcp/UaSCBinaryChannel.DataChannels.cs | 180 ------------------ .../UaSCBinaryChannel.MessageExtensions.cs | 27 ++- .../Stack/Tcp/UaSCBinaryChannel.Symmetric.cs | 5 +- .../Stack/Tcp/UaSCBinaryClientChannel.cs | 2 +- .../Stack/Tcp/UaSCBinaryTransportChannel.cs | 29 +-- .../Stack/Tcp/UaSCSecureChannelRegistry.cs | 95 +++++++++ .../Stack/Transport/ITransportChannel.cs | 6 +- .../RedundantClientSession.cs | 12 +- src/Opc.Ua.Server/Opc.Ua.Server.csproj | 1 + .../Server/StandardServer.DataChannels.cs | 29 --- .../DataChannelConformanceTests.cs | 6 +- .../DataChannelSequenceBudgetTests.cs | 6 +- .../DataChannelSequencingTests.cs | 10 +- .../InlineServerDataChannelTransportTests.cs | 10 +- .../Opc.Ua.Core.DataChannels.Tests.csproj | 1 + .../UaSCDataChannelDispatchTests.cs | 6 +- 55 files changed, 650 insertions(+), 320 deletions(-) create mode 100644 src/Opc.Ua.Core.Channels/ChannelsEventIds.cs rename src/{Opc.Ua.Core/Stack => Opc.Ua.Core.Channels}/DataChannels/DataChannel.cs (100%) rename src/{Opc.Ua.Core/Stack => Opc.Ua.Core.Channels}/DataChannels/DataChannelConstants.cs (97%) rename src/{Opc.Ua.Core/Stack => Opc.Ua.Core.Channels}/DataChannels/DataChannelExtension.cs (94%) rename src/{Opc.Ua.Core/Stack => Opc.Ua.Core.Channels}/DataChannels/DataChannelLog.cs (84%) rename src/{Opc.Ua.Core/Stack => Opc.Ua.Core.Channels}/DataChannels/DataChannelManager.cs (100%) rename src/{Opc.Ua.Core/Stack => Opc.Ua.Core.Channels}/DataChannels/DataChannelMessage.cs (100%) rename src/{Opc.Ua.Core/Stack => Opc.Ua.Core.Channels}/DataChannels/DataChannelSettings.cs (100%) rename src/{Opc.Ua.Core/Stack => Opc.Ua.Core.Channels}/DataChannels/FlowControl/DataChannelCredit.cs (100%) rename src/{Opc.Ua.Core/Stack => Opc.Ua.Core.Channels}/DataChannels/Framing/DataChannelFrame.cs (100%) rename src/{Opc.Ua.Core/Stack => Opc.Ua.Core.Channels}/DataChannels/Framing/DataChannelFrameCodec.cs (100%) rename src/{Opc.Ua.Core/Stack => Opc.Ua.Core.Channels}/DataChannels/Framing/DataChannelFrameError.cs (100%) rename src/{Opc.Ua.Core/Stack => Opc.Ua.Core.Channels}/DataChannels/Framing/DataChannelFrameType.cs (100%) rename src/{Opc.Ua.Core/Stack => Opc.Ua.Core.Channels}/DataChannels/IDataChannelTransport.cs (100%) rename src/{Opc.Ua.Core/Stack => Opc.Ua.Core.Channels}/DataChannels/Model/DataChannelModel.cs (100%) rename src/{Opc.Ua.Core/Stack => Opc.Ua.Core.Channels}/DataChannels/Scheduling/DataChannelSendQueue.cs (100%) rename src/{Opc.Ua.Core/Stack => Opc.Ua.Core.Channels}/DataChannels/Sequencing/DataChannelReceiveWindow.cs (100%) rename src/{Opc.Ua.Core/Stack => Opc.Ua.Core.Channels}/DataChannels/Sequencing/DataChannelSequence.cs (100%) rename src/{Opc.Ua.Core/Stack => Opc.Ua.Core.Channels}/DataChannels/Services/DataChannelNegotiator.cs (100%) rename src/{Opc.Ua.Core/Stack => Opc.Ua.Core.Channels}/DataChannels/Services/DataChannelServiceHandler.cs (100%) rename src/{Opc.Ua.Core/Stack => Opc.Ua.Core.Channels}/DataChannels/Services/DataChannelSourceRegistry.cs (100%) rename src/{Opc.Ua.Core/Stack => Opc.Ua.Core.Channels}/DataChannels/Services/QuicPeerBinding.cs (100%) create mode 100644 src/Opc.Ua.Core.Channels/IServerDataChannelTransport.cs rename src/{Opc.Ua.Server/Server => Opc.Ua.Core.Channels}/InlineServerDataChannelTransport.cs (95%) create mode 100644 src/Opc.Ua.Core.Channels/Properties/AssemblyInfo.cs create mode 100644 src/Opc.Ua.Core.Channels/UaSCDataChannelExtensions.cs create mode 100644 src/Opc.Ua.Core/Stack/DataChannelFeature.cs rename src/Opc.Ua.Core/Stack/{DataChannels/Sequencing/DataChannelSequenceBudget.cs => Tcp/SequenceNumberBudget.cs} (91%) delete mode 100644 src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.DataChannels.cs create mode 100644 src/Opc.Ua.Core/Stack/Tcp/UaSCSecureChannelRegistry.cs diff --git a/.azurepipelines/expected-packages.txt b/.azurepipelines/expected-packages.txt index 78da758690..ef913cc1b3 100644 --- a/.azurepipelines/expected-packages.txt +++ b/.azurepipelines/expected-packages.txt @@ -15,10 +15,12 @@ # package ids with this file in the same pull request that changes them. OPCFoundation.NetStandard.Opc.Ua.Bindings.Https +OPCFoundation.NetStandard.Opc.Ua.Bindings.Quic OPCFoundation.NetStandard.Opc.Ua.Client OPCFoundation.NetStandard.Opc.Ua.Client.ComplexTypes OPCFoundation.NetStandard.Opc.Ua.Configuration OPCFoundation.NetStandard.Opc.Ua.Core +OPCFoundation.NetStandard.Opc.Ua.Core.Channels OPCFoundation.NetStandard.Opc.Ua.Core.Diagnostics OPCFoundation.NetStandard.Opc.Ua.Core.Schema OPCFoundation.NetStandard.Opc.Ua.Core.Types diff --git a/docs/DataChannels.md b/docs/DataChannels.md index 95addfd2fb..d09baba4f8 100644 --- a/docs/DataChannels.md +++ b/docs/DataChannels.md @@ -36,14 +36,32 @@ it, and a peer that does use it never speaks first: it may not transmit a frame because an unrecognized `MessageType` is a protocol error that closes the SecureChannel, taking every Session, Subscription and Service call with it. -Consumers opt in by suppressing the experimental diagnostic: +Consumers opt in by referencing the package and suppressing the experimental diagnostic: ```xml + + + $(NoWarn);DataChannels ``` +## Where the code lives + +The engine ships in `OPCFoundation.NetStandard.Opc.Ua.Core.Channels` rather than in +`Opc.Ua.Core`. It is a consumer of the message-extension seam in Core: it registers as the +owner of the `STR` MessageType, and the SecureChannel decrypts, verifies and +sequence-checks every chunk before the engine sees it. Core itself carries no data channel +vocabulary โ€” `ISecureChannelMessageExtension` and `ISecureChannelMessageHost` name only "a +MessageType that is neither a Service call nor part of establishing the SecureChannel". + +Two things stay in Core because they are the SecureChannel's own concerns rather than the +feature's: `SequenceNumberBudget`, which tracks the sequence space every MessageType draws +on, and `UaSCSecureChannelRegistry`, which maps a SecureChannel identifier to the channel +that owns it. The `opc.quic` transport remains its own package, +`OPCFoundation.NetStandard.Opc.Ua.Bindings.Quic`. + ## Server side ```csharp @@ -253,8 +271,8 @@ second. The sample measured 0.5 Mbit/s before the fix and 1.3 Gbit/s after it, a The suite is 284 tests on `net10.0` and 212 tests on `net48` over `tests/Opc.Ua.Core.DataChannels.Tests`, covering -`Stack/DataChannels/**`, `Stack/Tcp/UaSCBinaryChannel.DataChannels.cs`, -`Server/InlineServerDataChannelTransport.cs` and `Opc.Ua.Bindings.Quic`. Coverage was +`Opc.Ua.Core.Channels`, `Stack/Tcp/UaSCBinaryChannel.MessageExtensions.cs` +and `Opc.Ua.Bindings.Quic`. Coverage was **80.1%** at 255 tests, having fallen from 87.6% while the test count rose, because wiring the previously-uncalled obligations added a good deal of production code โ€” the server-side Service dispatch, the stream mapping, the diff --git a/docs/README.md b/docs/README.md index 16a4cb90d1..2ba0ebedf3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -27,7 +27,7 @@ Here is a list of available documentation for different topics: * Pluggable [Identity Providers](IdentityProviders.md) โ€” interfaces (`IClientIdentityProvider`, `IUserTokenAuthenticator`, `IAccessTokenProvider`, `ITokenIssuer`, `IIdentityClaims`) plus the OPC 10000-6 ยง6.5.2.2 `IssuerEndpointUrl` JSON parser for OAuth2 / OIDC / Entra / JWT flows. * Support for [ECC Certificates](EccProfiles.md). * Working with [ComplexTypes](ComplexTypes.md) - Custom structures and enumerations. -* [Data Channels](DataChannels.md) - **Experimental.** Inline streaming of opaque bytes over a SecureChannel: the `STR` frame, per-channel flow control and scheduling, and the `OpenDataChannel` / `ModifyDataChannel` / `CloseDataChannel` Service Set. Identifiers are provisional and the errata is not endorsed by the OPC Foundation. +* [Data Channels](DataChannels.md) - **Experimental.** Inline streaming of opaque bytes over a SecureChannel: the `STR` frame, per-channel flow control and scheduling, and the `OpenDataChannel` / `ModifyDataChannel` / `CloseDataChannel` Service Set. Ships as `OPCFoundation.NetStandard.Opc.Ua.Core.Channels`, attached to the SecureChannel through the message-extension seam in `Opc.Ua.Core`. Identifiers are provisional and the errata is not endorsed by the OPC Foundation. * Client-based [NodeSet Export](NodeSetExport.md) - Export server address space to NodeSet2 XML. * Source generated [DataTypes] - How to annotate POCO classes and let the source generator generate the `IEncodeable` implementation. * Runtime [Schema Generation](SchemaGeneration.md) - Produce XSD, OPC Binary (BSD) and JSON Schema (Part 6 Annex C, compact + verbose) for generated encodeable types and dynamically added complex types via the injectable `ISchemaProvider`; schemas are built as object models in code (trimmable, NativeAOT compatible). diff --git a/samples/ConsoleDataChannelStreaming/ConsoleDataChannelStreaming.csproj b/samples/ConsoleDataChannelStreaming/ConsoleDataChannelStreaming.csproj index 435078bee1..2f4c4ff181 100644 --- a/samples/ConsoleDataChannelStreaming/ConsoleDataChannelStreaming.csproj +++ b/samples/ConsoleDataChannelStreaming/ConsoleDataChannelStreaming.csproj @@ -20,6 +20,7 @@ + diff --git a/samples/ConsoleDataChannelStreaming/StreamingHarness.cs b/samples/ConsoleDataChannelStreaming/StreamingHarness.cs index aadbc8a43f..4dea07f9fc 100644 --- a/samples/ConsoleDataChannelStreaming/StreamingHarness.cs +++ b/samples/ConsoleDataChannelStreaming/StreamingHarness.cs @@ -365,7 +365,7 @@ namespace ConsoleDataChannelStreaming } else { - clientManager = clientChannel.EnableDataChannels( + clientManager = clientChannel.SecureChannel!.EnableDataChannels( isServer: false, telemetry, maxDataChannels: 16, @@ -789,7 +789,7 @@ namespace ConsoleDataChannelStreaming out uint maxFrameSize, out bool isReliable) { - if (!UaSCDataChannelSecureChannelRegistry.TryGet( + if (!UaSCSecureChannelRegistry.TryGet( secureChannelContext.SecureChannelId, out UaSCUaBinaryChannel? channel) || channel == null) diff --git a/src/Opc.Ua.Bindings.Quic/Opc.Ua.Bindings.Quic.csproj b/src/Opc.Ua.Bindings.Quic/Opc.Ua.Bindings.Quic.csproj index 0c21ca8919..f48ce9fd6e 100644 --- a/src/Opc.Ua.Bindings.Quic/Opc.Ua.Bindings.Quic.csproj +++ b/src/Opc.Ua.Bindings.Quic/Opc.Ua.Bindings.Quic.csproj @@ -23,6 +23,7 @@ + diff --git a/src/Opc.Ua.Bindings.Quic/QuicMultiplexedTransport.cs b/src/Opc.Ua.Bindings.Quic/QuicMultiplexedTransport.cs index fcd47a3e29..8ee1bd9325 100644 --- a/src/Opc.Ua.Bindings.Quic/QuicMultiplexedTransport.cs +++ b/src/Opc.Ua.Bindings.Quic/QuicMultiplexedTransport.cs @@ -129,7 +129,7 @@ namespace Opc.Ua.Bindings public TransportChannelFeatures Features => TransportChannelFeatures.Reconnect | TransportChannelFeatures.ReverseConnect | - TransportChannelFeatures.DataChannels | + TransportChannelFeatures.MessageExtensions | TransportChannelFeatures.MultiplexedStreams; /// diff --git a/src/Opc.Ua.Client/Session/ManagedSession.Services.cs b/src/Opc.Ua.Client/Session/ManagedSession.Services.cs index 2c4dbd69e6..8de69e38fe 100644 --- a/src/Opc.Ua.Client/Session/ManagedSession.Services.cs +++ b/src/Opc.Ua.Client/Session/ManagedSession.Services.cs @@ -610,8 +610,8 @@ namespace Opc.Ua.Client /// #if NET8_0_OR_GREATER - [Experimental(DataChannelConstants.ExperimentalDiagnosticId, - UrlFormat = DataChannelConstants.ExperimentalUrlFormat)] + [Experimental(DataChannelFeature.ExperimentalDiagnosticId, + UrlFormat = DataChannelFeature.ExperimentalUrlFormat)] #endif public async ValueTask OpenDataChannelAsync( RequestHeader? requestHeader, @@ -635,8 +635,8 @@ namespace Opc.Ua.Client /// #if NET8_0_OR_GREATER - [Experimental(DataChannelConstants.ExperimentalDiagnosticId, - UrlFormat = DataChannelConstants.ExperimentalUrlFormat)] + [Experimental(DataChannelFeature.ExperimentalDiagnosticId, + UrlFormat = DataChannelFeature.ExperimentalUrlFormat)] #endif public async ValueTask ModifyDataChannelAsync( RequestHeader? requestHeader, @@ -656,8 +656,8 @@ namespace Opc.Ua.Client /// #if NET8_0_OR_GREATER - [Experimental(DataChannelConstants.ExperimentalDiagnosticId, - UrlFormat = DataChannelConstants.ExperimentalUrlFormat)] + [Experimental(DataChannelFeature.ExperimentalDiagnosticId, + UrlFormat = DataChannelFeature.ExperimentalUrlFormat)] #endif public async ValueTask CloseDataChannelAsync( RequestHeader? requestHeader, diff --git a/src/Opc.Ua.Core.Channels/ChannelsEventIds.cs b/src/Opc.Ua.Core.Channels/ChannelsEventIds.cs new file mode 100644 index 0000000000..2013f8112d --- /dev/null +++ b/src/Opc.Ua.Core.Channels/ChannelsEventIds.cs @@ -0,0 +1,46 @@ +/* ======================================================================== + * Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ + +namespace Opc.Ua.Bindings +{ + /// + /// The base EventIds each source-generated logging class in this assembly + /// counts from. + /// + /// + /// The values continue the block these messages used while the engine + /// lived in Opc.Ua.Core, so an operator's existing log filters keep + /// working across the move. + /// + internal static class ChannelsEventIds + { + public const int DataChannel = 600; + public const int DataChannelManager = 620; + } +} diff --git a/src/Opc.Ua.Core/Stack/DataChannels/DataChannel.cs b/src/Opc.Ua.Core.Channels/DataChannels/DataChannel.cs similarity index 100% rename from src/Opc.Ua.Core/Stack/DataChannels/DataChannel.cs rename to src/Opc.Ua.Core.Channels/DataChannels/DataChannel.cs diff --git a/src/Opc.Ua.Core/Stack/DataChannels/DataChannelConstants.cs b/src/Opc.Ua.Core.Channels/DataChannels/DataChannelConstants.cs similarity index 97% rename from src/Opc.Ua.Core/Stack/DataChannels/DataChannelConstants.cs rename to src/Opc.Ua.Core.Channels/DataChannels/DataChannelConstants.cs index 3e18de533f..7fb71cedc9 100644 --- a/src/Opc.Ua.Core/Stack/DataChannels/DataChannelConstants.cs +++ b/src/Opc.Ua.Core.Channels/DataChannels/DataChannelConstants.cs @@ -45,14 +45,13 @@ namespace Opc.Ua.Bindings /// The diagnostic id suppressed by a consumer that opts in to the /// experimental data channel surface. /// - public const string ExperimentalDiagnosticId = "DataChannels"; + public const string ExperimentalDiagnosticId = DataChannelFeature.ExperimentalDiagnosticId; /// /// The documentation url reported alongside /// . /// - public const string ExperimentalUrlFormat = - "https://github.com/OPCFoundation/UA-.NETStandard/blob/master/docs/DataChannels.md"; + public const string ExperimentalUrlFormat = DataChannelFeature.ExperimentalUrlFormat; /// /// The connection control channel (Part 6 errata 5.6). Never diff --git a/src/Opc.Ua.Core/Stack/DataChannels/DataChannelExtension.cs b/src/Opc.Ua.Core.Channels/DataChannels/DataChannelExtension.cs similarity index 94% rename from src/Opc.Ua.Core/Stack/DataChannels/DataChannelExtension.cs rename to src/Opc.Ua.Core.Channels/DataChannels/DataChannelExtension.cs index 19d6e52eeb..9fae0a7301 100644 --- a/src/Opc.Ua.Core/Stack/DataChannels/DataChannelExtension.cs +++ b/src/Opc.Ua.Core.Channels/DataChannels/DataChannelExtension.cs @@ -65,7 +65,6 @@ namespace Opc.Ua.Bindings { m_host = host ?? throw new ArgumentNullException(nameof(host)); m_isSource = isServer; - m_budget = new DataChannelSequenceBudget(host.TimeProvider); // Cached so a frame does not allocate a closure on the send path. m_onSecuring = ReserveSequenceNumber; @@ -88,14 +87,7 @@ namespace Opc.Ua.Bindings /// under the current token. STR, MSG, OPN and CLO chunks all draw on /// the same space. /// - public DataChannelSequenceBudget SequenceBudget - { - get - { - m_budget.ObserveConsumed(m_host.SequenceNumbersIssued); - return m_budget; - } - } + public SequenceNumberBudget SequenceBudget => m_host.SequenceBudget; /// /// Raised when a frame violated the framing rules badly enough to cost @@ -145,7 +137,7 @@ namespace Opc.Ua.Bindings ArraySegment body = message.Body; if (!DataChannelFrameCodec.TryDecode( - new ReadOnlyMemory(body.GetArray(), body.Offset, body.Count), + new ReadOnlyMemory(body.Array!, body.Offset, body.Count), m_host.ReceiveBufferSize, out DataChannelFrame frame, out DataChannelFrameError error)) @@ -179,7 +171,8 @@ namespace Opc.Ua.Bindings /// public void OnSecurityTokenActivated() { - m_budget.OnTokenActivated(); + // The channel rebases the budget itself, because every MessageType + // it carries draws on the same space. } /// @@ -243,9 +236,7 @@ namespace Opc.Ua.Bindings /// private void ReserveSequenceNumber() { - m_budget.ObserveConsumed(m_host.SequenceNumbersIssued); - - if (!m_budget.TryConsume()) + if (!m_host.SequenceBudget.TryConsume()) { throw ServiceResultException.Create( StatusCodes.BadSecureChannelTokenUnknown, @@ -254,7 +245,6 @@ namespace Opc.Ua.Bindings } private readonly ISecureChannelMessageHost m_host; - private readonly DataChannelSequenceBudget m_budget; private readonly Action m_onSecuring; private readonly bool m_isSource; } diff --git a/src/Opc.Ua.Core/Stack/DataChannels/DataChannelLog.cs b/src/Opc.Ua.Core.Channels/DataChannels/DataChannelLog.cs similarity index 84% rename from src/Opc.Ua.Core/Stack/DataChannels/DataChannelLog.cs rename to src/Opc.Ua.Core.Channels/DataChannels/DataChannelLog.cs index a456d2b0de..9424cb24e0 100644 --- a/src/Opc.Ua.Core/Stack/DataChannels/DataChannelLog.cs +++ b/src/Opc.Ua.Core.Channels/DataChannels/DataChannelLog.cs @@ -37,23 +37,23 @@ namespace Opc.Ua.Bindings /// internal static partial class DataChannelLog { - [LoggerMessage(EventId = CoreEventIds.DataChannelManager + 0, Level = LogLevel.Warning, + [LoggerMessage(EventId = ChannelsEventIds.DataChannelManager + 0, Level = LogLevel.Warning, Message = "DataChannel: A frame named ChannelId {ChannelId}, which is not open on this SecureChannel.")] public static partial void DataChannelManagerUnknownChannel( this ILogger logger, uint channelId); - [LoggerMessage(EventId = CoreEventIds.DataChannelManager + 1, Level = LogLevel.Error, + [LoggerMessage(EventId = ChannelsEventIds.DataChannelManager + 1, Level = LogLevel.Error, Message = "DataChannel: A connection level credit grant would overflow the window.")] public static partial void DataChannelManagerCreditOverflow(this ILogger logger); - [LoggerMessage(EventId = CoreEventIds.DataChannelManager + 2, Level = LogLevel.Error, + [LoggerMessage(EventId = ChannelsEventIds.DataChannelManager + 2, Level = LogLevel.Error, Message = "DataChannel: The scheduler round faulted.")] public static partial void DataChannelManagerSchedulerFault( this ILogger logger, Exception exception); - [LoggerMessage(EventId = CoreEventIds.DataChannel + 0, Level = LogLevel.Debug, + [LoggerMessage(EventId = ChannelsEventIds.DataChannel + 0, Level = LogLevel.Debug, Message = "DataChannel {ChannelId}: entered {State} with {Status}.")] public static partial void DataChannelStateChanged( this ILogger logger, @@ -61,14 +61,14 @@ namespace Opc.Ua.Bindings DataChannelState state, StatusCode status); - [LoggerMessage(EventId = CoreEventIds.DataChannel + 1, Level = LogLevel.Warning, + [LoggerMessage(EventId = ChannelsEventIds.DataChannel + 1, Level = LogLevel.Warning, Message = "DataChannel {ChannelId}: rejected a frame, resetting with {Status}.")] public static partial void DataChannelFrameRejected( this ILogger logger, uint channelId, StatusCode status); - [LoggerMessage(EventId = CoreEventIds.DataChannel + 2, Level = LogLevel.Warning, + [LoggerMessage(EventId = ChannelsEventIds.DataChannel + 2, Level = LogLevel.Warning, Message = "DataChannel: a STR frame arrived but the data channel feature is not enabled.")] public static partial void DataChannelFeatureDisabled(this ILogger logger); } diff --git a/src/Opc.Ua.Core/Stack/DataChannels/DataChannelManager.cs b/src/Opc.Ua.Core.Channels/DataChannels/DataChannelManager.cs similarity index 100% rename from src/Opc.Ua.Core/Stack/DataChannels/DataChannelManager.cs rename to src/Opc.Ua.Core.Channels/DataChannels/DataChannelManager.cs diff --git a/src/Opc.Ua.Core/Stack/DataChannels/DataChannelMessage.cs b/src/Opc.Ua.Core.Channels/DataChannels/DataChannelMessage.cs similarity index 100% rename from src/Opc.Ua.Core/Stack/DataChannels/DataChannelMessage.cs rename to src/Opc.Ua.Core.Channels/DataChannels/DataChannelMessage.cs diff --git a/src/Opc.Ua.Core/Stack/DataChannels/DataChannelSettings.cs b/src/Opc.Ua.Core.Channels/DataChannels/DataChannelSettings.cs similarity index 100% rename from src/Opc.Ua.Core/Stack/DataChannels/DataChannelSettings.cs rename to src/Opc.Ua.Core.Channels/DataChannels/DataChannelSettings.cs diff --git a/src/Opc.Ua.Core/Stack/DataChannels/FlowControl/DataChannelCredit.cs b/src/Opc.Ua.Core.Channels/DataChannels/FlowControl/DataChannelCredit.cs similarity index 100% rename from src/Opc.Ua.Core/Stack/DataChannels/FlowControl/DataChannelCredit.cs rename to src/Opc.Ua.Core.Channels/DataChannels/FlowControl/DataChannelCredit.cs diff --git a/src/Opc.Ua.Core/Stack/DataChannels/Framing/DataChannelFrame.cs b/src/Opc.Ua.Core.Channels/DataChannels/Framing/DataChannelFrame.cs similarity index 100% rename from src/Opc.Ua.Core/Stack/DataChannels/Framing/DataChannelFrame.cs rename to src/Opc.Ua.Core.Channels/DataChannels/Framing/DataChannelFrame.cs diff --git a/src/Opc.Ua.Core/Stack/DataChannels/Framing/DataChannelFrameCodec.cs b/src/Opc.Ua.Core.Channels/DataChannels/Framing/DataChannelFrameCodec.cs similarity index 100% rename from src/Opc.Ua.Core/Stack/DataChannels/Framing/DataChannelFrameCodec.cs rename to src/Opc.Ua.Core.Channels/DataChannels/Framing/DataChannelFrameCodec.cs diff --git a/src/Opc.Ua.Core/Stack/DataChannels/Framing/DataChannelFrameError.cs b/src/Opc.Ua.Core.Channels/DataChannels/Framing/DataChannelFrameError.cs similarity index 100% rename from src/Opc.Ua.Core/Stack/DataChannels/Framing/DataChannelFrameError.cs rename to src/Opc.Ua.Core.Channels/DataChannels/Framing/DataChannelFrameError.cs diff --git a/src/Opc.Ua.Core/Stack/DataChannels/Framing/DataChannelFrameType.cs b/src/Opc.Ua.Core.Channels/DataChannels/Framing/DataChannelFrameType.cs similarity index 100% rename from src/Opc.Ua.Core/Stack/DataChannels/Framing/DataChannelFrameType.cs rename to src/Opc.Ua.Core.Channels/DataChannels/Framing/DataChannelFrameType.cs diff --git a/src/Opc.Ua.Core/Stack/DataChannels/IDataChannelTransport.cs b/src/Opc.Ua.Core.Channels/DataChannels/IDataChannelTransport.cs similarity index 100% rename from src/Opc.Ua.Core/Stack/DataChannels/IDataChannelTransport.cs rename to src/Opc.Ua.Core.Channels/DataChannels/IDataChannelTransport.cs diff --git a/src/Opc.Ua.Core/Stack/DataChannels/Model/DataChannelModel.cs b/src/Opc.Ua.Core.Channels/DataChannels/Model/DataChannelModel.cs similarity index 100% rename from src/Opc.Ua.Core/Stack/DataChannels/Model/DataChannelModel.cs rename to src/Opc.Ua.Core.Channels/DataChannels/Model/DataChannelModel.cs diff --git a/src/Opc.Ua.Core/Stack/DataChannels/Scheduling/DataChannelSendQueue.cs b/src/Opc.Ua.Core.Channels/DataChannels/Scheduling/DataChannelSendQueue.cs similarity index 100% rename from src/Opc.Ua.Core/Stack/DataChannels/Scheduling/DataChannelSendQueue.cs rename to src/Opc.Ua.Core.Channels/DataChannels/Scheduling/DataChannelSendQueue.cs diff --git a/src/Opc.Ua.Core/Stack/DataChannels/Sequencing/DataChannelReceiveWindow.cs b/src/Opc.Ua.Core.Channels/DataChannels/Sequencing/DataChannelReceiveWindow.cs similarity index 100% rename from src/Opc.Ua.Core/Stack/DataChannels/Sequencing/DataChannelReceiveWindow.cs rename to src/Opc.Ua.Core.Channels/DataChannels/Sequencing/DataChannelReceiveWindow.cs diff --git a/src/Opc.Ua.Core/Stack/DataChannels/Sequencing/DataChannelSequence.cs b/src/Opc.Ua.Core.Channels/DataChannels/Sequencing/DataChannelSequence.cs similarity index 100% rename from src/Opc.Ua.Core/Stack/DataChannels/Sequencing/DataChannelSequence.cs rename to src/Opc.Ua.Core.Channels/DataChannels/Sequencing/DataChannelSequence.cs diff --git a/src/Opc.Ua.Core/Stack/DataChannels/Services/DataChannelNegotiator.cs b/src/Opc.Ua.Core.Channels/DataChannels/Services/DataChannelNegotiator.cs similarity index 100% rename from src/Opc.Ua.Core/Stack/DataChannels/Services/DataChannelNegotiator.cs rename to src/Opc.Ua.Core.Channels/DataChannels/Services/DataChannelNegotiator.cs diff --git a/src/Opc.Ua.Core/Stack/DataChannels/Services/DataChannelServiceHandler.cs b/src/Opc.Ua.Core.Channels/DataChannels/Services/DataChannelServiceHandler.cs similarity index 100% rename from src/Opc.Ua.Core/Stack/DataChannels/Services/DataChannelServiceHandler.cs rename to src/Opc.Ua.Core.Channels/DataChannels/Services/DataChannelServiceHandler.cs diff --git a/src/Opc.Ua.Core/Stack/DataChannels/Services/DataChannelSourceRegistry.cs b/src/Opc.Ua.Core.Channels/DataChannels/Services/DataChannelSourceRegistry.cs similarity index 100% rename from src/Opc.Ua.Core/Stack/DataChannels/Services/DataChannelSourceRegistry.cs rename to src/Opc.Ua.Core.Channels/DataChannels/Services/DataChannelSourceRegistry.cs diff --git a/src/Opc.Ua.Core/Stack/DataChannels/Services/QuicPeerBinding.cs b/src/Opc.Ua.Core.Channels/DataChannels/Services/QuicPeerBinding.cs similarity index 100% rename from src/Opc.Ua.Core/Stack/DataChannels/Services/QuicPeerBinding.cs rename to src/Opc.Ua.Core.Channels/DataChannels/Services/QuicPeerBinding.cs diff --git a/src/Opc.Ua.Core.Channels/IServerDataChannelTransport.cs b/src/Opc.Ua.Core.Channels/IServerDataChannelTransport.cs new file mode 100644 index 0000000000..68cdefa0bf --- /dev/null +++ b/src/Opc.Ua.Core.Channels/IServerDataChannelTransport.cs @@ -0,0 +1,109 @@ +/* ======================================================================== + * Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ + +using System.Threading; +using System.Threading.Tasks; + +namespace Opc.Ua.Bindings +{ + /// + /// The transport a Server binds an accepted OpenDataChannel to. + /// + /// + /// A data channel rides a transport that already exists, so the Service + /// Set needs a way to reach it from the SecureChannel a request arrived + /// on. Inline framing and the outer-protocol transports answer this + /// differently: inline multiplexes on the ChannelId in the frame header, + /// while opc.quic binds each channel to its own stream. + /// + public interface IServerDataChannelTransport + { + /// + /// Resolves the data channel engine for a SecureChannel, if this + /// transport carries it. + /// + /// The SecureChannel the request + /// arrived on. + /// What the Server advertises. + /// Telemetry context. + /// The engine that carries the channels. + /// The largest payload a frame may carry + /// on this transport. + /// False when frames can be genuinely lost + /// in flight. + bool TryGetManager( + SecureChannelContext secureChannelContext, + DataChannelServerCapabilities capabilities, + ITelemetryContext telemetry, + out DataChannelManager manager, + out uint maxFrameSize, + out bool isReliable); + + /// + /// Opens the transport stream for a direction this peer initiates, and + /// returns the identifier to carry in revisedTransportChannelId. + /// + /// The SecureChannel. + /// The data channel. + /// The negotiated direction. + /// Cancellation token. + ValueTask AllocateServerStreamAsync( + SecureChannelContext secureChannelContext, + uint channelId, + DataChannelDirection direction, + CancellationToken ct); + + /// + /// Binds a data channel to the transport stream the Client named. + /// + /// + /// The identifier is taken straight from the requester, so an + /// implementation validates it before it binds anything and refuses + /// rather than echoing a value it has not checked. + /// + /// The SecureChannel. + /// The data channel. + /// The identifier from transportChannelId. + /// The negotiated direction. + /// Cancellation token. + ValueTask BindClientStreamAsync( + SecureChannelContext secureChannelContext, + uint channelId, + ulong streamId, + DataChannelDirection direction, + CancellationToken ct); + + /// + /// Tears down every data channel on a SecureChannel. + /// + /// The SecureChannel. + /// Why the channels cannot continue. + void AbortSecureChannel(SecureChannelContext secureChannelContext, StatusCode reason); + } +} diff --git a/src/Opc.Ua.Server/Server/InlineServerDataChannelTransport.cs b/src/Opc.Ua.Core.Channels/InlineServerDataChannelTransport.cs similarity index 95% rename from src/Opc.Ua.Server/Server/InlineServerDataChannelTransport.cs rename to src/Opc.Ua.Core.Channels/InlineServerDataChannelTransport.cs index 4e0bc67f69..b5d069c54d 100644 --- a/src/Opc.Ua.Server/Server/InlineServerDataChannelTransport.cs +++ b/src/Opc.Ua.Core.Channels/InlineServerDataChannelTransport.cs @@ -30,9 +30,9 @@ using System; using System.Threading; using System.Threading.Tasks; -using Opc.Ua.Bindings; -namespace Opc.Ua.Server + +namespace Opc.Ua.Bindings { /// /// Carries data channels over the SecureChannel that already exists, @@ -81,7 +81,7 @@ namespace Opc.Ua.Server return false; } - if (!UaSCDataChannelSecureChannelRegistry.TryGet( + if (!UaSCSecureChannelRegistry.TryGet( secureChannelContext.SecureChannelId, out UaSCUaBinaryChannel? channel) || channel == null) @@ -95,7 +95,7 @@ namespace Opc.Ua.Server capabilities.MaxDataChannels, capabilities.MaxCreditPerChannel); - int body = channel.MaxDataChannelBodySize; + int body = channel.GetMaxDataChannelBodySize(); maxFrameSize = body <= 0 ? 0 : (uint)body; // Inline framing rides the SecureChannel, which is ordered and @@ -132,12 +132,12 @@ namespace Opc.Ua.Server public void AbortSecureChannel(SecureChannelContext secureChannelContext, StatusCode reason) { if (secureChannelContext != null && - UaSCDataChannelSecureChannelRegistry.TryGet( + UaSCSecureChannelRegistry.TryGet( secureChannelContext.SecureChannelId, out UaSCUaBinaryChannel? channel) && channel != null) { - channel.DataChannels?.AbortAll(reason); + channel.GetDataChannels()?.AbortAll(reason); } } diff --git a/src/Opc.Ua.Core.Channels/Properties/AssemblyInfo.cs b/src/Opc.Ua.Core.Channels/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..7798c9bd57 --- /dev/null +++ b/src/Opc.Ua.Core.Channels/Properties/AssemblyInfo.cs @@ -0,0 +1,32 @@ +/* ======================================================================== + * Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ + +using System; + +[assembly: CLSCompliant(false)] diff --git a/src/Opc.Ua.Core.Channels/UaSCDataChannelExtensions.cs b/src/Opc.Ua.Core.Channels/UaSCDataChannelExtensions.cs new file mode 100644 index 0000000000..1a39843ed7 --- /dev/null +++ b/src/Opc.Ua.Core.Channels/UaSCDataChannelExtensions.cs @@ -0,0 +1,161 @@ +/* ======================================================================== + * Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ + +using System; + +namespace Opc.Ua.Bindings +{ + /// + /// Attaches inline data channel framing to a UASC binary channel. + /// + /// + /// A frame is one STR MessageChunk, written to the connection exactly as a + /// MSG chunk is. The channel itself knows nothing of the frame format; it + /// carries the MessageType on behalf of the extension registered here. + /// + public static class UaSCDataChannelExtensions + { + /// + /// Enables the data channel feature on a SecureChannel. + /// + /// + /// Experimental. Until this is called the STR dispatch is inert and an + /// incoming frame closes the SecureChannel, which is what the + /// interoperability rule of the Part 6 errata 5.16 requires of a peer + /// that does not implement this specification. + /// + /// The SecureChannel that carries the frames. + /// True on the server side, which allocates + /// ChannelIds. + /// Telemetry context. + /// The most channels kept open at once + /// on this SecureChannel. + /// The largest window granted to one + /// channel. + /// is null. + public static DataChannelManager EnableDataChannels( + this UaSCUaBinaryChannel channel, + bool isServer, + ITelemetryContext telemetry, + ushort maxDataChannels = 16, + uint maxCreditPerChannel = 1024 * 1024) + { + if (channel == null) + { + throw new ArgumentNullException(nameof(channel)); + } + + if (channel.TryGetDataChannelExtension(out DataChannelExtension? existing) && + existing != null) + { + return existing.Manager; + } + + var extension = new DataChannelExtension( + channel, + isServer, + telemetry, + maxDataChannels, + maxCreditPerChannel); + + var registered = (DataChannelExtension)channel.RegisterMessageExtension(extension); + + if (!ReferenceEquals(registered, extension)) + { + _ = extension.Manager.DisposeAsync().AsTask(); + } + + return registered.Manager; + } + + /// + /// Returns the data channels multiplexed onto a SecureChannel, or + /// null when the feature has not been enabled on it. + /// + /// The SecureChannel. + /// is null. + public static DataChannelManager? GetDataChannels(this UaSCUaBinaryChannel channel) + { + return channel.TryGetDataChannelExtension(out DataChannelExtension? extension) + ? extension?.Manager + : null; + } + + /// + /// Returns the extension that owns the STR MessageType on a + /// SecureChannel, if one is registered. + /// + /// The SecureChannel. + /// The extension. + /// is null. + public static bool TryGetDataChannelExtension( + this UaSCUaBinaryChannel channel, + out DataChannelExtension? extension) + { + if (channel == null) + { + throw new ArgumentNullException(nameof(channel)); + } + + if (channel.TryGetMessageExtension( + TcpMessageType.Stream, + out ISecureChannelMessageExtension? registered) && + registered is DataChannelExtension dataChannels) + { + extension = dataChannels; + return true; + } + + extension = null; + return false; + } + + /// + /// The largest secured body a data channel frame may occupy on a + /// SecureChannel. + /// + /// The SecureChannel. + /// is null. + public static int GetMaxDataChannelBodySize(this UaSCUaBinaryChannel channel) + { + if (channel == null) + { + throw new ArgumentNullException(nameof(channel)); + } + + ISecureChannelMessageHost host = channel; + + return DataChannelFrameCodec.MaxPayload( + DataChannelFramingMode.Inline, + host.SendBufferSize, + host.SymmetricSignatureSize + 2, + withDeadline: true); + } + } +} diff --git a/src/Opc.Ua.Core/EventIds.cs b/src/Opc.Ua.Core/EventIds.cs index d5b57a1e1d..ea4eeb052e 100644 --- a/src/Opc.Ua.Core/EventIds.cs +++ b/src/Opc.Ua.Core/EventIds.cs @@ -103,7 +103,5 @@ namespace Opc.Ua public const int X509CertificateStore = 570; public const int X509CrlHelper = 580; public const int CryptoProvider = 590; - public const int DataChannel = 600; - public const int DataChannelManager = 620; } } diff --git a/src/Opc.Ua.Core/Stack/DataChannelFeature.cs b/src/Opc.Ua.Core/Stack/DataChannelFeature.cs new file mode 100644 index 0000000000..eb180c3908 --- /dev/null +++ b/src/Opc.Ua.Core/Stack/DataChannelFeature.cs @@ -0,0 +1,58 @@ +/* ======================================================================== + * Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ + +namespace Opc.Ua +{ + /// + /// Marks the DataChannel Service Set as experimental. + /// + /// + /// The Services themselves are generated into the standard type surface + /// and are reachable from any Client, while the engine that carries the + /// frames ships separately in + /// OPCFoundation.NetStandard.Opc.Ua.Core.Channels. The marker + /// therefore lives here, where both the Services and their callers can see + /// it: every identifier the feature uses is provisional and will change if + /// and when the OPC Foundation assigns final values. + /// + public static class DataChannelFeature + { + /// + /// The diagnostic id reported for the experimental DataChannel API. + /// + public const string ExperimentalDiagnosticId = "DataChannels"; + + /// + /// The documentation url reported alongside + /// . + /// + public const string ExperimentalUrlFormat = + "https://github.com/OPCFoundation/UA-.NETStandard/blob/master/docs/DataChannels.md"; + } +} diff --git a/src/Opc.Ua.Core/Stack/Tcp/ISecureChannelMessageExtension.cs b/src/Opc.Ua.Core/Stack/Tcp/ISecureChannelMessageExtension.cs index efb90aa0a4..70913dd14f 100644 --- a/src/Opc.Ua.Core/Stack/Tcp/ISecureChannelMessageExtension.cs +++ b/src/Opc.Ua.Core/Stack/Tcp/ISecureChannelMessageExtension.cs @@ -159,17 +159,17 @@ namespace Opc.Ua.Bindings TimeProvider TimeProvider { get; } /// - /// How many SequenceNumbers this channel has issued under the + /// How much of the SequenceNumber space remains under the /// SecurityToken currently in force. /// /// - /// The SequenceNumber space is per token and is shared by every - /// MessageType the channel carries, so an extension that emits a large - /// number of messages has to watch it. Read this inside the callback - /// passed to to observe it consistently - /// with the message being sent. + /// The space is per token and is shared by every MessageType the + /// channel carries, so an extension that emits a large number of + /// messages has to draw on this rather than assume it is alone. + /// Consume it inside the callback passed to , + /// which is where the reservation and the send are atomic. /// - long SequenceNumbersIssued { get; } + SequenceNumberBudget SequenceBudget { get; } /// /// Reports a fault whose blast radius is the whole SecureChannel. diff --git a/src/Opc.Ua.Core/Stack/DataChannels/Sequencing/DataChannelSequenceBudget.cs b/src/Opc.Ua.Core/Stack/Tcp/SequenceNumberBudget.cs similarity index 91% rename from src/Opc.Ua.Core/Stack/DataChannels/Sequencing/DataChannelSequenceBudget.cs rename to src/Opc.Ua.Core/Stack/Tcp/SequenceNumberBudget.cs index 75d6723a99..249220b916 100644 --- a/src/Opc.Ua.Core/Stack/DataChannels/Sequencing/DataChannelSequenceBudget.cs +++ b/src/Opc.Ua.Core/Stack/Tcp/SequenceNumberBudget.cs @@ -64,7 +64,7 @@ namespace Opc.Ua.Bindings /// than emitting a chunk that would reuse a value. /// /// - public sealed class DataChannelSequenceBudget + public sealed class SequenceNumberBudget { /// /// Creates a budget. @@ -73,7 +73,7 @@ namespace Opc.Ua.Bindings /// emission rate. /// The number of SequenceNumbers usable /// under one token. Defaults to the 32 bit space. - public DataChannelSequenceBudget( + public SequenceNumberBudget( TimeProvider? timeProvider = null, uint capacity = uint.MaxValue) { @@ -142,14 +142,13 @@ namespace Opc.Ua.Bindings { get { - double perMinute = ChunksPerSecond * - DataChannelConstants.SequenceNumberRenewalSeconds; + double perMinute = ChunksPerSecond * SequenceNumberRenewalSeconds; long rateBased = perMinute >= long.MaxValue ? long.MaxValue : (long)perMinute; - return Math.Min(DataChannelConstants.SequenceNumberRenewalHeadroom, rateBased); + return Math.Min(SequenceNumberRenewalHeadroom, rateBased); } } @@ -230,5 +229,18 @@ namespace Opc.Ua.Bindings private readonly TimeProvider m_timeProvider; private long m_consumed; private long m_tokenStarted; + + /// + /// The SequenceNumber headroom below which a channel initiates token + /// renewal ahead of the normal lifetime schedule. + /// + private const uint SequenceNumberRenewalHeadroom = 1u << 30; + + /// + /// The window, in seconds, of expected chunk emission that also + /// triggers renewal when it exceeds the remaining SequenceNumber + /// space. + /// + private const int SequenceNumberRenewalSeconds = 60; } } diff --git a/src/Opc.Ua.Core/Stack/Tcp/TcpListenerChannel.cs b/src/Opc.Ua.Core/Stack/Tcp/TcpListenerChannel.cs index c82c93575b..eaa6cabf25 100644 --- a/src/Opc.Ua.Core/Stack/Tcp/TcpListenerChannel.cs +++ b/src/Opc.Ua.Core/Stack/Tcp/TcpListenerChannel.cs @@ -150,7 +150,7 @@ namespace Opc.Ua.Bindings /// protected override void Dispose(bool disposing) { - UaSCDataChannelSecureChannelRegistry.Unbind(GlobalChannelId, this); + UaSCSecureChannelRegistry.Unbind(GlobalChannelId, this); base.Dispose(disposing); } @@ -268,7 +268,7 @@ namespace Opc.Ua.Bindings boundTransport.OnSecureChannelAttached(GlobalChannelId); } - UaSCDataChannelSecureChannelRegistry.Bind(GlobalChannelId, this); + UaSCSecureChannelRegistry.Bind(GlobalChannelId, this); m_logger.TcpListenChannelLog0( ChannelName, diff --git a/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.DataChannels.cs b/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.DataChannels.cs deleted file mode 100644 index 0c26a0e17d..0000000000 --- a/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.DataChannels.cs +++ /dev/null @@ -1,180 +0,0 @@ -/* ======================================================================== - * Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved. - * - * OPC Foundation MIT License 1.00 - * - * Permission is hereby granted, free of charge, to any person - * obtaining a copy of this software and associated documentation - * files (the "Software"), to deal in the Software without - * restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following - * conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - * OTHER DEALINGS IN THE SOFTWARE. - * - * The complete license agreement can be found here: - * http://opcfoundation.org/License/MIT/1.00/ - * ======================================================================*/ - -using System.Collections.Concurrent; - -namespace Opc.Ua.Bindings -{ - /// - /// Inline data channel framing over the UASC binary channel, expressed as - /// an extension that owns the STR MessageType. - /// - public partial class UaSCUaBinaryChannel - { - /// - /// The data channels multiplexed onto this SecureChannel, or null - /// when the feature is not enabled. - /// - /// - /// Experimental. Until is called the - /// STR dispatch is inert and an incoming frame closes the - /// SecureChannel, which is what the interoperability rule of the - /// Part 6 errata 5.16 requires of a peer that does not implement this - /// specification. - /// - public DataChannelManager? DataChannels => m_dataChannels?.Manager; - - /// - /// Enables the data channel feature on this SecureChannel. - /// - /// True on the server side, which - /// allocates ChannelIds. - /// Telemetry context. - /// The most channels kept open at - /// once on this SecureChannel. - /// The largest window granted - /// to one channel. - public DataChannelManager EnableDataChannels( - bool isServer, - ITelemetryContext telemetry, - ushort maxDataChannels = 16, - uint maxCreditPerChannel = 1024 * 1024) - { - DataChannelExtension? existing = m_dataChannels; - - if (existing != null) - { - return existing.Manager; - } - - var extension = new DataChannelExtension( - this, - isServer, - telemetry, - maxDataChannels, - maxCreditPerChannel); - - var registered = (DataChannelExtension)RegisterMessageExtension(extension); - - if (ReferenceEquals(registered, extension)) - { - m_isDataChannelSource = isServer; - } - else - { - _ = extension.Manager.DisposeAsync().AsTask(); - } - - m_dataChannels = registered; - return registered.Manager; - } - - /// - /// Tracks how much of the SecureChannel SequenceNumber space - /// remains under the current SecurityToken. STR, MSG, OPN and CLO - /// chunks all consume the same sender SequenceNumber space. - /// - public DataChannelSequenceBudget SequenceBudget - { - get - { - m_sequenceBudget.ObserveConsumed(SequenceNumbersIssuedUnderCurrentToken); - return m_sequenceBudget; - } - } - - /// - /// True when the SequenceNumber space remaining under the current - /// token has fallen below the renewal threshold, so the owning - /// channel should initiate OpenSecureChannel with - /// RenewalRequest ahead of the normal lifetime based renewal. - /// - public bool IsSequenceRenewalDue => SequenceBudget.ShouldRenew; - - /// - /// The largest secured body a data channel frame may occupy on - /// this SecureChannel. - /// - /// - /// Public because a Server has to advertise this ceiling when it - /// negotiates a channel, and the negotiation happens in the Server - /// assembly rather than here. - /// - public int MaxDataChannelBodySize - => DataChannelFrameCodec.MaxPayload( - DataChannelFramingMode.Inline, - SendBufferSize, - SymmetricSignatureSize + 2, - withDeadline: true); - - private readonly DataChannelSequenceBudget m_sequenceBudget = new(); - private DataChannelExtension? m_dataChannels; - private bool m_isDataChannelSource; - } - - /// - /// Maps server-side SecureChannel identifiers to their UASC channels so - /// the DataChannel Service Set can bind an accepted OpenDataChannel to the - /// transport that will carry its STR frames. - /// - public static class UaSCDataChannelSecureChannelRegistry - { - /// - /// Finds the server-side UASC channel that owns a SecureChannel. - /// - /// The SecureChannel identifier. - /// The channel that owns it. - public static bool TryGet( - string secureChannelId, - out UaSCUaBinaryChannel? channel) - { - return s_channels.TryGetValue(secureChannelId, out channel); - } - - internal static void Bind(string secureChannelId, UaSCUaBinaryChannel channel) - { - if (!string.IsNullOrEmpty(secureChannelId)) - { - s_channels[secureChannelId] = channel; - } - } - - internal static void Unbind(string secureChannelId, UaSCUaBinaryChannel channel) - { - if (!string.IsNullOrEmpty(secureChannelId) && - s_channels.TryGetValue(secureChannelId, out UaSCUaBinaryChannel? current) && - ReferenceEquals(current, channel)) - { - s_channels.TryRemove(secureChannelId, out _); - } - } - - private static readonly ConcurrentDictionary s_channels = new(); - } -} diff --git a/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.MessageExtensions.cs b/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.MessageExtensions.cs index 2ae3e1b808..95cd05c753 100644 --- a/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.MessageExtensions.cs +++ b/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.MessageExtensions.cs @@ -56,7 +56,29 @@ namespace Opc.Ua.Bindings TimeProvider ISecureChannelMessageHost.TimeProvider => TimeProvider; /// - long ISecureChannelMessageHost.SequenceNumbersIssued => SequenceNumbersIssuedUnderCurrentToken; + SequenceNumberBudget ISecureChannelMessageHost.SequenceBudget => SequenceBudget; + + /// + /// Tracks how much of the SecureChannel SequenceNumber space remains + /// under the current SecurityToken. Every MessageType the channel + /// carries draws on the same space. + /// + public SequenceNumberBudget SequenceBudget + { + get + { + m_sequenceBudget.ObserveConsumed(SequenceNumbersIssuedUnderCurrentToken); + return m_sequenceBudget; + } + } + + /// + /// True when the SequenceNumber space remaining under the current + /// token has fallen below the renewal threshold, so the owning channel + /// should initiate OpenSecureChannel with RenewalRequest ahead of the + /// normal lifetime based renewal. + /// + public bool IsSequenceRenewalDue => SequenceBudget.ShouldRenew; /// /// Registers the extension that owns a MessageType on this channel. @@ -244,6 +266,8 @@ namespace Opc.Ua.Bindings ref m_sequenceNumberBaseline, Interlocked.Read(ref m_sequenceNumber)); + m_sequenceBudget.OnTokenActivated(); + if (m_messageExtensions.IsEmpty) { return; @@ -305,6 +329,7 @@ namespace Opc.Ua.Bindings private readonly ConcurrentDictionary m_messageExtensions = new(); + private readonly SequenceNumberBudget m_sequenceBudget = new(); private long m_sequenceNumberBaseline; } } diff --git a/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs b/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs index 8efec94d93..aaa7305632 100644 --- a/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs +++ b/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs @@ -199,11 +199,10 @@ namespace Opc.Ua.Bindings CurrentToken = token; RenewedToken = null; - // The SequenceNumber space is per token, so a new token - // restores the budget an extension stalls against + // The SequenceNumber space is per token, so a new token restores + // the budget every MessageType on the channel draws on // (Part 6 errata 5.1.1). NotifySecurityTokenActivated(); - m_sequenceBudget.OnTokenActivated(); TokenActivatedCallback?.Invoke(token, PreviousToken); diff --git a/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs b/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs index ec99595cfc..0951d57b6d 100644 --- a/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs +++ b/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs @@ -1483,7 +1483,7 @@ namespace Opc.Ua.Bindings if (m_logger.IsEnabled(LogLevel.Information)) { - DataChannelSequenceBudget budget = SequenceBudget; + SequenceNumberBudget budget = SequenceBudget; m_logger.UaSCClientLog37( ChannelId, diff --git a/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs b/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs index a8f55983f6..d6e1174b1d 100644 --- a/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs +++ b/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs @@ -150,29 +150,18 @@ namespace Opc.Ua.Bindings /// public TransportChannelFeatures SupportedFeatures => (m_channel?.Transport?.Features ?? TransportChannelFeatures.None) | - TransportChannelFeatures.DataChannels; + TransportChannelFeatures.MessageExtensions; /// - /// Enables OPC UA data channels on the current SecureChannel. + /// The SecureChannel behind this transport channel, or null + /// while it is not connected. /// - public DataChannelManager EnableDataChannels( - bool isServer, - ITelemetryContext telemetry, - ushort maxDataChannels = 16, - uint maxCreditPerChannel = 1024 * 1024) - { - UaSCUaBinaryClientChannel channel = m_channel ?? throw BadNotConnected(); - return channel.EnableDataChannels( - isServer, - telemetry, - maxDataChannels, - maxCreditPerChannel); - } - - /// - /// The data channels multiplexed onto the current SecureChannel, if enabled. - /// - public DataChannelManager? DataChannels => m_channel?.DataChannels; + /// + /// Exposed so a message extension can attach itself to the channel a + /// Client is already using. It is the channel, not this wrapper, that + /// carries the MessageType an extension owns. + /// + public UaSCUaBinaryClientChannel? SecureChannel => m_channel; /// public EndpointDescription EndpointDescription diff --git a/src/Opc.Ua.Core/Stack/Tcp/UaSCSecureChannelRegistry.cs b/src/Opc.Ua.Core/Stack/Tcp/UaSCSecureChannelRegistry.cs new file mode 100644 index 0000000000..7088dafc3c --- /dev/null +++ b/src/Opc.Ua.Core/Stack/Tcp/UaSCSecureChannelRegistry.cs @@ -0,0 +1,95 @@ +/* ======================================================================== + * Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ + +using System.Collections.Concurrent; + +namespace Opc.Ua.Bindings +{ + /// + /// Maps server-side SecureChannel identifiers to the UASC channels that + /// own them. + /// + /// + /// A Service request carries a SecureChannel identifier rather than the + /// channel object, so anything that has to reach the transport behind a + /// request - a message extension binding itself to the connection the + /// request arrived on, for example - needs this lookup. + /// + /// TODO: This is process-global rather than scoped to the listener that + /// owns the channels, which means two servers in one process share it. + /// Scoping it to the listener requires threading a registry instance + /// through the transport bindings and is left as separate work. + /// + /// + public static class UaSCSecureChannelRegistry + { + /// + /// Finds the server-side UASC channel that owns a SecureChannel. + /// + /// The SecureChannel identifier. + /// The channel that owns it. + public static bool TryGet( + string secureChannelId, + out UaSCUaBinaryChannel? channel) + { + return s_channels.TryGetValue(secureChannelId, out channel); + } + + /// + /// Records the channel that owns a SecureChannel. + /// + /// The SecureChannel identifier. + /// The channel that owns it. + public static void Bind(string secureChannelId, UaSCUaBinaryChannel channel) + { + if (!string.IsNullOrEmpty(secureChannelId)) + { + s_channels[secureChannelId] = channel; + } + } + + /// + /// Releases the record for a SecureChannel, if the channel given still + /// owns it. + /// + /// The SecureChannel identifier. + /// The channel that owned it. + public static void Unbind(string secureChannelId, UaSCUaBinaryChannel channel) + { + if (!string.IsNullOrEmpty(secureChannelId) && + s_channels.TryGetValue(secureChannelId, out UaSCUaBinaryChannel? current) && + ReferenceEquals(current, channel)) + { + s_channels.TryRemove(secureChannelId, out _); + } + } + + private static readonly ConcurrentDictionary s_channels = new(); + } +} diff --git a/src/Opc.Ua.Core/Stack/Transport/ITransportChannel.cs b/src/Opc.Ua.Core/Stack/Transport/ITransportChannel.cs index 360d5b4989..ce689e6a6c 100644 --- a/src/Opc.Ua.Core/Stack/Transport/ITransportChannel.cs +++ b/src/Opc.Ua.Core/Stack/Transport/ITransportChannel.cs @@ -201,10 +201,10 @@ namespace Opc.Ua ReverseConnect = 0x0040, /// - /// The channel can multiplex OPC UA data channels onto the - /// SecureChannel. Experimental. + /// The channel can carry MessageTypes owned by a registered + /// extension, for example OPC UA data channels. Experimental. /// - DataChannels = 0x0080, + MessageExtensions = 0x0080, /// /// The transport under the channel provides independently diff --git a/src/Opc.Ua.Redundancy.Client/RedundantClientSession.cs b/src/Opc.Ua.Redundancy.Client/RedundantClientSession.cs index fafd7906d6..dce50c7e66 100644 --- a/src/Opc.Ua.Redundancy.Client/RedundantClientSession.cs +++ b/src/Opc.Ua.Redundancy.Client/RedundantClientSession.cs @@ -1237,8 +1237,8 @@ namespace Opc.Ua.Redundancy.Client /// #if NET8_0_OR_GREATER [System.Diagnostics.CodeAnalysis.Experimental( - Opc.Ua.Bindings.DataChannelConstants.ExperimentalDiagnosticId, - UrlFormat = Opc.Ua.Bindings.DataChannelConstants.ExperimentalUrlFormat)] + Opc.Ua.DataChannelFeature.ExperimentalDiagnosticId, + UrlFormat = Opc.Ua.DataChannelFeature.ExperimentalUrlFormat)] #endif public async ValueTask OpenDataChannelAsync( RequestHeader? requestHeader, @@ -1262,8 +1262,8 @@ namespace Opc.Ua.Redundancy.Client /// #if NET8_0_OR_GREATER [System.Diagnostics.CodeAnalysis.Experimental( - Opc.Ua.Bindings.DataChannelConstants.ExperimentalDiagnosticId, - UrlFormat = Opc.Ua.Bindings.DataChannelConstants.ExperimentalUrlFormat)] + Opc.Ua.DataChannelFeature.ExperimentalDiagnosticId, + UrlFormat = Opc.Ua.DataChannelFeature.ExperimentalUrlFormat)] #endif public async ValueTask ModifyDataChannelAsync( RequestHeader? requestHeader, @@ -1283,8 +1283,8 @@ namespace Opc.Ua.Redundancy.Client /// #if NET8_0_OR_GREATER [System.Diagnostics.CodeAnalysis.Experimental( - Opc.Ua.Bindings.DataChannelConstants.ExperimentalDiagnosticId, - UrlFormat = Opc.Ua.Bindings.DataChannelConstants.ExperimentalUrlFormat)] + Opc.Ua.DataChannelFeature.ExperimentalDiagnosticId, + UrlFormat = Opc.Ua.DataChannelFeature.ExperimentalUrlFormat)] #endif public async ValueTask CloseDataChannelAsync( RequestHeader? requestHeader, diff --git a/src/Opc.Ua.Server/Opc.Ua.Server.csproj b/src/Opc.Ua.Server/Opc.Ua.Server.csproj index 785fa43496..53486b6272 100644 --- a/src/Opc.Ua.Server/Opc.Ua.Server.csproj +++ b/src/Opc.Ua.Server/Opc.Ua.Server.csproj @@ -35,6 +35,7 @@ + diff --git a/src/Opc.Ua.Server/Server/StandardServer.DataChannels.cs b/src/Opc.Ua.Server/Server/StandardServer.DataChannels.cs index cb3333a28d..161a839893 100644 --- a/src/Opc.Ua.Server/Server/StandardServer.DataChannels.cs +++ b/src/Opc.Ua.Server/Server/StandardServer.DataChannels.cs @@ -16,35 +16,6 @@ using Opc.Ua.Bindings; namespace Opc.Ua.Server { - /// - /// Optional transport-side binding for the DataChannel Service Set. - /// - public interface IServerDataChannelTransport - { - bool TryGetManager( - SecureChannelContext secureChannelContext, - DataChannelServerCapabilities capabilities, - ITelemetryContext telemetry, - out DataChannelManager manager, - out uint maxFrameSize, - out bool isReliable); - - ValueTask AllocateServerStreamAsync( - SecureChannelContext secureChannelContext, - uint channelId, - DataChannelDirection direction, - CancellationToken ct); - - ValueTask BindClientStreamAsync( - SecureChannelContext secureChannelContext, - uint channelId, - ulong streamId, - DataChannelDirection direction, - CancellationToken ct); - - void AbortSecureChannel(SecureChannelContext secureChannelContext, StatusCode reason); - } - public partial class StandardServer { public DataChannelSourceRegistry DataChannelSources { get; } = new(); diff --git a/tests/Opc.Ua.Core.DataChannels.Tests/DataChannelConformanceTests.cs b/tests/Opc.Ua.Core.DataChannels.Tests/DataChannelConformanceTests.cs index e539563b5f..e345eb2b0c 100644 --- a/tests/Opc.Ua.Core.DataChannels.Tests/DataChannelConformanceTests.cs +++ b/tests/Opc.Ua.Core.DataChannels.Tests/DataChannelConformanceTests.cs @@ -126,14 +126,14 @@ namespace Opc.Ua.Core.DataChannels.Tests channel.AttachTransport(transport); channel.Activate(SecureChannelId, TokenId); DataChannel sink = OpenSink(channel); - await PumpSchedulerRoundAsync(channel.DataChannels!).ConfigureAwait(false); + await PumpSchedulerRoundAsync(channel.GetDataChannels()!).ConfigureAwait(false); transport.ClearChunks(); byte[] chunk = SpecVectors.Load("inline_data_first"); chunk[SpecVectors.InlinePrefix + 4] = frameType; Assert.That(channel.DispatchStream(chunk), Is.False); - await PumpSchedulerRoundAsync(channel.DataChannels!).ConfigureAwait(false); + await PumpSchedulerRoundAsync(channel.GetDataChannels()!).ConfigureAwait(false); DataChannelFrame reset = await WaitForOutboundFrameAsync(transport, DataChannelFrameType.Reset) .ConfigureAwait(false); @@ -151,7 +151,7 @@ namespace Opc.Ua.Core.DataChannels.Tests Assert.That(expectedDecodeError.ToStatusCode(), Is.EqualTo(expectedStatus)); }); - await channel.DataChannels!.DisposeAsync().ConfigureAwait(false); + await channel.GetDataChannels()!.DisposeAsync().ConfigureAwait(false); } [Test] diff --git a/tests/Opc.Ua.Core.DataChannels.Tests/DataChannelSequenceBudgetTests.cs b/tests/Opc.Ua.Core.DataChannels.Tests/DataChannelSequenceBudgetTests.cs index d215cae66e..c29add8eea 100644 --- a/tests/Opc.Ua.Core.DataChannels.Tests/DataChannelSequenceBudgetTests.cs +++ b/tests/Opc.Ua.Core.DataChannels.Tests/DataChannelSequenceBudgetTests.cs @@ -13,13 +13,13 @@ namespace Opc.Ua.Core.DataChannels.Tests { [TestFixture] [Category("DataChannels")] - public sealed class DataChannelSequenceBudgetTests + public sealed class SequenceNumberBudgetTests { [Test] public void ObservedServiceChunksContributeToRenewalAndStallBudget() { var clock = new FakeTimeProvider(DateTimeOffset.UtcNow); - var budget = new DataChannelSequenceBudget(clock, capacity: 5); + var budget = new SequenceNumberBudget(clock, capacity: 5); budget.ObserveConsumed(4); clock.Advance(TimeSpan.FromSeconds(1)); @@ -45,7 +45,7 @@ namespace Opc.Ua.Core.DataChannels.Tests [Test] public void TokenActivationResetsObservedAndConsumedBudget() { - var budget = new DataChannelSequenceBudget(capacity: 3); + var budget = new SequenceNumberBudget(capacity: 3); budget.ObserveSequenceNumber(2); Assert.That(budget.MustStall, Is.True); diff --git a/tests/Opc.Ua.Core.DataChannels.Tests/DataChannelSequencingTests.cs b/tests/Opc.Ua.Core.DataChannels.Tests/DataChannelSequencingTests.cs index 908f2598fc..a916978d14 100644 --- a/tests/Opc.Ua.Core.DataChannels.Tests/DataChannelSequencingTests.cs +++ b/tests/Opc.Ua.Core.DataChannels.Tests/DataChannelSequencingTests.cs @@ -261,7 +261,7 @@ namespace Opc.Ua.Core.DataChannels.Tests [Test] public void DcF034RenewalIsDueBeforeTheSpaceIsExhausted() { - var budget = new DataChannelSequenceBudget(capacity: 1000); + var budget = new SequenceNumberBudget(capacity: 1000); Assert.That(budget.ShouldRenew, Is.False, "a fresh token has the whole space"); @@ -281,7 +281,7 @@ namespace Opc.Ua.Core.DataChannels.Tests [Test] public void DcF034ASenderStallsRatherThanReusingASequenceNumber() { - var budget = new DataChannelSequenceBudget(capacity: 4); + var budget = new SequenceNumberBudget(capacity: 4); for (int ii = 0; ii < 4; ii++) { @@ -304,7 +304,7 @@ namespace Opc.Ua.Core.DataChannels.Tests [Test] public void ANewTokenRestoresTheWholeSpace() { - var budget = new DataChannelSequenceBudget(capacity: 4); + var budget = new SequenceNumberBudget(capacity: 4); for (int ii = 0; ii < 4; ii++) { @@ -329,7 +329,7 @@ namespace Opc.Ua.Core.DataChannels.Tests var clock = new Microsoft.Extensions.Time.Testing.FakeTimeProvider( new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero)); - var budget = new DataChannelSequenceBudget(clock); + var budget = new SequenceNumberBudget(clock); // Ten chunks over ten seconds is one per second, so one // minute of traffic is sixty values: far below the fixed @@ -359,7 +359,7 @@ namespace Opc.Ua.Core.DataChannels.Tests var clock = new Microsoft.Extensions.Time.Testing.FakeTimeProvider( new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero)); - var budget = new DataChannelSequenceBudget(clock); + var budget = new SequenceNumberBudget(clock); // A hundred million chunks in one second is a rate whose // minute of traffic exceeds the 32 bit space entirely, so the diff --git a/tests/Opc.Ua.Core.DataChannels.Tests/InlineServerDataChannelTransportTests.cs b/tests/Opc.Ua.Core.DataChannels.Tests/InlineServerDataChannelTransportTests.cs index 07e6442ad1..4cddbcded2 100644 --- a/tests/Opc.Ua.Core.DataChannels.Tests/InlineServerDataChannelTransportTests.cs +++ b/tests/Opc.Ua.Core.DataChannels.Tests/InlineServerDataChannelTransportTests.cs @@ -62,7 +62,7 @@ namespace Opc.Ua.Core.DataChannels.Tests channel.Activate(0x0000A17C, 7); const string secureChannelId = "inline-transport-1"; - UaSCDataChannelSecureChannelRegistry.Bind(secureChannelId, channel); + UaSCSecureChannelRegistry.Bind(secureChannelId, channel); try { @@ -84,7 +84,7 @@ namespace Opc.Ua.Core.DataChannels.Tests { Assert.That(maxFrameSize, Is.GreaterThan(0u)); Assert.That(isReliable, Is.True); - Assert.That(manager, Is.SameAs(channel.DataChannels)); + Assert.That(manager, Is.SameAs(channel.GetDataChannels())); }); DataChannel source = manager.Register( @@ -112,7 +112,7 @@ namespace Opc.Ua.Core.DataChannels.Tests } finally { - UaSCDataChannelSecureChannelRegistry.Unbind(secureChannelId, channel); + UaSCSecureChannelRegistry.Unbind(secureChannelId, channel); } } @@ -140,7 +140,7 @@ namespace Opc.Ua.Core.DataChannels.Tests ITelemetryContext telemetry = NUnitTelemetryContext.Create(); using InlineTestChannel channel = InlineTestChannel.Create("inline-profile", telemetry); const string secureChannelId = "inline-transport-2"; - UaSCDataChannelSecureChannelRegistry.Bind(secureChannelId, channel); + UaSCSecureChannelRegistry.Bind(secureChannelId, channel); try { @@ -159,7 +159,7 @@ namespace Opc.Ua.Core.DataChannels.Tests } finally { - UaSCDataChannelSecureChannelRegistry.Unbind(secureChannelId, channel); + UaSCSecureChannelRegistry.Unbind(secureChannelId, channel); } } diff --git a/tests/Opc.Ua.Core.DataChannels.Tests/Opc.Ua.Core.DataChannels.Tests.csproj b/tests/Opc.Ua.Core.DataChannels.Tests/Opc.Ua.Core.DataChannels.Tests.csproj index 3da3c4dae0..6643096cc6 100644 --- a/tests/Opc.Ua.Core.DataChannels.Tests/Opc.Ua.Core.DataChannels.Tests.csproj +++ b/tests/Opc.Ua.Core.DataChannels.Tests/Opc.Ua.Core.DataChannels.Tests.csproj @@ -42,6 +42,7 @@ + diff --git a/tests/Opc.Ua.Core.DataChannels.Tests/UaSCDataChannelDispatchTests.cs b/tests/Opc.Ua.Core.DataChannels.Tests/UaSCDataChannelDispatchTests.cs index 9391954b82..d2a25b7fde 100644 --- a/tests/Opc.Ua.Core.DataChannels.Tests/UaSCDataChannelDispatchTests.cs +++ b/tests/Opc.Ua.Core.DataChannels.Tests/UaSCDataChannelDispatchTests.cs @@ -432,9 +432,11 @@ namespace Opc.Ua.Core.DataChannels.Tests private static async Task DisposeDataChannelsAsync(TestChannel channel) { - if (channel.DataChannels != null) + DataChannelManager? manager = channel.GetDataChannels(); + + if (manager != null) { - await channel.DataChannels.DisposeAsync().ConfigureAwait(false); + await manager.DisposeAsync().ConfigureAwait(false); } }